← 리포트 목록
knowledge 소스 context 통합 wrapper 설계·구현
2026-04-16
knowledge
[knowledge-pipeline, analyst, macro, telegram-context, hermes]
결론
구현 완료. analyst-macro 실행 전 local knowledge 소스에서 텔레그램 해석 글을 우선 압축해 프롬프트에 주입하는 wrapper를 추가했고, 실제 1회 실행 결과 텔레그램 market 토픽으로 message_id=2388 발송까지 확인했다.
핵심 변화는 “수치만 나열”이 아니라 “수치 + 텔레그램 해석 글의 원인/시사점”을 결합하게 만든 것이다. 최신 실행 결과 latest.json에는 knowledge_context_used와 telegram_interpretation_used가 기록됐고, 본문에 유가→CPI 간접 전이, Fed 전망, 나프타 공급망 리스크가 반영됐다.
변경 파일
| 파일 | 변경 |
|---|---|
/Users/ron/.hermes/workspace/scripts/knowledge_context_builder.py |
신규. knowledge/Gmail/Telegram/Blog/Twitter/NEPCON/Credit 소스 context builder |
/Users/ron/.hermes/workspace/scripts/analyst_runner.sh |
macro 실행 시 builder 출력 주입 |
/Users/ron/.hermes/workspace/scripts/analyst_runner.sh.bak-knowledge-context-* |
수정 전 백업 |
1단계 — knowledge 소스 디렉토리 매핑
소스별 위치/포맷/추출 방법
| 소스 | 위치 | 파일 포맷 | 최신 N개 추출 명령 | 평균/특징 |
|---|---|---|---|---|
| Gmail 뉴스레터 | ~/knowledge-agent/100 수신함/121 뉴스레터/ |
Markdown + frontmatter/body | ls -t "$HOME/knowledge-agent/100 수신함/121 뉴스레터"/2604*_*.md | head -N |
40건, 최근 평균 약 2.7KB |
| 텔레그램 채널 | ~/knowledge-agent/100 수신함/118 텔레그램/ |
Markdown + source_channel/source_msgid frontmatter + 요약/body | find "$HOME/knowledge-agent/100 수신함/118 텔레그램" -name '*.md' -type f -print0 | xargs -0 ls -t | head -N |
1,308건, 최근 평균 약 3.3KB. macro는 이 소스 최우선 |
| 매크로·채권 텔레그램 | ~/knowledge-agent/100 수신함/116 croned_data/매크로채권/ |
Markdown + channel/analyst/frontmatter | ls -t "$HOME/knowledge-agent/100 수신함/116 croned_data/매크로채권"/*.md | head -N |
10건, 리서치 전문 길어 평균 큼 |
| 트위터/X | ~/knowledge-agent/**/*twitter*.md; ~/.hermes/workspace/memory/twitter-collector/ |
Markdown. Hermes memory는 현재 파일 0건 | find "$HOME/knowledge-agent" -iname '*twitter*.md' -type f -print0 | xargs -0 ls -t | head -N |
3건, 최근 평균 약 0.9KB |
| 블로그/ranto28 | ~/knowledge-agent/**/*ranto28*.md; 일부 ~/.hermes/workspace/memory/blog-insights/ |
Markdown 또는 JSON | find "$HOME/knowledge-agent" -iname '*ranto28*.md' -type f -print0 | xargs -0 ls -t | head -N |
128건, 최근 평균 약 12KB |
| NEPCON/리서치 | ~/knowledge/100 수신함/120 지식사랑방/124 nepcon/, ~/knowledge-agent/200 아토믹/210 원자노트/*nepcon*.md |
Markdown | find "$HOME/knowledge/100 수신함/120 지식사랑방/124 nepcon" -name '*.md' -type f -print0 | xargs -0 ls -t | head -N |
knowledge 쪽 80건, agent 쪽 2건 |
| 채권 크레딧 메일 | ~/knowledge-agent/100-inbox/119-크레딧메일/, ~/knowledge-agent/100 수신함/119 크레딧메일/ |
Markdown + 메일 본문 | find "$HOME/knowledge-agent" \( -path '*119-크레딧메일*' -o -path '*119 크레딧메일*' \) -name '*.md' -type f -print0 | xargs -0 ls -t | head -N |
59건, 최근 큰 메일 다수 |
builder가 확인한 소스 카운트
[
{
"key": "telegram",
"label": "텔레그램 해석 글",
"roots": "/Users/ron/knowledge-agent/100 수신함/118 텔레그램",
"pattern": "*.md",
"count": 1308,
"avg_recent_bytes": 3294
},
{
"key": "macro_bond_telegram",
"label": "매크로·채권 텔레그램 수집",
"roots": "/Users/ron/knowledge-agent/100 수신함/116 croned_data/매크로채권",
"pattern": "*.md",
"count": 10,
"avg_recent_bytes": 22844
},
{
"key": "newsletter",
"label": "뉴스레터",
"roots": "/Users/ron/knowledge-agent/100 수신함/121 뉴스레터",
"pattern": "2604*_*.md",
"count": 40,
"avg_recent_bytes": 2355
},
{
"key": "blog",
"label": "블로그/ranto28",
"roots": "/Users/ron/knowledge-agent",
"pattern": "*ranto28*.md",
"count": 128,
"avg_recent_bytes": 12494
},
{
"key": "twitter",
"label": "트위터/X",
"roots": "/Users/ron/knowledge-agent",
"pattern": "*twitter*.md",
"count": 3,
"avg_recent_bytes": 878
},
{
"key": "nepcon",
"label": "NEPCON/리서치",
"roots": "/Users/ron/knowledge/100 수신함/120 지식사랑방/124 nepcon; /Users/ron/knowledge-agent/200 아토믹/210 원자노트",
"pattern": "*nepcon*.md",
"count": 4,
"avg_recent_bytes": 654
},
{
"key": "credit",
"label": "채권/크레딧 메일",
"roots": "/Users/ron/knowledge-agent/100-inbox/119-크레딧메일; /Users/ron/knowledge-agent/100 수신함/119 크레딧메일",
"pattern": "*.md",
"count": 59,
"avg_recent_bytes": 30571
}
]
2단계 — 통합 wrapper 설계/구현
설계
- 입력:
--analyst macro|fundamental|technical|pm,--hours,--max-chars - 출력: Markdown context를 stdout 또는
--output으로 출력 - 읽기 방식:
- YAML frontmatter 제거
- title/date/source_channel 추출
- Markdown 링크/이미지 제거
- 본문 첫 의미 문단을 2줄 안팎으로 축약
- 선별 방식:
- analyst별 source limit 분리
- macro는 텔레그램/매크로채권 텔레그램에 최상위 가중치
유가가 인플레이션,연준 전망,공급 충격,3고 시대,국채 안전성같은 해석 글 pin phrase 가중치 부여- 최근 96시간 기준이지만, macro 텔레그램 수집 공백 시 고득점 과거 글로 fallback
- 토큰/길이 가드:
- runner 주입은
--max-chars 5000 - 한국어 byte 수는 더 크므로 로그에는 약 9KB 내외로 보일 수 있음
macro 주입 샘플
# Knowledge Context for analyst-macro
- 생성시각: 2026-04-16 08:45:37
- 기본 범위: 최근 96시간. 단, macro 텔레그램 해석 글은 최근 수집 공백 시 고득점 과거 글까지 보강.
- 사용법: 수치가 아니라 해석·원인·시사점을 1~3개만 반영하고, 본문에 과잉 인용하지 말 것.
- macro 우선순위: 텔레그램 전문가/채널 해석 글 > 매크로채권 수집 > 뉴스레터/블로그. RRP/TGA/CPI 같은 수치는 아래 해석 글과 연결해 설명할 것.
- latest.json에는 가능하면 `knowledge_context_used` 배열로 반영한 항목의 title/source/reason을 기록할 것.
## 최근 텔레그램 해석 글
- **[Statista] 연준 전망: 견조한 성장, 단기적 인플레이션 상승** — getfeed, 2026-03-20
- 요약: [Statista] 연준 전망: 견조한 성장, 단기적 인플레이션 상승 Fed Projections: Solid Growth, Higher Short-Term Inflation 미국 경제에 대한 이란 전쟁의 영향에 대한 높은 불확실성과 어려운 거시 경제 환경에도 불구하고, 연준은 최근 경제 전망에서 신중한 낙관론을 유지하고 있다. 실제로 연준 관계자들은 회복력 있는 소비 지출과 민간 투자의 확대를 언급하며 2026년 GDP 성장 전망을 소폭 상향 조정했다. 연방 공개 시장 위원회(FOMC)의 2026년 중간 성장 전망은 현재 2.4%로, 12월의 2.3%에서, 9월의 1.8%에서 상승했다. 2027년의 경우, FOMC는 2.3% 성장을 예…
- 파일: `~/knowledge-agent/100 수신함/118 텔레그램/전문가들의 마켓 인사이트/2026-03-20/260320_122585_Statista_연준_전망_견조한_성장_단기적_인플레이.md`
- **[원유] 유가 충격과 경제 전망: 공급 충격이 경기 침체에 미치는 영향** — getfeed, 2026-04-01
- 요약: [원유] 유가 충격과 경제 전망: 공급 충격이 경기 침체에 미치는 영향 핵심적 본문 요약 최근 이란 공습 이후 국제 유가가 급등하며, 배럴당 100달러를 넘어섰다. 북미 원유(WTI)는 116달러까지, 브렌트유는 118달러까지 치솟았다. 과거 유가 충격은 충격의 지속성, 정책 대응, 경제 상황에 따라 경제에 미치는 영향이 달랐다. 현재 미국 경제는 1970년대와 달리 원유 의존도가 감소했고 셰일가스 개발로 에너지 자립도가 높아졌다. 그러나 고용 부진, 인플레이션 심화, 실질 소득 감소 등 취약한 경제 상황에서 유가 상승은 추가적인 부담으로 작용하고 있다. 시장은 경기 침체 위험을 높게 평가하며, 전문가들은 25%에서 50%까지 경기 침체…
- 파일: `~/knowledge-agent/100 수신함/118 텔레그램/전문가들의 마켓 인사이트/2026-04-01/260401_124118_원유_유가_충격과_경제_전망_공급_충격이_경기_침체에_.md`
- **유가가 인플레이션에 미치는 영향은 도대체 얼마나 될까? — CPI 6%의** — getfeed, 2026-03-25
- 요약: 유가가 인플레이션에 미치는 영향은 도대체 얼마나 될까? — CPI 6%의 함정 핵심적 본문 요약 4월 10일 발표될 3월 CPI에 시장의 이목이 집중되고 있다. 이는 이란 전쟁 발발 이후 유가 상승이 물가에 미치는 영향을 가늠하는 첫 번째 지표가 될 것이기 때문이다. 에너지 비중은 CPI에서 6.3%로 낮지만, 유가 상승은 직접적인 에너지 가격 상승뿐 아니라 식품, 항공료, 운송비 등 다양한 분야의 비용을 증가시키는 간접적인 영향을 미친다. 이번 전쟁으로 인한 유가 충격은 역사상 최대 규모로 평가되며, 이는 기존의 우회 경로가 막혀 대체가 어려운 상황 때문이다. 시장에서는 3월 CPI가 2월 대비 상승할 것으로 예상하며, 장기화될 경우…
- 파일: `~/knowledge-agent/100 수신함/118 텔레그램/전문가들의 마켓 인사이트/2026-03-25/260325_123195_유가가_인플레이션에_미치는_영향은_도대체_얼마나_될까_.md`
- **3고 시대 : 고금리, 고유가, 고환율을 어떻게 볼 것인가?** — getfeed, 2026-03-23
- 요약: 3고 시대 : 고금리, 고유가, 고환율을 어떻게 볼 것인가? 핵심적 본문 요약 최근 국내외 불확실성 증폭으로 한국 경제가 '3고' 파고에 휩싸였다. 17년 만에 1,500원 선을 돌파한 고환율과 배럴당 100달러를 넘는 고유가는 수입 원가 상승을 부추겨 인플레이션 압력을 키우고 있다. 이는 소비 위축과 기업 부실 위험을 야기하며 국내 증시 변동성을 확대시키는 요인으로 작용한다. 고환율은 외국인 자본 이탈을 촉발하여 증시 하락을 부추기는 반면, 자동차, 조선 등 일부 외화벌이 업종에는 단기적 호재로 작용한다. 고유가는 유통업계의 원가 부담을 가중시키고 소비 위축을 심화시켜 스태그플레이션 우려를 증폭시킨다. 미국과 이란 간 군사적 긴장은 유…
- 파일: `~/knowledge-agent/100 수신함/118 텔레그램/전문가들의 마켓 인사이트/2026-03-23/260323_122816_3고_시대_고금리_고유가_고환율을_어떻게_볼_것인가.md`
## 최근 매크로·채권 텔레그램
- **하나 매크로팀 - 2026-03-23** — HANA_Macro, 2026-03-23
- 요약: 출처: 하나 매크로팀 | 2026-03-23 | 조회수 130 [Global ETF] ETF 트렌드 & 포트폴리오: 사라진 금리인하 기대 ▶ 하나 Global ETF 박승진(T.3771-7761) ▶ 자료: https://buly.kr/9MSEbW5 ▶ 텔레그램: https://t.me/globaletfi - 여전히 지정학적 리스크가 시장의 주요 변수로 작용 중. 미국과 이란, 이스라엘이 각자의 입장에 따라 강경한 스탠스를 유지 하고 있으며, 이는 시장이 가장 경계하는 인플레이션 우려를 계속해서 자극하는 요인으로 반영. 정치·경제적 측면에서 인플레이션에 민감한 미국, 이를 전략적으로 활용(호르무즈 해협 봉쇄)하는 이란, 그리고 미국의 지…
- 파일: `~/knowledge-agent/100 수신함/116 croned_data/매크로채권/260323_HANA_Macro.md`
- **키움 김승혁(매크로) - 2026-03-22** — kisthemacro, 2026-03-22
- 요약: 출처: 키움 김승혁(매크로) | 2026-03-22 | 조회수 395 [한투증권 안재균] 채권 Note_이란 사태 3주차 점검: 금리인상 우려 본격화(260323) 이제는 이란 사태가 해결되어도 고유가에 의한 물가 충격은 불가피해진 단계로 진입했다는 판단입니다. 영국과 유럽을 필두로 연내 금리인상 우려는 더 이상 가능성이 아닌 현실로 다가오는 모습입니다. 사태 초기 마련했던 기본 시나리오에서 부정 상황을 보다 염두에 두고 향후 흐름을 지켜볼 시기입니다. 우리는 당초 한은의 연내 기준금리 동결 전망에서 4분기 1회 인상으로 기본 시나리오를 변경합니다. 기본 시나리오 하에서 국고 3년 적정 레벨은 3.35%로 추정하며 상하단 밴드는 3.25…
- 파일: `~/knowledge-agent/100 수신함/116 croned_data/매크로채권/260322_kisthemacro.md`
- **키움 김승혁(매크로) - 2026-03-25** — kisthemacro, 2026-03-25
- 요약: 출처: 키움 김승혁(매크로) | 2026-03-25 | 조회수 507 [한투증권 김기명] 크레딧 Note(260326): 나프타발 공급망 충격 가능성과 트럼프 대통령의 출구전략 ■ 리포트 링크: https://vo.la/J6Xn2Jm ■ 나프타 수급난으로 전방산업 연쇄적 생산 차질 우려 - 호르무즈 해협 봉쇄로 나프타 수급에 차질을 빚으면서 이제 국내 산업계 전반에 미치는 부정적 파급영향에 대해 고민해야 될 임계점에 다다르고 있어 보임 - 호르무즈 해협 봉쇄가 풀릴지가 관건인데, 이와 관련해 트럼프 대통령은 미국·이란 전쟁의 출구전략을 모색하고 있음 ■ 트럼프 대통령, ‘선 휴전-후 논의’ 방식의 전쟁 출구전략 추진 - 출구전략은 ‘선…
- 파일: `~/knowledge-agent/100 수신함/116 croned_data/매크로채권/260325_kisthemacro.md`
## 최근 뉴스레터
- **One unreleased AI model just triggered a global financial emergency 😳🚨; AI Agents started opening bank accounts …** — LinasNewsletter, 2026-04-14
- 요약: 이번 주의 주요 이슈는 AI 모델인 Claude Mythos가 금융 시스템에 미치는 파급력입니다. Anthropic에서 최근에 공개한 AI 모델인 Claude Mythos가 72시간 안에 미국 연방 정부, 연준, 캐나다 중앙은행 및 영국 금융 당국이 가장 큰 은행들과의 비밀 회의를 소집하게 만들었습니다. 이들은_rates와 유동성보다는 단일 AI 모델과 관련된 문제에 초점을 맞추고 있음을 확인할 수 있습니다. 이러한 사태가 어떻게 발생했는지, 그리고 그로 인해…
- 파일: `~/knowledge-agent/100 수신함/121 뉴스레터/260414_LinasNewsletter_One-unreleased-AI-model-just-triggered-a.md`
- **🔥 A Strategy For Challenging Times** — CompoundingQuality, 2026-04-15
[truncated: token budget cap applied]
3단계 — 각 analyst별 context 커스텀 설계
| analyst | 우선 소스 | 현재 구현 상태 |
|---|---|---|
| macro | 텔레그램 전문가 해석 글, 매크로·채권 텔레그램, MacroCompass/Concoda류 뉴스레터, ranto28 macro 글, 크레딧 | 실제 연결 완료 |
| fundamental | AppEconomy/CompoundingQuality/QualityCompounding, 크레딧 메일, NEPCON, 기업 텔레그램 | builder selector만 구현. runner 연결은 다음 라운드 |
| technical | 트위터/X, 시장 텔레그램, chart/trader 키워드 뉴스레터 | builder selector만 구현 |
| pm | ranto28, macro 텔레그램, 뉴스레터, 크레딧 | builder selector만 구현 |
4단계 — macro 실제 적용 결과
실행 명령
/bin/bash /Users/ron/.hermes/workspace/scripts/analyst_common_wrapper.sh macro
실행 결과
- 시작: 2026-04-16 08:37:24 KST
- 종료: 2026-04-16 08:43:53 KST
- exit code: 0
- Telegram sector:
market - message_id:
2388 - latest.json mtime: 2026-04-16 08:43:58 KST
TSV 증거:
2026-04-16T08:14:35 OK market no_send no_send_dry_run
2026-04-16T08:15:10 OK 2383 market -1003522748967 5 none
2026-04-16T08:43:53 OK 2388 market -1003522748967 5 none
2026-04-16T08:45:50 OK 2389 market -1003522748967 5 none
2026-04-16T08:46:27 OK 2390 market -1003522748967 5 none
sector_trace 증거:
2026-04-16 08:15:09 [SECTOR_TRACE] send_sector(market) caller=analyst_common_wrapper.sh:macro text='<b>🌍 매크로 리포트</b>\n날짜: <code>2026-04-16</code> · 전송: <code>2026-04-16 08:15:09 KST'
2026-04-16 08:43:52 [SECTOR_TRACE] send_sector(market) caller=analyst_common_wrapper.sh:macro text='<b>🌍 매크로 리포트</b>\n날짜: <code>2026-04-16</code> · 전송: <code>2026-04-16 08:43:52 KST'
2026-04-16 08:45:48 [SECTOR_TRACE] send_sector(market) caller=analyst_common_wrapper.sh:macro text='<b>🌍 매크로 리포트</b>\n날짜: <code>2026-04-16</code> · 전송: <code>2026-04-16 08:45:48 KST'
2026-04-16 08:46:25 [SECTOR_TRACE] send_sector(market) caller=analyst_common_wrapper.sh:macro text='<b>🌍 매크로 리포트</b>\n날짜: <code>2026-04-16</code> · 전송: <code>2026-04-16 08:46:25 KST'
wrapper log 증거:
[2026-04-16 08:14:35] SKIP generation by ANALYST_COMMON_SKIP_RUN=1 analyst=macro
[2026-04-16 08:14:35] send analyst=macro ok=True message_id=None status=dry_run error=no_send_dry_run format=v2_quality no_send=True
[2026-04-16 08:15:09] SKIP generation by ANALYST_COMMON_SKIP_RUN=1 analyst=macro
[2026-04-16 08:15:10] send analyst=macro ok=True message_id=2383 status=success error=None format=v2_quality no_send=False
[2026-04-16 08:37:24] START generation analyst=macro runner=/Users/ron/.hermes/workspace/scripts/analyst_runner.sh
[2026-04-16 08:38:53] START generation analyst=macro runner=/Users/ron/.hermes/workspace/scripts/analyst_runner.sh
[2026-04-16 08:43:52] END generation analyst=macro rc=0 stdout=/Users/ron/.hermes/logs/analyst_macro_common_runner_stdout.log stderr=/Users/ron/.hermes/logs/analyst_macro_common_runner_stderr.log
[2026-04-16 08:43:53] send analyst=macro ok=True message_id=2388 status=success error=None format=v2_quality no_send=False
[2026-04-16 08:45:48] SKIP generation by ANALYST_COMMON_SKIP_RUN=1 analyst=macro
[2026-04-16 08:45:50] send analyst=macro ok=True message_id=2389 status=success error=None format=v2_quality no_send=False
[2026-04-16 08:46:25] END generation analyst=macro rc=0 stdout=/Users/ron/.hermes/logs/analyst_macro_common_runner_stdout.log stderr=/Users/ron/.hermes/logs/analyst_macro_common_runner_stderr.log
[2026-04-16 08:46:27] send analyst=macro ok=True message_id=2390 status=success error=None format=v2_quality no_send=False
context 반영 증거 — latest.json
knowledge_context_used
[
{
"title": "유가가 인플레이션에 미치는 영향 — CPI 6%의 함정",
"source": "전문가들의 마켓 인사이트",
"reason": "유가-CPI 간접 전이 메커니즘 이해"
},
{
"title": "유가 급등과 연준 정책 전망",
"source": "전문가들의 마켓 인사이트",
"reason": "연준 정책 경로 다변수 분석"
},
{
"title": "연준 전망: 견조한 성장, 단기적 인플레이션 상승",
"source": "Statista/전문가들의 마켓 인사이트",
"reason": "Fed 공식 전망치 참조"
},
{
"title": "나프타발 공급망 충격 가능성",
"source": "키움 크레딧 노트",
"reason": "한국 석화 공급망 리스크 평가"
},
{
"title": "BofA 경제 전망 수정",
"source": "승도리",
"reason": "유가 100달러 지속 시나리오 + 글로벌 경기침체 리스크"
}
]
telegram_interpretation_used
[
{
"title": "유가가 인플레이션에 미치는 영향 — CPI 6%의 함정",
"channel": "전문가들의 마켓 인사이트",
"used_for": "CPI/유가 간접 전이 경로 분석(운송·식품·항공료)에 반영"
},
{
"title": "유가 급등과 연준 정책 전망",
"channel": "전문가들의 마켓 인사이트",
"used_for": "연준 비둘기파 전환 근거(대선·노동시장·부채)에 반영"
},
{
"title": "연준 전망: 견조한 성장, 단기적 인플레이션 상승",
"channel": "전문가들의 마켓 인사이트",
"used_for": "Fed GDP 2.4%, PCE 2.7% 전망 수치에 반영"
}
]
summary
Recovery 유지(67%) — WTI -7.28% 급락(기뢰 제거+휴전 기대)으로 에너지 프리미엄 해소 가속. DXY z=-2.11 달러 약세+신용스프레드 양호(IG 0.82%)로 Risk-On 접근. 단 GPR 304+BEI 2.38% 고착은 불확실성 잔존 시사. 유가 간접 전이(운송·식품) 시차로 인플레 즉각 해소 어려움.
causal_analysis
{
"WTI": "호르무즈 기뢰 제거 개시 + 미-이란 휴전 협상 진전 → 공급 재개 기대로 하루 -7.28% 급락. IPSA 우회 가동 중, IEA 비축유 방출 완료 상태.",
"VIX": "18.46(MA20 24.34 대비 급락). 전쟁 프리미엄 해소 진행 중, 단 GPR 304로 지정학 리스크 잔존. 단기 노이즈 아닌 구조적 하향.",
"KRW_USD": "1,472원. 달러 약세(DXY z=-2.11) 주도로 원화 강세 전환. 한국 특수 요인보다 글로벌 달러 약세가 지배적 원인.",
"US10Y": "4.26%. TIPS 1.95%(실질금리) + BEI 2.38%(기대인플레) = 혼합 주도. 유가 하락이 인플레 기대 상승 제한하나, BEI 5Y 2.61%로 단기 인플레 기대 여전히 높음.",
"summary": "호르무즈 기뢰 제거+휴전 협상이 에너지 프리미엄을 빠르게 해소 중. 달러 약세 가속은 연준 인하 기대(연말 69bp 인하 내재) 반영. 단, GPR 304+BEI 고착은 완전한 정상화 전 불확실성 잔존을 시사."
}
growth_inflation
{
"quadrant": "골디락스_접근",
"computed_quadrant": "디플레이션",
"note": "정량 모델은 디플레이션(성장↓물가↓)이나, BEI 10Y 2.38%(z=1.25)+5Y 2.61%는 인플레 기대 상승 시사. 유가 급락(-7.28%)이 물가 하방 압력이나, 간접 전이 시차(운송·식품) 고려 시 즉각 해소 어려움. 판단: 성장 유지+인플레 피크아웃 중간 단계.",
"growth_signal": "maintaining",
"inflation_signal": "peaking_but_sticky",
"growth_score": 0.0,
"inflation_score": -2.0,
"econ_implication": "유가 급락이 인플레 정점 형성 중이나 BEI·서비스 물가 고착으로 완전 해소 지연. 연준 인하 여건은 개선되나 급격한 완화는 어려움."
}
텔레그램 본문 샘플
<b>🌍 매크로 | 4월 16일</b>
<b>🔵 레짐: Recovery</b> 신뢰도 <b>67%</b>
(전일 대비 +7%p 개선, Risk-On 접근 중)
<b>🔍 원인 분석</b>
• WTI <b>$91.87</b>(-7.28%) = 미군 호르무즈 기뢰 제거 개시 + 미-이란 휴전 협상 진전
• VIX <b>18.46</b> = 전쟁 프리미엄 해소 진행(MA20 대비 -24%)
• 원/달러 <b>1,472</b> = 글로벌 달러 약세 주도(DXY z=-2.1)
• US10Y <b>4.26%</b> = 실질금리 1.95% + 인플레 기대 2.38% 혼합
<b>🏛 정책·금리</b>
• Fed: 4.25-4.50% 유지, 선물시장 연말 69bp 인하 반영
• 금리곡선: bull steep — 2s10s <b>+45bp</b>(정상), 실질금리 <b>1.95%</b>
• 정책 분기: Fed 인하 기대 확대 vs BOK 동결 제약(물가+환율)
<b>📊 경기·물가</b>
• 경기 국면: <b>후기 확장</b> — Fed GDP 2.4% 전망, 노동시장 완만 둔화
• 물가: BEI 2.38%(유가 하락에도 고착), 에너지 비중 6.3%이나 운송·식품·항공료 간접 전이 큼
• 성장×물가: <b>인플레 피크아웃 중간단계</b> — 유가 급락이 정점 형성 중이나 서비스 물가 경직
<b>💱 환율</b>
• DXY <b>98.1</b> 약세 가속(z=-2.1) / 원/달러 <b>1,472</b> 강세 전환
• 엔 158.8 약세 유지 — BOJ 정상화 지연, 개입 경계(160) 근접
<b>📍 시나리오</b>
Base(60%): 기뢰 제거 점진, WTI $85-95 → 성장 유지, PCE 2.5-2.7% 고착
Bear(20%): 이란 재도발, WTI $110+ → 스태그플레이션, 나프타 도미노
<b>💧 유동성:</b> neutral — SOFR 3.61(중립), IG스프레드 0.82%(양호, z=-1.65)
<b>⚠️ 리스크</b>
• GPR 304 — 휴전 중이나 지정학 리스크 잔존
• 나프타 수급난 → 한국 석화 전방산업 차질 우려
• 카타르 LNG 12.8Mtpa 소실(복구 3-5년)
<i>볼트 연결 2건 (호르무즈해협 기뢰 매설 시 시장 영향 요약)</i>
analyst_runner.sh diff
--- /Users/ron/.hermes/workspace/scripts/analyst_runner.sh.bak-knowledge-context-20260416T083643 2026-04-15 20:44:13
+++ /Users/ron/.hermes/workspace/scripts/analyst_runner.sh 2026-04-16 08:46:48
@@ -68,13 +68,35 @@
CLAUDE_OUTPUT="$LOG_DIR/analyst_${ANALYST_TYPE}_output.log"
ANALYST_COMMON_PROMPT_FILE=$(mktemp "/tmp/analyst_${ANALYST_TYPE}_prompt.XXXXXX")
cat "$PROMPT_FILE" > "$ANALYST_COMMON_PROMPT_FILE"
+
+# Macro analyst needs interpretation context, not just numeric indicators.
+# Read-only local knowledge sources are compacted by knowledge_context_builder.py.
+if [ "$ANALYST_TYPE" = "macro" ] && [ -x "$SCRIPTS_DIR/knowledge_context_builder.py" ]; then
+ KNOWLEDGE_CONTEXT_FILE=$(mktemp "/tmp/analyst_${ANALYST_TYPE}_knowledge_context.XXXXXX")
+ if "$SCRIPTS_DIR/knowledge_context_builder.py" --analyst macro --hours 96 --max-chars 5000 > "$KNOWLEDGE_CONTEXT_FILE" 2>> "$LOG_FILE"; then
+ {
+ echo ""
+ echo "---"
+ echo "[주입 컨텍스트 — 2026-04-16 knowledge_context_builder]"
+ cat "$KNOWLEDGE_CONTEXT_FILE"
+ echo ""
+ echo "[필수 반영] 위 텔레그램 해석 글에서 2~3개를 골라 오늘 수치(RRP/TGA/CPI/유가/금리/달러 등)의 원인·시사점 설명에 연결하세요. 단순 수치 나열 금지."
+ } >> "$ANALYST_COMMON_PROMPT_FILE"
+ log "Injected macro knowledge context: $(wc -c < "$KNOWLEDGE_CONTEXT_FILE") bytes"
+ else
+ log "WARN: knowledge_context_builder failed; continuing without injected context"
+ fi
+ rm -f "$KNOWLEDGE_CONTEXT_FILE"
+fi
+
cat >> "$ANALYST_COMMON_PROMPT_FILE" <<'EOF'
---
[운영 오버라이드 — 2026-04-15 analyst_common_wrapper]
- Telegram/DM/sector 발송은 절대 실행하지 마세요.
- send_sector, telegram_send.py, curl api.telegram.org 호출 금지.
-- latest.json과 필요한 볼트/메모리 산출물만 생성/갱신하세요.
+- knowledge 볼트(~/knowledge)에는 쓰지 마세요. 읽기만 허용합니다.
+- latest.json과 ~/.hermes/workspace/memory 하위 산출물만 생성/갱신하세요.
- Telegram 발송과 message_id 로깅은 wrapper가 일괄 처리합니다.
EOF
bash -c "
knowledge_context_builder.py 전문
#!/usr/bin/env python3
"""Build compact knowledge-source context for analyst prompts.
Read-only source aggregator for Hermes analyst wrappers. The first concrete
integration target is analyst-macro, where Telegram interpretation notes are
weighted above raw numeric feeds so the macro brief can explain *why* numbers
matter rather than just list them.
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import os
import re
import statistics
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterable
HOME = Path.home()
KNOWLEDGE_AGENT = HOME / "knowledge-agent"
KNOWLEDGE = HOME / "knowledge"
HERMES = HOME / ".hermes"
MACRO_KEYWORDS = [
"CPI", "PCE", "PPI", "FOMC", "Fed", "연준", "금리", "국채", "채권",
"물가", "인플레", "기대인플레", "실질금리", "유동성", "RRP", "TGA",
"지급준비금", "달러", "DXY", "환율", "고용", "실업", "JOLTS", "PMI",
"ISM", "경기", "소비", "GDP", "재정", "QT", "QE", "유가", "WTI",
"호르무즈", "OPEC", "무역", "관세", "침체", "스태그플레이션",
]
MACRO_PIN_PHRASES = ["유가가 인플레이션", "유가 급등과 연준", "연준 전망", "공급 충격이 경기 침체", "3고 시대", "국채는 정말 안전한가"]
INTERPRETATION_KEYWORDS = [
"해석", "의미", "시사점", "전망", "판단", "영향", "압력", "우려", "반영",
"딜레마", "둔화", "고착", "재가속", "완화", "긴축", "선반영", "리스크",
"왜", "because", "therefore", "implies", "means",
]
@dataclass(frozen=True)
class SourceSpec:
key: str
label: str
roots: tuple[Path, ...]
pattern: str
max_files_scan: int = 2500
analyst_weights: dict[str, int] = field(default_factory=dict)
keywords: tuple[str, ...] = ()
recursive: bool = True
@dataclass
class Item:
source_key: str
section: str
path: Path
title: str
source: str
date: str
mtime: float
size: int
score: float
body: str
summary: str
SOURCES: list[SourceSpec] = [
SourceSpec(
key="telegram",
label="텔레그램 해석 글",
roots=(KNOWLEDGE_AGENT / "100 수신함/118 텔레그램",),
pattern="*.md",
analyst_weights={"macro": 180, "technical": 80, "pm": 90, "fundamental": 60},
keywords=tuple(MACRO_KEYWORDS + INTERPRETATION_KEYWORDS),
),
SourceSpec(
key="macro_bond_telegram",
label="매크로·채권 텔레그램 수집",
roots=(KNOWLEDGE_AGENT / "100 수신함/116 croned_data/매크로채권",),
pattern="*.md",
analyst_weights={"macro": 170, "pm": 90, "technical": 40},
keywords=tuple(MACRO_KEYWORDS + INTERPRETATION_KEYWORDS),
),
SourceSpec(
key="newsletter",
label="뉴스레터",
roots=(KNOWLEDGE_AGENT / "100 수신함/121 뉴스레터",),
pattern="2604*_*.md",
recursive=False,
analyst_weights={"macro": 90, "fundamental": 130, "technical": 50, "pm": 90},
keywords=("MacroCompass", "Concoda", "AppEconomy", "Quality", "Compounding", "Linas", *MACRO_KEYWORDS),
),
SourceSpec(
key="blog",
label="블로그/ranto28",
roots=(KNOWLEDGE_AGENT,),
pattern="*ranto28*.md",
analyst_weights={"macro": 100, "pm": 130, "fundamental": 55},
keywords=tuple(MACRO_KEYWORDS + ["미장", "유동성", "공급망", "에너지", "석유", "원자재"]),
),
SourceSpec(
key="twitter",
label="트위터/X",
roots=(KNOWLEDGE_AGENT,),
pattern="*twitter*.md",
analyst_weights={"technical": 120, "macro": 45, "pm": 45},
keywords=tuple(MACRO_KEYWORDS + ["trader", "chart", "risk"]),
),
SourceSpec(
key="nepcon",
label="NEPCON/리서치",
roots=(KNOWLEDGE / "100 수신함/120 지식사랑방/124 nepcon", KNOWLEDGE_AGENT / "200 아토믹/210 원자노트"),
pattern="*nepcon*.md",
analyst_weights={"fundamental": 90, "pm": 35, "macro": 25},
keywords=("반도체", "전자", "AI", "전력", "인프라"),
),
SourceSpec(
key="credit",
label="채권/크레딧 메일",
roots=(KNOWLEDGE_AGENT / "100-inbox/119-크레딧메일", KNOWLEDGE_AGENT / "100 수신함/119 크레딧메일"),
pattern="*.md",
analyst_weights={"fundamental": 110, "macro": 110, "pm": 70},
keywords=tuple(MACRO_KEYWORDS + ["크레딧", "회사채", "CP", "스프레드", "입찰"]),
),
]
SECTION_ORDER = [
("telegram", "최근 텔레그램 해석 글"),
("macro_bond_telegram", "최근 매크로·채권 텔레그램"),
("newsletter", "최근 뉴스레터"),
("blog", "최근 블로그"),
("twitter", "최근 트윗/X"),
("nepcon", "최근 NEPCON/리서치"),
("credit", "최근 채권/크레딧"),
]
ANALYST_SOURCE_LIMITS = {
"macro": {"telegram": 4, "macro_bond_telegram": 3, "newsletter": 2, "blog": 2, "credit": 2, "twitter": 1, "nepcon": 0},
"fundamental": {"newsletter": 3, "credit": 3, "nepcon": 2, "telegram": 2, "blog": 1, "twitter": 1},
"technical": {"twitter": 3, "telegram": 3, "newsletter": 1, "blog": 1, "credit": 1},
"pm": {"blog": 3, "telegram": 3, "newsletter": 2, "credit": 2, "macro_bond_telegram": 2, "twitter": 1},
}
def iter_files(spec: SourceSpec) -> Iterable[Path]:
for root in spec.roots:
if not root.exists():
continue
it = root.rglob(spec.pattern) if spec.recursive else root.glob(spec.pattern)
count = 0
for path in it:
if not path.is_file() or path.suffix.lower() != ".md":
continue
if "/.git/" in str(path):
continue
count += 1
if count > spec.max_files_scan:
break
yield path
def read_text(path: Path, limit: int = 120_000) -> str:
try:
data = path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return ""
return data[:limit]
def split_frontmatter(text: str) -> tuple[dict[str, str], str]:
fm: dict[str, str] = {}
if text.startswith("---"):
end = text.find("\n---", 3)
if end != -1:
raw = text[3:end].strip().splitlines()
body = text[end + 4 :].lstrip()
for line in raw:
if ":" not in line:
continue
k, v = line.split(":", 1)
fm[k.strip()] = v.strip().strip('"\'')
return fm, body
return fm, text
def date_from_filename(path: Path) -> str | None:
m = re.search(r"(?<!\d)(\d{6})(?!\d)", path.name)
if not m:
return None
y, mo, d = int(m.group(1)[:2]), int(m.group(1)[2:4]), int(m.group(1)[4:6])
year = 2000 + y
try:
return dt.date(year, mo, d).isoformat()
except ValueError:
return None
def normalize_title(path: Path, fm: dict[str, str], body: str) -> str:
title = fm.get("title") or fm.get("Title")
if not title:
for line in body.splitlines():
line = line.strip()
if line.startswith("#"):
title = line.lstrip("#").strip()
break
if not title:
title = re.sub(r"^\d{6}[_-]?", "", path.stem)
title = title.replace("_", " ").replace("-", " ").strip()
return re.sub(r"\s+", " ", title)[:140]
def source_name(path: Path, fm: dict[str, str]) -> str:
for key in ("source_channel", "channel", "source", "from", "author"):
if fm.get(key):
return str(fm[key]).strip()[:60]
parts = path.parts
if "118 텔레그램" in parts:
idx = parts.index("118 텔레그램")
if idx + 1 < len(parts):
return parts[idx + 1]
if "121 뉴스레터" in parts:
name = path.name
m = re.match(r"\d{6}_([^_]+)_", name)
return m.group(1) if m else "newsletter"
if "119-크레딧메일" in parts or "119 크레딧메일" in parts:
return "credit-mail"
return path.parent.name[:60]
def clean_body(body: str) -> str:
lines = []
in_code = False
for raw in body.splitlines():
line = raw.strip()
if line.startswith("```"):
in_code = not in_code
continue
if in_code:
continue
if not line:
continue
if line.startswith("![") or line.startswith("<img"):
continue
if line.startswith("tags:") or line.startswith("---"):
continue
line = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", line)
line = re.sub(r"\[\[([^|\]]+)\|([^\]]+)\]\]", r"\2", line)
line = re.sub(r"\[\[([^\]]+)\]\]", r"\1", line)
line = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1", line)
line = re.sub(r"\s+", " ", line).strip()
if line:
lines.append(line)
return "\n".join(lines)
def summarize(text: str, max_chars: int = 320) -> str:
text = clean_body(text)
if not text:
return "본문 요약 불가"
selected = []
for line in text.splitlines():
if line.startswith("#"):
continue
if len(line) < 12 and not re.search(r"[가-힣A-Za-z]", line):
continue
selected.append(line.lstrip("#> "))
if len(" ".join(selected)) >= max_chars:
break
s = " ".join(selected) if selected else text.replace("\n", " ")
return s[:max_chars].rstrip() + ("…" if len(s) > max_chars else "")
def keyword_hits(text: str, keywords: Iterable[str]) -> int:
low = text.lower()
hits = 0
for kw in keywords:
if kw.lower() in low:
hits += 1
return hits
def item_score(path: Path, spec: SourceSpec, analyst: str, body: str, fm: dict[str, str], now: dt.datetime) -> float:
st = path.stat()
age_hours = max(0.0, (now.timestamp() - st.st_mtime) / 3600.0)
recency = max(0.0, 80.0 - min(age_hours, 720.0) / 9.0) # 30일 내 완만 감점
text = f"{path} {fm} {body[:20000]}"
score = spec.analyst_weights.get(analyst, 20) + recency
score += keyword_hits(text, spec.keywords) * 8
if analyst == "macro" and spec.key in {"telegram", "macro_bond_telegram"}:
score += keyword_hits(text, MACRO_KEYWORDS) * 5
score += keyword_hits(text, INTERPRETATION_KEYWORDS) * 10
# prefer named macro channels and daily interpretation digest over random company snippets
path_s = str(path).lower()
if any(x.lower() in path_s for x in ["macro", "매크로", "채권", "harvey", "hana_macro", "kisthemacro"]):
score += 80
if any(phrase in text for phrase in MACRO_PIN_PHRASES):
score += 90
if "_daily.md" in path.name:
score -= 70 # daily digests are fallback; individual interpretation notes are cleaner
else:
score += 25
if "400-reports" in str(path) or "플레이북" in str(path):
score -= 150
return score
def collect_items(analyst: str, hours: int | None, min_per_macro: int = 6) -> list[Item]:
now = dt.datetime.now()
cutoff = now.timestamp() - hours * 3600 if hours else None
raw: list[Item] = []
seen: set[tuple[str, int]] = set()
for spec in SOURCES:
for path in iter_files(spec):
try:
st = path.stat()
except OSError:
continue
if st.st_size <= 0 or st.st_size > 1_500_000:
continue
# First pass respects hours, but macro has fallback below.
if cutoff and st.st_mtime < cutoff:
pass
text = read_text(path)
if not text.strip():
continue
fm, body = split_frontmatter(text)
key = (path.name, st.st_size)
if key in seen:
continue
seen.add(key)
title = normalize_title(path, fm, body)
date_s = fm.get("date") or date_from_filename(path) or dt.datetime.fromtimestamp(st.st_mtime).date().isoformat()
src = source_name(path, fm)
score = item_score(path, spec, analyst, body, fm, now)
summary = summarize(body, 360 if spec.key in {"telegram", "macro_bond_telegram"} else 260)
item = Item(spec.key, dict(SECTION_ORDER).get(spec.key, spec.label), path, title, src, str(date_s), st.st_mtime, st.st_size, score, body, summary)
# Preserve old-but-relevant macro telegram items; otherwise apply cutoff.
if cutoff and st.st_mtime < cutoff and not (analyst == "macro" and spec.key in {"telegram", "macro_bond_telegram"} and score >= 260):
continue
raw.append(item)
if analyst == "macro":
macro_interp = [i for i in raw if i.source_key in {"telegram", "macro_bond_telegram"}]
if len(macro_interp) < min_per_macro:
# Relax time window for macro interpretation if recent ingest is sparse.
return collect_items(analyst, None, min_per_macro=0)
raw.sort(key=lambda i: (i.score, i.mtime), reverse=True)
return raw
def render_context(analyst: str, hours: int | None, max_chars: int, limit_per_source: int | None) -> str:
items = collect_items(analyst, hours)
limits = ANALYST_SOURCE_LIMITS.get(analyst, {})
if limit_per_source is not None:
limits = {k: limit_per_source for k, _ in SECTION_ORDER}
by_key: dict[str, list[Item]] = {k: [] for k, _ in SECTION_ORDER}
for item in items:
lim = limits.get(item.source_key, 1)
if lim <= 0:
continue
if len(by_key.setdefault(item.source_key, [])) < lim:
by_key[item.source_key].append(item)
lines: list[str] = []
lines.append(f"# Knowledge Context for analyst-{analyst}")
lines.append(f"- 생성시각: {dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}")
if hours:
lines.append(f"- 기본 범위: 최근 {hours}시간. 단, macro 텔레그램 해석 글은 최근 수집 공백 시 고득점 과거 글까지 보강.")
lines.append("- 사용법: 수치가 아니라 해석·원인·시사점을 1~3개만 반영하고, 본문에 과잉 인용하지 말 것.")
if analyst == "macro":
lines.append("- macro 우선순위: 텔레그램 전문가/채널 해석 글 > 매크로채권 수집 > 뉴스레터/블로그. RRP/TGA/CPI 같은 수치는 아래 해석 글과 연결해 설명할 것.")
lines.append('- latest.json에는 가능하면 `knowledge_context_used` 배열로 반영한 항목의 title/source/reason을 기록할 것.')
lines.append("")
used = 0
for key, label in SECTION_ORDER:
selected = by_key.get(key, [])
if not selected:
continue
section_lines = [f"## {label}"]
for item in selected:
rel = str(item.path).replace(str(HOME) + "/", "~/")
section_lines.append(f"- **{item.title}** — {item.source}, {item.date}")
section_lines.append(f" - 요약: {item.summary}")
section_lines.append(f" - 파일: `{rel}`")
section_text = "\n".join(section_lines) + "\n\n"
if used + len(section_text) > max_chars:
remain = max_chars - used
if remain > 500:
lines.append(section_text[:remain].rstrip() + "\n")
break
lines.append(section_text.rstrip())
lines.append("")
used += len(section_text)
text = "\n".join(lines).strip() + "\n"
if len(text) > max_chars:
keep = max(1000, max_chars - 80)
text = text[:keep].rstrip()
if "\n" in text:
text = text.rsplit("\n", 1)[0].rstrip()
text += "\n\n[truncated: token budget cap applied]\n"
return text
def list_sources() -> int:
rows = []
for spec in SOURCES:
files = []
for p in iter_files(spec):
try:
files.append(p)
except Exception:
pass
sizes = [p.stat().st_size for p in sorted(files, key=lambda x: x.stat().st_mtime, reverse=True)[:30]]
avg = int(statistics.mean(sizes)) if sizes else 0
roots = "; ".join(str(r) for r in spec.roots)
rows.append({"key": spec.key, "label": spec.label, "roots": roots, "pattern": spec.pattern, "count": len(files), "avg_recent_bytes": avg})
print(json.dumps(rows, ensure_ascii=False, indent=2))
return 0
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="Build compact markdown context from local knowledge sources.")
ap.add_argument("--analyst", choices=["macro", "fundamental", "technical", "pm"], default="macro")
ap.add_argument("--date", default=dt.date.today().isoformat(), help="analysis date label; currently informational")
ap.add_argument("--hours", type=int, default=96, help="recent window in hours; macro interpretation can fallback older")
ap.add_argument("--max-chars", type=int, default=4500)
ap.add_argument("--limit-per-source", type=int, default=None)
ap.add_argument("--output", default=None)
ap.add_argument("--list-sources", action="store_true")
args = ap.parse_args(argv)
if args.list_sources:
return list_sources()
text = render_context(args.analyst, args.hours, args.max_chars, args.limit_per_source)
if args.output:
Path(args.output).write_text(text, encoding="utf-8")
else:
sys.stdout.write(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())
검증
python3 -m py_compile /Users/ron/.hermes/workspace/scripts/knowledge_context_builder.py
bash -n /Users/ron/.hermes/workspace/scripts/analyst_runner.sh
/Users/ron/.hermes/workspace/scripts/knowledge_context_builder.py --analyst macro --hours 96 --max-chars 5000
/bin/bash /Users/ron/.hermes/workspace/scripts/analyst_common_wrapper.sh macro
결과:
syntax=OK
manual_run_rc=0
telegram_message_id=2388
latest_contains_knowledge_context_used=true
latest_contains_telegram_interpretation_used=true
잔존 리스크 / 다음 라운드
- 이번 실제 연결은 macro만 완료. fundamental/technical/pm은 builder selector만 준비됐고 runner 주입은 아직 안 했다.
- 텔레그램 수집물 최신 날짜가 일부 2026-04-04/03-25에 머물러 있어, 96시간 내 글이 부족하면 과거 고득점 글로 fallback한다. 수집 cron 품질은 별도 점검 필요.
- 이번 수동 실행 중 wrapper log에
START generation analyst=macro가 2회 찍혔지만 최종 발송은 1건(2388)만 확인됐다. 향후 수동 실행 때는 lock 파일을 임의 삭제하지 않는 방식 권장. - 뉴스레터 일부는 원문 품질이 낮거나 promotional copy가 섞여 있다. macro에서는 텔레그램을 우선시하므로 영향은 작지만, fundamental 연결 전에는 소스 품질 필터가 필요하다.
자체평가
- 정확성: 4.7/5 — macro에 텔레그램 해석 context가 실제 주입되고 결과 JSON/텔레그램 본문에 반영 확인.
- 완성도: 4.6/5 — macro 실사용 완료, 나머지 analyst는 설계/selector까지만 요구 범위대로 보류.
- 검증: 4.7/5 — 구문검사, builder 샘플, 실제 macro 실행, message_id, latest.json 필드 확인.
- 최소 변경: 4.8/5 — 신규 builder + runner macro 조건부 주입만 수정. 원본 analyst prompt/비즈니스 로직 미수정.
- 종합: 4.7/5
추가 반영 — macro/technical 역할 경계 강화 (2026-04-16 08:56 KST)
해리 추가 룰 반영 완료.
반영 내용
- macro 금지 표현 추가:
z-score,zscore,MA 대비,이동평균,편차,과열/과매도,통계적 이상치,표준편차,변동성/변동성 밴드,RSI,볼린저 밴드,브레이크아웃/breakout,차트 패턴,모멘텀- macro 관장 영역을 프롬프트에 명시:
- CPI/PCE/PPI, GDP/소비/투자, 고용, 금리/금리차, FX, 중앙은행 정책, 유가·원자재 실물 지표, 재정/무역/공급망, 그리고 이들의 원인·정책 함의
- technical 관장 영역을 프롬프트에 명시:
- z-score/MA/편차/이동평균/변동성/모멘텀/차트 패턴/RSI/볼린저/브레이크아웃
analyst_runner.sh의 macro context 주입문에 동일 금지 룰 추가analyst_common_sender.py에 macro 전송 전 최종 sanitizer 추가- 기존 latest.json에 금지 표현이 남아 있어도 Telegram 본문에서는 제거/완화됨
- macro causal section에서
VIX항목 제거. VIX/변동성 해석은 technical 쪽으로 넘김
검증
python3 -m py_compile /Users/ron/.hermes/workspace/scripts/analyst_common_sender.py
bash -n /Users/ron/.hermes/workspace/scripts/analyst_runner.sh
ANALYST_COMMON_NO_SEND=1 ANALYST_COMMON_PRINT_BODY=1 \
PYTHONPATH=/Users/ron/.hermes/workspace/scripts:/Users/ron/.hermes/workspace/scripts/shared:/Users/ron/.hermes/workspace/scripts/pipeline:$PYTHONPATH \
python3 /Users/ron/.hermes/workspace/scripts/analyst_common_sender.py \
macro /Users/ron/.hermes/workspace/memory/analyst-macro/latest.json \
/tmp/macro_boundary_test.tsv /tmp/macro_boundary_test.log \
> /tmp/macro_sender_body_boundary.txt
결과:
syntax=OK
forbidden_grep_hits=[]
DXY_artifact=False
BEI_artifact=False
수정 파일
/Users/ron/.hermes/workspace/scripts/analyst_macro_prompt.md/Users/ron/.hermes/workspace/scripts/analyst_runner.sh/Users/ron/.hermes/workspace/scripts/analyst_common_sender.py
백업
/Users/ron/.hermes/workspace/scripts/analyst_macro_prompt.md.bak-macro-boundary-*/Users/ron/.hermes/workspace/scripts/analyst_runner.sh.bak-macro-boundary-*/Users/ron/.hermes/workspace/scripts/analyst_common_sender.py.bak-macro-boundary-*
자체평가 갱신
- 정확성: 4.8/5 — prompt·runner·최종 발송 sanitizer에 모두 반영
- 완성도: 4.7/5 — macro 실제 출력 경로 기준 금지어 검출 0건 확인
- 검증: 4.7/5 — 구문검사 + no-send body 생성 + 금지어 regex 검사 완료
- 최소 변경: 4.6/5 — macro 경로만 수정, technical/fundamental/pm 경로 영향 없음
- 종합: 4.7/5