@mulsok/traders-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +103 -0
  2. package/bin/cli.js +160 -0
  3. package/bin/postinstall.js +57 -0
  4. package/bin/preuninstall.js +36 -0
  5. package/dist/server/broker/kiwoom/cache.js +86 -0
  6. package/dist/server/broker/kiwoom/cache.js.map +1 -0
  7. package/dist/server/broker/kiwoom/client.js +256 -0
  8. package/dist/server/broker/kiwoom/client.js.map +1 -0
  9. package/dist/server/broker/kiwoom/endpoints/_helpers.js +61 -0
  10. package/dist/server/broker/kiwoom/endpoints/_helpers.js.map +1 -0
  11. package/dist/server/broker/kiwoom/endpoints/account.js +448 -0
  12. package/dist/server/broker/kiwoom/endpoints/account.js.map +1 -0
  13. package/dist/server/broker/kiwoom/endpoints/detail.js +118 -0
  14. package/dist/server/broker/kiwoom/endpoints/detail.js.map +1 -0
  15. package/dist/server/broker/kiwoom/endpoints/investor.js +139 -0
  16. package/dist/server/broker/kiwoom/endpoints/investor.js.map +1 -0
  17. package/dist/server/broker/kiwoom/endpoints/order.js +134 -0
  18. package/dist/server/broker/kiwoom/endpoints/order.js.map +1 -0
  19. package/dist/server/broker/kiwoom/endpoints/quote.js +165 -0
  20. package/dist/server/broker/kiwoom/endpoints/quote.js.map +1 -0
  21. package/dist/server/broker/kiwoom/endpoints/ranking.js +180 -0
  22. package/dist/server/broker/kiwoom/endpoints/ranking.js.map +1 -0
  23. package/dist/server/broker/kiwoom/endpoints/sector.js +135 -0
  24. package/dist/server/broker/kiwoom/endpoints/sector.js.map +1 -0
  25. package/dist/server/broker/kiwoom/endpoints/theme.js +104 -0
  26. package/dist/server/broker/kiwoom/endpoints/theme.js.map +1 -0
  27. package/dist/server/broker/kiwoom/endpoints/universe.js +119 -0
  28. package/dist/server/broker/kiwoom/endpoints/universe.js.map +1 -0
  29. package/dist/server/broker/kiwoom/index.js +59 -0
  30. package/dist/server/broker/kiwoom/index.js.map +1 -0
  31. package/dist/server/broker/kiwoom/order-tracker.js +353 -0
  32. package/dist/server/broker/kiwoom/order-tracker.js.map +1 -0
  33. package/dist/server/broker/kiwoom/price-feed.js +119 -0
  34. package/dist/server/broker/kiwoom/price-feed.js.map +1 -0
  35. package/dist/server/broker/kiwoom/rate-limiter.js +97 -0
  36. package/dist/server/broker/kiwoom/rate-limiter.js.map +1 -0
  37. package/dist/server/broker/kiwoom/types.js +13 -0
  38. package/dist/server/broker/kiwoom/types.js.map +1 -0
  39. package/dist/server/broker/kiwoom/ws/client.js +370 -0
  40. package/dist/server/broker/kiwoom/ws/client.js.map +1 -0
  41. package/dist/server/broker/kiwoom/ws/endpoints/condition.js +146 -0
  42. package/dist/server/broker/kiwoom/ws/endpoints/condition.js.map +1 -0
  43. package/dist/server/broker/kiwoom/ws/realtime-bus.js +42 -0
  44. package/dist/server/broker/kiwoom/ws/realtime-bus.js.map +1 -0
  45. package/dist/server/broker/kiwoom/ws/types.js +19 -0
  46. package/dist/server/broker/kiwoom/ws/types.js.map +1 -0
  47. package/dist/server/broker/news.js +34 -0
  48. package/dist/server/broker/news.js.map +1 -0
  49. package/dist/server/bundle.js +43 -0
  50. package/dist/server/bundle.js.map +1 -0
  51. package/dist/server/calendar/krx-holidays.js +162 -0
  52. package/dist/server/calendar/krx-holidays.js.map +1 -0
  53. package/dist/server/config.js +263 -0
  54. package/dist/server/config.js.map +1 -0
  55. package/dist/server/db/sqlite.js +252 -0
  56. package/dist/server/db/sqlite.js.map +1 -0
  57. package/dist/server/diary/writer.js +266 -0
  58. package/dist/server/diary/writer.js.map +1 -0
  59. package/dist/server/index.js +316 -0
  60. package/dist/server/index.js.map +1 -0
  61. package/dist/server/jobs/universe-sync.js +132 -0
  62. package/dist/server/jobs/universe-sync.js.map +1 -0
  63. package/dist/server/jobs/watchdog.js +87 -0
  64. package/dist/server/jobs/watchdog.js.map +1 -0
  65. package/dist/server/journal/pnl-stats.js +108 -0
  66. package/dist/server/journal/pnl-stats.js.map +1 -0
  67. package/dist/server/journal/telemetry.js +174 -0
  68. package/dist/server/journal/telemetry.js.map +1 -0
  69. package/dist/server/journal/trade-journal.js +239 -0
  70. package/dist/server/journal/trade-journal.js.map +1 -0
  71. package/dist/server/llm/anthropic.js +98 -0
  72. package/dist/server/llm/anthropic.js.map +1 -0
  73. package/dist/server/llm/claude-cli.js +204 -0
  74. package/dist/server/llm/claude-cli.js.map +1 -0
  75. package/dist/server/llm/context-builder.js +229 -0
  76. package/dist/server/llm/context-builder.js.map +1 -0
  77. package/dist/server/llm/gemini.js +86 -0
  78. package/dist/server/llm/gemini.js.map +1 -0
  79. package/dist/server/llm/index.js +36 -0
  80. package/dist/server/llm/index.js.map +1 -0
  81. package/dist/server/llm/openai.js +87 -0
  82. package/dist/server/llm/openai.js.map +1 -0
  83. package/dist/server/personas/executor.js +318 -0
  84. package/dist/server/personas/executor.js.map +1 -0
  85. package/dist/server/personas/loader.js +165 -0
  86. package/dist/server/personas/loader.js.map +1 -0
  87. package/dist/server/personas/persona-agent.js +386 -0
  88. package/dist/server/personas/persona-agent.js.map +1 -0
  89. package/dist/server/personas/persona-state.js +170 -0
  90. package/dist/server/personas/persona-state.js.map +1 -0
  91. package/dist/server/personas/runner.js +162 -0
  92. package/dist/server/personas/runner.js.map +1 -0
  93. package/dist/server/personas/schema.js +123 -0
  94. package/dist/server/personas/schema.js.map +1 -0
  95. package/dist/server/personas/wake-plan.js +313 -0
  96. package/dist/server/personas/wake-plan.js.map +1 -0
  97. package/dist/server/readiness.js +414 -0
  98. package/dist/server/readiness.js.map +1 -0
  99. package/dist/server/routes.js +1216 -0
  100. package/dist/server/routes.js.map +1 -0
  101. package/dist/server/safety.js +153 -0
  102. package/dist/server/safety.js.map +1 -0
  103. package/dist/server/screener/engine.js +856 -0
  104. package/dist/server/screener/engine.js.map +1 -0
  105. package/dist/server/server-sync.js +427 -0
  106. package/dist/server/server-sync.js.map +1 -0
  107. package/dist/server/signing.js +39 -0
  108. package/dist/server/signing.js.map +1 -0
  109. package/dist/server/watchers/condition-watcher.js +519 -0
  110. package/dist/server/watchers/condition-watcher.js.map +1 -0
  111. package/dist/server/watchers/types.js +16 -0
  112. package/dist/server/watchers/types.js.map +1 -0
  113. package/dist/web/assets/index-62SMpbaf.js +79 -0
  114. package/dist/web/assets/index-BPLQR0wt.css +1 -0
  115. package/dist/web/index.html +14 -0
  116. package/package.json +93 -0
  117. package/scripts/com.mulsok.traders.client.plist.template +58 -0
  118. package/scripts/install-daemon.sh +156 -0
  119. package/scripts/uninstall-daemon.sh +62 -0
@@ -0,0 +1,386 @@
1
+ /**
2
+ * SPEC-27 · Stateful Persona Agent — agent cycle 실행 (ADR-011).
3
+ *
4
+ * 모델: claude-cli (subprocess · multi-turn 도구 호출 자체 처리).
5
+ *
6
+ * 흐름:
7
+ * 1. config.llm.provider === "claude-cli" 검증
8
+ * 2. system prompt = Layer 1 + Layer 2 (server bundle 그대로)
9
+ * 3. user prompt = agent 작업 지시 (도구 사용 가이드 + 도메인 컨텍스트)
10
+ * 4. claude-cli subprocess 실행 (--add-dir 페르소나 폴더 + 프로젝트 root)
11
+ * 5. agent 가:
12
+ * - state.json + strategy.md Read
13
+ * - HTTP 도구 (curl localhost:PORT) 로 시세/스크리닝/holdings 등 조회
14
+ * - 의사결정 + state.json Write/Edit + strategy.md Edit
15
+ * - 필요시 placeOrder + registerWatcher 호출
16
+ * 6. journal 에 cycle 시작/종료 이벤트 기록
17
+ * 7. 결과 반환
18
+ *
19
+ * 안전:
20
+ * - SPEC-26 halted 시 즉시 차단
21
+ * - claude-cli 미설치 시 즉시 fail
22
+ * - 도구 호출 격리는 prompt 강제 + path 검증 (state.json 같은 파일은 페르소나 폴더 안만)
23
+ */
24
+ import { spawn } from "node:child_process";
25
+ import os from "node:os";
26
+ import path from "node:path";
27
+ import { loadConfig } from "../config.js";
28
+ import { checkLlmCallSafety } from "../safety.js";
29
+ import { fetchCommonBundle, fetchPersonaBundle, assembleFullPrompt } from "../bundle.js";
30
+ import { SERVER_BASE_URL } from "../config.js";
31
+ import { loadPersona } from "./loader.js";
32
+ import { loadState, personaDir } from "./persona-state.js";
33
+ import { logDecision } from "../journal/trade-journal.js";
34
+ const TIMEOUT_SEC = (() => {
35
+ const env = process.env.CLAUDE_CLI_TIMEOUT_SEC;
36
+ if (env && /^\d+$/.test(env)) {
37
+ const n = Number(env);
38
+ if (n >= 60 && n <= 3600)
39
+ return n;
40
+ }
41
+ return 30 * 60; // 기본 30분
42
+ })();
43
+ export async function executePersonaCycle(slug, triggeredBy = { kind: "manual" }) {
44
+ const startedAt = new Date().toISOString();
45
+ const t0 = Date.now();
46
+ // 1. Safety
47
+ const guard = checkLlmCallSafety();
48
+ if (!guard.ok) {
49
+ return failResult(slug, triggeredBy, startedAt, t0, "safety", `safety:${guard.code} · ${guard.reason}`);
50
+ }
51
+ // 2. config + provider
52
+ const cfg = loadConfig();
53
+ if (cfg.llm.provider !== "claude-cli") {
54
+ return failResult(slug, triggeredBy, startedAt, t0, "config", `agent cycle 은 LLM provider = "claude-cli" 만 지원 (현재 ${cfg.llm.provider}). config 변경 후 재시도.`);
55
+ }
56
+ const cliBinary = cfg.llm.claudeCli?.binaryPath ?? "claude";
57
+ const cliModel = cfg.llm.claudeCli?.model ?? "sonnet";
58
+ // 3. persona
59
+ const persona = await loadPersona(slug);
60
+ if (!persona) {
61
+ return failResult(slug, triggeredBy, startedAt, t0, "persona", `persona '${slug}' 미존재`);
62
+ }
63
+ // 4. Layer 1+2 prompt
64
+ let systemPrompt;
65
+ try {
66
+ const common = await fetchCommonBundle(SERVER_BASE_URL);
67
+ const personaB = await fetchPersonaBundle(SERVER_BASE_URL, slug, cfg.auth?.deviceToken);
68
+ systemPrompt = assembleFullPrompt({
69
+ layer1: common.bundle.body,
70
+ layer2: personaB.bundle.body,
71
+ layer3Header: agentInstructionLayer3(slug, triggeredBy),
72
+ });
73
+ }
74
+ catch (e) {
75
+ return failResult(slug, triggeredBy, startedAt, t0, "bundle", `bundle 조립 실패: ${String(e)}`);
76
+ }
77
+ // 5. claude-cli 실행
78
+ const projectRoot = process.env.MULSOK_PROJECT_ROOT ?? process.cwd();
79
+ const personaWorkDir = personaDir(slug);
80
+ const port = process.env.CLIENT_PORT ?? "8787";
81
+ const userPrompt = buildAgentUserPrompt(slug, triggeredBy, port);
82
+ const args = [
83
+ "--dangerously-skip-permissions",
84
+ "--add-dir", projectRoot,
85
+ "--add-dir", personaWorkDir, // 페르소나 폴더 read/write
86
+ "-p", userPrompt,
87
+ "--output-format", "json",
88
+ "--model", cliModel,
89
+ "--system-prompt", systemPrompt,
90
+ ];
91
+ let stdout = "";
92
+ try {
93
+ const r = await runClaude(cliBinary, args, TIMEOUT_SEC);
94
+ stdout = r.stdout;
95
+ }
96
+ catch (e) {
97
+ return failResult(slug, triggeredBy, startedAt, t0, "agent", `claude-cli 실행 실패: ${String(e)}`);
98
+ }
99
+ const summary = extractAssistantText(stdout);
100
+ // 6. journal cycle 기록
101
+ try {
102
+ logDecision({
103
+ personaSlug: slug,
104
+ llmContext: {
105
+ reasoning: summary,
106
+ model: `claude-cli/${cliModel}`,
107
+ stopReason: "stop",
108
+ truncated: false,
109
+ triggered_by: triggeredBy.watcherId
110
+ ? { intent: triggeredBy.intent ?? triggeredBy.kind, watcherId: triggeredBy.watcherId }
111
+ : null,
112
+ },
113
+ });
114
+ }
115
+ catch (e) {
116
+ console.warn(`[persona-agent] ${slug} journal 실패:`, e);
117
+ }
118
+ const stateAfter = loadState(slug);
119
+ const endedAt = new Date().toISOString();
120
+ return {
121
+ ok: true,
122
+ slug,
123
+ triggeredBy,
124
+ startedAt,
125
+ endedAt,
126
+ elapsedMs: Date.now() - t0,
127
+ summary,
128
+ stateAfter,
129
+ };
130
+ }
131
+ function failResult(slug, triggeredBy, startedAt, t0, stage, error) {
132
+ return {
133
+ ok: false,
134
+ slug,
135
+ triggeredBy,
136
+ startedAt,
137
+ endedAt: new Date().toISOString(),
138
+ elapsedMs: Date.now() - t0,
139
+ stage,
140
+ error,
141
+ };
142
+ }
143
+ /* ================================================================
144
+ Agent prompt — Layer 3 header + user prompt (도구 사용 가이드)
145
+ ================================================================ */
146
+ function agentInstructionLayer3(slug, triggeredBy) {
147
+ const triggerLine = triggeredBy.kind === "watcher"
148
+ ? `이번 사이클은 감시자 발화 트리거 (intent: ${triggeredBy.intent ?? "?"} · watcherId: ${triggeredBy.watcherId ?? "?"}).`
149
+ : triggeredBy.kind === "scheduled"
150
+ ? "이번 사이클은 당신이 자체적으로 예약한 시각 (state.json.nextRunAt) 도달."
151
+ : triggeredBy.kind === "boot"
152
+ ? "이번 사이클은 클라이언트 첫 시작."
153
+ : "이번 사이클은 사용자가 「지금 분석」 수동 트리거.";
154
+ return `## 현재 컨텍스트
155
+
156
+ 당신은 페르소나 \`${slug}\` 입니다. ${triggerLine}
157
+
158
+ ## 당신의 메모리
159
+
160
+ - 자기 전용 폴더: \`~/.mulsok-traders/personas/${slug}/\`
161
+ - \`state.json\` — watchlist · positions · stats · nextRunAt
162
+ - \`strategy.md\` — 자유 형식 전략 노트 (시장 판단 · 학습 등)
163
+
164
+ ## 자율성 원칙
165
+
166
+ - 정기 cron 은 없습니다. 다음 사이클 시각은 당신이 \`state.json.nextRunAt\` (ISO8601) 으로 직접 결정.
167
+ - 또는 시세 조건 watcher 를 등록 (\`POST /api/watchers\`) 해서 트리거 받기.
168
+ - 둘 다 안 잡으면 시스템 watchdog 가 「잠들음」 alert 를 띄웁니다.
169
+ `;
170
+ }
171
+ function buildAgentUserPrompt(slug, triggeredBy, port) {
172
+ return `다음 작업을 순서대로 수행하세요. 각 단계마다 도구를 직접 사용하세요.
173
+
174
+ # 1. 메모리 read
175
+ - \`Read\` 도구로 \`~/.mulsok-traders/personas/${slug}/state.json\` 읽기
176
+ - \`Read\` 도구로 \`~/.mulsok-traders/personas/${slug}/strategy.md\` 읽기
177
+
178
+ # 2. 시장 컨텍스트 조회 (필요한 만큼만 — 토큰 절약)
179
+
180
+ 다음 HTTP 도구를 \`curl\` (Bash) 로 호출:
181
+
182
+ - 보유 종목: \`curl -s http://localhost:${port}/api/account/status\`
183
+ - 활성 watcher: \`curl -s 'http://localhost:${port}/api/watchers?personaSlug=${slug}&activeOnly=true'\`
184
+ - 최근 7일 이벤트: \`curl -s 'http://localhost:${port}/api/journal/events?days=7'\`
185
+ - 종목 일봉 (필요 시): \`curl -s 'http://localhost:${port}/api/quote/daily/<종목코드>?days=30'\`
186
+ - 종목 현재가: \`curl -s http://localhost:${port}/api/quote/current/<종목코드>\`
187
+
188
+ # 3. ★ Cleanup 단계 (필수 · 사이클 시작마다 반드시 수행)
189
+
190
+ 이 단계는 **항상** 수행하세요. 누적 잔존 watcher 와 ETF/우선주가 watchlist 에 들어오는 것을 막습니다.
191
+
192
+ ## 3-A. 활성 watcher 정리
193
+
194
+ 위에서 받은 활성 watcher 리스트를 검사:
195
+
196
+ 1. **같은 종목 + 같은 조건 (intent 가 거의 동일) 인 watcher 가 여러 개** 있으면 가장 최근 1개만 유지하고 나머지 제거:
197
+ \`\`\`
198
+ curl -s -X DELETE 'http://localhost:${port}/api/watchers/<id>'
199
+ \`\`\`
200
+
201
+ 2. **만료 임박 (24시간 내 expiresAt) + 트리거 조건이 현재가에서 멀어진** watcher 는 제거 후 필요하면 재등록.
202
+
203
+ 3. **bulk 정리** (한 종목 모두 지우고 깨끗이 다시 등록):
204
+ \`\`\`
205
+ curl -s -X DELETE 'http://localhost:${port}/api/watchers?personaSlug=${slug}&symbolCode=<종목코드>&keepIds=wt_xxx'
206
+ \`\`\`
207
+ keepIds 는 유지할 watcher 의 id (콤마 구분, 비우면 전부 제거).
208
+
209
+ ## 3-B. watchlist 검토 (state.json)
210
+
211
+ state.json 의 watchlist 를 순회하면서:
212
+
213
+ 1. **종목명 검사 — 다음은 즉시 제거**:
214
+ - prefix: KODEX·TIGER·KBSTAR·ARIRANG·ACE·KOSEF·HANARO·SOL·PLUS·RISE 등 (ETF)
215
+ - 키워드 포함: ETF·ETN·ELS·ELW·인버스·레버리지 (파생)
216
+ - 「리츠」/「REITs」 (리츠)
217
+ - 「스팩」/「SPAC」 (스팩)
218
+ - 종목 코드 끝자리 5~9 (우선주)
219
+ - 종목 코드에 알파벳 (예: 0148J0 — 액티브 ETF)
220
+ 2. **expiresAt 지난 항목 제거**
221
+ 3. **트리거 가격이 현재가와 너무 동떨어진 항목** (예: -30% 이상) 은 재평가 또는 제거
222
+
223
+ # 4. 신규 후보 발굴 (스크리닝 · 자율 판단)
224
+
225
+ **screen 호출 여부는 당신이 판단합니다**. 매번 호출 X.
226
+
227
+ 다음 경우에만 호출:
228
+ - watchlist 가 5개 이하로 줄었을 때
229
+ - 마지막 발굴이 1일 이상 지났을 때
230
+ - 시장 상황이 크게 바뀌어 (지수 급변·테마 주도주 변경 등) 재평가 필요할 때
231
+ - 사용자가 「↻ 지금 분석」 수동 트리거 시
232
+
233
+ 호출 시:
234
+ \`\`\`
235
+ curl -s 'http://localhost:${port}/api/screen/${slug}?topN=20'
236
+ \`\`\`
237
+
238
+ 응답 \`screener.universeSize == 0\` 이면 데이터 미적재:
239
+ \`\`\`
240
+ # 1500 종목 backfill (3~5분)
241
+ curl -s -X POST http://localhost:${port}/api/local/sync \\
242
+ -H 'content-type: application/json' \\
243
+ -d '{"maxCodes": 200, "candleDays": 120}'
244
+
245
+ # 또는 양음양 타겟 배치 (~1분)
246
+ curl -s -X POST http://localhost:${port}/api/local/sync/yinyang \\
247
+ -H 'content-type: application/json' \\
248
+ -d '{"candleDays": 120}'
249
+ \`\`\`
250
+
251
+ 장 마감 후 (15:30 KST) 라면 D-0 포함 데이터로 내일 거래 후보 미리 잡기.
252
+
253
+ # 5. 의사결정 (당신의 전략에 따라)
254
+
255
+ state.json 의 watchlist + 현재 시장 + 보유 + 활성 watcher 를 종합:
256
+
257
+ - watchlist 의 트리거 가격 도달 여부 확인 → 매수 검토
258
+ - 보유 종목의 손절·익절 조건 충족 여부 → 매도 검토
259
+ - 신규 발굴 결과가 흥미로우면 watchlist 추가 + watcher 등록
260
+
261
+ # 6. 행동 (자율 결정)
262
+
263
+ 필요한 행동만 하세요. 모든 단계는 선택사항:
264
+
265
+ - **매수/매도 주문** (실거래·모의 둘 다 안전 게이트 통과 필요):
266
+ \`\`\`
267
+ curl -s -X POST http://localhost:${port}/api/orders \\
268
+ -H 'content-type: application/json' \\
269
+ -d '{"personaSlug":"${slug}","symbolCode":"...","side":"buy","quantity":N,"priceKrw":N}'
270
+ \`\`\`
271
+ - **시세 watcher 등록** (다음 깨우기 조건):
272
+ \`\`\`
273
+ curl -s -X POST http://localhost:${port}/api/watchers \\
274
+ -H 'content-type: application/json' \\
275
+ -d '{"personaSlug":"${slug}","watchers":[{"intent":"...","condition":{...},"wakeAction":"...","maxWaitHours":N}]}'
276
+ \`\`\`
277
+
278
+ # 7. 메모리 갱신 (필수)
279
+
280
+ - \`Write\` 도구로 \`~/.mulsok-traders/personas/${slug}/state.json\` 갱신
281
+ - watchlist 추가/제거
282
+ - positions 동기 (주문 후)
283
+ - stats 갱신 (매도 후 손익)
284
+ - **\`nextRunAt\` 설정** (ISO8601 · 한국 영업일 고려) — 또는 watcher 만 등록했으면 \`null\`
285
+ - \`Edit\` 도구로 \`~/.mulsok-traders/personas/${slug}/strategy.md\` 갱신
286
+ - 「현재 시장 판단」 섹션 갱신
287
+ - 학습 노트 추가 (있으면)
288
+ - \`lastRunAt\` 도 같이 현재 시각으로 갱신
289
+
290
+ ## ⚠ state.json watchlist[] 항목의 정확한 필드명 (UI 가 이 명명으로 읽음)
291
+
292
+ \`\`\`json
293
+ {
294
+ "code": "036010", // 종목 코드 6자리 (필수)
295
+ "name": "쓰리에이로직스", // 종목명 (필수)
296
+ "pattern": "yinYangBull", // 매칭 패턴 또는 자연어 (필수)
297
+ "reason": "D-1 +12% 거래량 5배 ...", // 진입 근거 (필수 · 자연어)
298
+ "addedAt": "2026-05-04T12:00:00Z", // 관심 등록 시각 ISO (필수)
299
+ "triggerPrice": 12500, // 진입 트리거 (선택)
300
+ "targetPrice": 14000, // 목표가 (선택)
301
+ "stopPrice": 11800, // 손절가 (선택)
302
+ "expiresAt": "2026-05-09T00:00:00Z" // 관심 만료 (선택)
303
+ }
304
+ \`\`\`
305
+
306
+ 추가 메타 (score · stars · watcherIds 등) 는 자유 필드로 보존. 다만 위 9개 키는 정확히 사용.
307
+ **symbolCode/symbolName/note/entryTriggerKrw 같은 별칭 사용 금지** (UI 표시 안 됨).
308
+
309
+ # 8. 마지막 응답
310
+
311
+ 수행한 작업을 한 단락으로 요약 (사용자에게 보일 텍스트). 예:
312
+ - "watchlist 의 한화솔루션 트리거 도달 안 함 (현재가 51,000원 vs 트리거 48,650원). 매수 보류. 신규 발굴 결과 동아엘텍 (088130) 패턴3 후보로 watchlist 추가. 다음 사이클은 내일 09:30 KST 예약."
313
+
314
+ 이제 시작하세요.`;
315
+ }
316
+ /* ================================================================
317
+ claude-cli subprocess
318
+ ================================================================ */
319
+ async function runClaude(binary, args, timeoutSec) {
320
+ const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("CLAUDE")));
321
+ return new Promise((resolve, reject) => {
322
+ const child = spawn(binary, args, {
323
+ env,
324
+ stdio: ["ignore", "pipe", "pipe"],
325
+ });
326
+ let stdout = "";
327
+ let stderr = "";
328
+ let killed = false;
329
+ const timer = setTimeout(() => {
330
+ killed = true;
331
+ try {
332
+ child.kill("SIGTERM");
333
+ }
334
+ catch { /* ignore */ }
335
+ }, timeoutSec * 1000);
336
+ child.stdout.on("data", (d) => { stdout += d.toString("utf-8"); });
337
+ child.stderr.on("data", (d) => { stderr += d.toString("utf-8"); });
338
+ child.on("error", (e) => { clearTimeout(timer); reject(e); });
339
+ child.on("close", (code) => {
340
+ clearTimeout(timer);
341
+ if (killed)
342
+ return reject(new Error(`claude-cli 타임아웃 (${timeoutSec}s)`));
343
+ if (code !== 0 && code !== null) {
344
+ return reject(new Error(`claude-cli exit ${code} · stderr: ${stderr.slice(0, 500)}`));
345
+ }
346
+ resolve({ stdout, stderr });
347
+ });
348
+ });
349
+ }
350
+ function extractAssistantText(stdout) {
351
+ // claude-cli json output: 단일 객체 또는 NDJSON. 마지막 assistant text 추출.
352
+ const trimmed = stdout.trim();
353
+ if (!trimmed)
354
+ return "";
355
+ // 단일 객체
356
+ try {
357
+ const obj = JSON.parse(trimmed);
358
+ if (obj.result)
359
+ return String(obj.result);
360
+ if (obj.text)
361
+ return String(obj.text);
362
+ if (Array.isArray(obj.messages)) {
363
+ const last = [...obj.messages].reverse().find((m) => m.role === "assistant");
364
+ if (last && typeof last.content === "string")
365
+ return last.content;
366
+ }
367
+ }
368
+ catch { /* fallthrough */ }
369
+ // NDJSON — 마지막 assistant 라인
370
+ const lines = trimmed.split("\n").filter(Boolean);
371
+ for (let i = lines.length - 1; i >= 0; i--) {
372
+ try {
373
+ const o = JSON.parse(lines[i]);
374
+ if (o.role === "assistant" && typeof o.content === "string")
375
+ return o.content;
376
+ if (o.type === "text" && typeof o.text === "string")
377
+ return o.text;
378
+ }
379
+ catch { /* skip */ }
380
+ }
381
+ return trimmed.slice(0, 2000);
382
+ }
383
+ // 부수: os 의 path 가져오기 (linter 만족용)
384
+ void os;
385
+ void path;
386
+ //# sourceMappingURL=persona-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persona-agent.js","sourceRoot":"","sources":["../../../src-server/personas/persona-agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * SPEC-27 · Stateful Persona Agent — state.json read/write 모듈.
3
+ *
4
+ * 페르소나별 ~/.mulsok-traders/personas/<slug>/state.json 을 zod 검증으로 read/write.
5
+ * AI agent 가 직접 도구로 read/write 하지만, 시스템 (watchdog 등) 도 동일 모듈로 read 한다.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import os from "node:os";
10
+ import { z } from "zod";
11
+ const ROOT = path.join(os.homedir(), ".mulsok-traders", "personas");
12
+ // AI 가 자유 필드명 사용해도 정합되도록 preprocess 로 normalize.
13
+ // 별칭 매핑: symbolCode→code, symbolName→name, note→reason,
14
+ // entryTriggerKrw→triggerPrice, stopLossKrw→stopPrice
15
+ export const WatchlistItemSchema = z.preprocess((v) => {
16
+ if (!v || typeof v !== "object")
17
+ return v;
18
+ const o = v;
19
+ return {
20
+ code: o.code ?? o.symbolCode ?? o.symbol_code ?? o.symbolcode,
21
+ name: o.name ?? o.symbolName ?? o.symbol_name ?? o.symbolname,
22
+ pattern: o.pattern,
23
+ triggerPrice: o.triggerPrice ?? o.entryTriggerKrw ?? o.entryTrigger ?? o.entryPrice,
24
+ targetPrice: o.targetPrice ?? o.targetPriceKrw ?? o.takeProfitKrw,
25
+ stopPrice: o.stopPrice ?? o.stopLossKrw ?? o.stopLoss ?? o.stopPriceKrw,
26
+ reason: o.reason ?? o.note ?? o.notes ?? o.rationale ?? o.summary ?? "",
27
+ addedAt: o.addedAt ?? o.createdAt ?? o.added_at ?? new Date().toISOString(),
28
+ expiresAt: o.expiresAt ?? o.expires_at,
29
+ };
30
+ }, z.object({
31
+ code: z.string().min(1),
32
+ name: z.string().min(1),
33
+ pattern: z.string().min(1),
34
+ triggerPrice: z.number().nonnegative().optional(),
35
+ targetPrice: z.number().nonnegative().optional(),
36
+ stopPrice: z.number().nonnegative().optional(),
37
+ reason: z.string(), // empty 허용 (AI 가 빠뜨릴 수 있음)
38
+ addedAt: z.string().min(1),
39
+ expiresAt: z.string().optional(),
40
+ }).passthrough()); // 추가 필드 (score, stars, watcherIds 등) 보존
41
+ export const PositionSchema = z.object({
42
+ code: z.string().min(1),
43
+ name: z.string().min(1),
44
+ quantity: z.number().int().nonnegative(),
45
+ avgPriceKrw: z.number().nonnegative(),
46
+ boughtAt: z.string().min(1),
47
+ rationale: z.string().default(""),
48
+ });
49
+ // passthrough() — AI 가 자유 필드 (activeWatchers · notes 등) 추가해도 보존.
50
+ // 우리가 강제하는 핵심 필드는 명시 + 검증, 그 외는 통과.
51
+ export const PersonaStateSchema = z.object({
52
+ version: z.literal(1).default(1),
53
+ slug: z.string().min(1),
54
+ nextRunAt: z.string().nullable().default(null),
55
+ lastRunAt: z.string().default(() => new Date().toISOString()),
56
+ watchlist: z.array(WatchlistItemSchema).default([]),
57
+ positions: z.array(PositionSchema).default([]),
58
+ stats: z.object({
59
+ totalTrades: z.number().int().nonnegative().default(0),
60
+ winCount: z.number().int().nonnegative().default(0),
61
+ lossCount: z.number().int().nonnegative().default(0),
62
+ realizedPnlKrw: z.number().default(0),
63
+ }).default({ totalTrades: 0, winCount: 0, lossCount: 0, realizedPnlKrw: 0 }),
64
+ memo: z.string().optional(),
65
+ }).passthrough();
66
+ export function personaDir(slug) {
67
+ return path.join(ROOT, slug);
68
+ }
69
+ export function statePath(slug) {
70
+ return path.join(personaDir(slug), "state.json");
71
+ }
72
+ export function strategyPath(slug) {
73
+ return path.join(personaDir(slug), "strategy.md");
74
+ }
75
+ export function logPath(slug) {
76
+ return path.join(personaDir(slug), "log.jsonl");
77
+ }
78
+ function ensureDir(slug) {
79
+ const dir = personaDir(slug);
80
+ if (!fs.existsSync(dir))
81
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
82
+ }
83
+ /** state.json 이 없으면 default 생성 후 반환. 잘못된 형식이면 default 로 초기화 + warn. */
84
+ export function loadState(slug) {
85
+ ensureDir(slug);
86
+ const p = statePath(slug);
87
+ if (!fs.existsSync(p)) {
88
+ const init = PersonaStateSchema.parse({ slug });
89
+ fs.writeFileSync(p, JSON.stringify(init, null, 2), { mode: 0o600 });
90
+ return init;
91
+ }
92
+ try {
93
+ const raw = fs.readFileSync(p, "utf-8");
94
+ const parsed = PersonaStateSchema.safeParse(JSON.parse(raw));
95
+ if (!parsed.success) {
96
+ console.warn(`[persona-state] ${slug} 잘못된 형식 · default 로 초기화:`, parsed.error.issues);
97
+ const init = PersonaStateSchema.parse({ slug });
98
+ fs.writeFileSync(p, JSON.stringify(init, null, 2), { mode: 0o600 });
99
+ return init;
100
+ }
101
+ return parsed.data;
102
+ }
103
+ catch (e) {
104
+ console.warn(`[persona-state] ${slug} read 실패 · default:`, e);
105
+ return PersonaStateSchema.parse({ slug });
106
+ }
107
+ }
108
+ export function saveState(slug, state) {
109
+ ensureDir(slug);
110
+ const validated = PersonaStateSchema.parse({ ...state, slug });
111
+ fs.writeFileSync(statePath(slug), JSON.stringify(validated, null, 2), { mode: 0o600 });
112
+ return validated;
113
+ }
114
+ export function loadStrategy(slug) {
115
+ ensureDir(slug);
116
+ const p = strategyPath(slug);
117
+ if (!fs.existsSync(p)) {
118
+ const init = defaultStrategyMd(slug);
119
+ fs.writeFileSync(p, init, { mode: 0o600 });
120
+ return init;
121
+ }
122
+ return fs.readFileSync(p, "utf-8");
123
+ }
124
+ export function saveStrategy(slug, content) {
125
+ ensureDir(slug);
126
+ fs.writeFileSync(strategyPath(slug), content, { mode: 0o600 });
127
+ }
128
+ export function appendLog(slug, event) {
129
+ ensureDir(slug);
130
+ const line = JSON.stringify({ ...event, at: new Date().toISOString() }) + "\n";
131
+ fs.appendFileSync(logPath(slug), line, { mode: 0o600 });
132
+ }
133
+ function defaultStrategyMd(slug) {
134
+ return `# ${slug} 전략 노트
135
+
136
+ ## 핵심 원칙
137
+ (아직 작성 전. AI 가 매 사이클마다 자기 전략을 정리합니다.)
138
+
139
+ ## 현재 시장 판단
140
+ (첫 실행 시 작성)
141
+
142
+ ## 관심종목 현황
143
+ (state.json 의 watchlist 와 동기 · 자연어 plan)
144
+
145
+ ## 패턴별 성공률 (누적)
146
+ (첫 거래 후 자동 갱신)
147
+
148
+ ## 학습 노트
149
+ (첫 실행 — 아직 없음)
150
+
151
+ ## 주의 종목 이력
152
+ (없음)
153
+
154
+ ---
155
+ *마지막 업데이트: ${new Date().toISOString()}*
156
+ `;
157
+ }
158
+ /* ================================================================
159
+ Path 안전 검증 — agent 도구가 페르소나 폴더 밖 접근 시도 차단
160
+ ================================================================ */
161
+ /**
162
+ * 주어진 path 가 ~/.mulsok-traders/personas/<slug>/ 안에 있는지 검증.
163
+ * 도구 호출 (Read/Write/Edit) 의 path 인자에 사용.
164
+ */
165
+ export function isPathInPersonaDir(slug, target) {
166
+ const base = personaDir(slug);
167
+ const resolved = path.resolve(target);
168
+ return resolved.startsWith(base + path.sep) || resolved === base;
169
+ }
170
+ //# sourceMappingURL=persona-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persona-state.js","sourceRoot":"","sources":["../../../src-server/personas/persona-state.ts"],"names":[],"mappings":"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"}