@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.
- package/README.md +103 -0
- package/bin/cli.js +160 -0
- package/bin/postinstall.js +57 -0
- package/bin/preuninstall.js +36 -0
- package/dist/server/broker/kiwoom/cache.js +86 -0
- package/dist/server/broker/kiwoom/cache.js.map +1 -0
- package/dist/server/broker/kiwoom/client.js +256 -0
- package/dist/server/broker/kiwoom/client.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/_helpers.js +61 -0
- package/dist/server/broker/kiwoom/endpoints/_helpers.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/account.js +448 -0
- package/dist/server/broker/kiwoom/endpoints/account.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/detail.js +118 -0
- package/dist/server/broker/kiwoom/endpoints/detail.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/investor.js +139 -0
- package/dist/server/broker/kiwoom/endpoints/investor.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/order.js +134 -0
- package/dist/server/broker/kiwoom/endpoints/order.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/quote.js +165 -0
- package/dist/server/broker/kiwoom/endpoints/quote.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/ranking.js +180 -0
- package/dist/server/broker/kiwoom/endpoints/ranking.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/sector.js +135 -0
- package/dist/server/broker/kiwoom/endpoints/sector.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/theme.js +104 -0
- package/dist/server/broker/kiwoom/endpoints/theme.js.map +1 -0
- package/dist/server/broker/kiwoom/endpoints/universe.js +119 -0
- package/dist/server/broker/kiwoom/endpoints/universe.js.map +1 -0
- package/dist/server/broker/kiwoom/index.js +59 -0
- package/dist/server/broker/kiwoom/index.js.map +1 -0
- package/dist/server/broker/kiwoom/order-tracker.js +353 -0
- package/dist/server/broker/kiwoom/order-tracker.js.map +1 -0
- package/dist/server/broker/kiwoom/price-feed.js +119 -0
- package/dist/server/broker/kiwoom/price-feed.js.map +1 -0
- package/dist/server/broker/kiwoom/rate-limiter.js +97 -0
- package/dist/server/broker/kiwoom/rate-limiter.js.map +1 -0
- package/dist/server/broker/kiwoom/types.js +13 -0
- package/dist/server/broker/kiwoom/types.js.map +1 -0
- package/dist/server/broker/kiwoom/ws/client.js +370 -0
- package/dist/server/broker/kiwoom/ws/client.js.map +1 -0
- package/dist/server/broker/kiwoom/ws/endpoints/condition.js +146 -0
- package/dist/server/broker/kiwoom/ws/endpoints/condition.js.map +1 -0
- package/dist/server/broker/kiwoom/ws/realtime-bus.js +42 -0
- package/dist/server/broker/kiwoom/ws/realtime-bus.js.map +1 -0
- package/dist/server/broker/kiwoom/ws/types.js +19 -0
- package/dist/server/broker/kiwoom/ws/types.js.map +1 -0
- package/dist/server/broker/news.js +34 -0
- package/dist/server/broker/news.js.map +1 -0
- package/dist/server/bundle.js +43 -0
- package/dist/server/bundle.js.map +1 -0
- package/dist/server/calendar/krx-holidays.js +162 -0
- package/dist/server/calendar/krx-holidays.js.map +1 -0
- package/dist/server/config.js +263 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/db/sqlite.js +252 -0
- package/dist/server/db/sqlite.js.map +1 -0
- package/dist/server/diary/writer.js +266 -0
- package/dist/server/diary/writer.js.map +1 -0
- package/dist/server/index.js +316 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/jobs/universe-sync.js +132 -0
- package/dist/server/jobs/universe-sync.js.map +1 -0
- package/dist/server/jobs/watchdog.js +87 -0
- package/dist/server/jobs/watchdog.js.map +1 -0
- package/dist/server/journal/pnl-stats.js +108 -0
- package/dist/server/journal/pnl-stats.js.map +1 -0
- package/dist/server/journal/telemetry.js +174 -0
- package/dist/server/journal/telemetry.js.map +1 -0
- package/dist/server/journal/trade-journal.js +239 -0
- package/dist/server/journal/trade-journal.js.map +1 -0
- package/dist/server/llm/anthropic.js +98 -0
- package/dist/server/llm/anthropic.js.map +1 -0
- package/dist/server/llm/claude-cli.js +204 -0
- package/dist/server/llm/claude-cli.js.map +1 -0
- package/dist/server/llm/context-builder.js +229 -0
- package/dist/server/llm/context-builder.js.map +1 -0
- package/dist/server/llm/gemini.js +86 -0
- package/dist/server/llm/gemini.js.map +1 -0
- package/dist/server/llm/index.js +36 -0
- package/dist/server/llm/index.js.map +1 -0
- package/dist/server/llm/openai.js +87 -0
- package/dist/server/llm/openai.js.map +1 -0
- package/dist/server/personas/executor.js +318 -0
- package/dist/server/personas/executor.js.map +1 -0
- package/dist/server/personas/loader.js +165 -0
- package/dist/server/personas/loader.js.map +1 -0
- package/dist/server/personas/persona-agent.js +386 -0
- package/dist/server/personas/persona-agent.js.map +1 -0
- package/dist/server/personas/persona-state.js +170 -0
- package/dist/server/personas/persona-state.js.map +1 -0
- package/dist/server/personas/runner.js +162 -0
- package/dist/server/personas/runner.js.map +1 -0
- package/dist/server/personas/schema.js +123 -0
- package/dist/server/personas/schema.js.map +1 -0
- package/dist/server/personas/wake-plan.js +313 -0
- package/dist/server/personas/wake-plan.js.map +1 -0
- package/dist/server/readiness.js +414 -0
- package/dist/server/readiness.js.map +1 -0
- package/dist/server/routes.js +1216 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/server/safety.js +153 -0
- package/dist/server/safety.js.map +1 -0
- package/dist/server/screener/engine.js +856 -0
- package/dist/server/screener/engine.js.map +1 -0
- package/dist/server/server-sync.js +427 -0
- package/dist/server/server-sync.js.map +1 -0
- package/dist/server/signing.js +39 -0
- package/dist/server/signing.js.map +1 -0
- package/dist/server/watchers/condition-watcher.js +519 -0
- package/dist/server/watchers/condition-watcher.js.map +1 -0
- package/dist/server/watchers/types.js +16 -0
- package/dist/server/watchers/types.js.map +1 -0
- package/dist/web/assets/index-62SMpbaf.js +79 -0
- package/dist/web/assets/index-BPLQR0wt.css +1 -0
- package/dist/web/index.html +14 -0
- package/package.json +93 -0
- package/scripts/com.mulsok.traders.client.plist.template +58 -0
- package/scripts/install-daemon.sh +156 -0
- package/scripts/uninstall-daemon.sh +62 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 로컬 SQLite DB (apps/client)
|
|
3
|
+
*
|
|
4
|
+
* 위치: ~/.mulsok-traders/market.db
|
|
5
|
+
*
|
|
6
|
+
* 테이블:
|
|
7
|
+
* - universe 전종목 유니버스 (일 1회 배치)
|
|
8
|
+
* - daily_candles 종목별 일봉 (120일)
|
|
9
|
+
* - sync_meta 배치 메타 (last_run, count, status)
|
|
10
|
+
*
|
|
11
|
+
* 설계 원칙:
|
|
12
|
+
* - ADR-008 분산 자율 · 로컬 전용 (서버에 싱크 안 함)
|
|
13
|
+
* - 쓰기는 배치 job 에서만 · 런타임은 읽기만
|
|
14
|
+
* - WAL 모드 + PRAGMA synchronous=NORMAL · 성능 우선
|
|
15
|
+
*/
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import Database from "better-sqlite3";
|
|
20
|
+
const DB_DIR = path.join(os.homedir(), ".mulsok-traders");
|
|
21
|
+
const DB_PATH = path.join(DB_DIR, "market.db");
|
|
22
|
+
let instance;
|
|
23
|
+
export function getDb() {
|
|
24
|
+
if (instance)
|
|
25
|
+
return instance;
|
|
26
|
+
if (!fs.existsSync(DB_DIR)) {
|
|
27
|
+
fs.mkdirSync(DB_DIR, { recursive: true, mode: 0o700 });
|
|
28
|
+
}
|
|
29
|
+
instance = new Database(DB_PATH);
|
|
30
|
+
instance.pragma("journal_mode = WAL");
|
|
31
|
+
instance.pragma("synchronous = NORMAL");
|
|
32
|
+
applyMigrations(instance);
|
|
33
|
+
return instance;
|
|
34
|
+
}
|
|
35
|
+
export function closeDb() {
|
|
36
|
+
if (instance) {
|
|
37
|
+
instance.close();
|
|
38
|
+
instance = undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function applyMigrations(db) {
|
|
42
|
+
db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS universe (
|
|
44
|
+
code TEXT PRIMARY KEY,
|
|
45
|
+
name TEXT NOT NULL,
|
|
46
|
+
market TEXT NOT NULL, -- 'kospi' / 'kosdaq' / ...
|
|
47
|
+
market_code TEXT,
|
|
48
|
+
up_name TEXT,
|
|
49
|
+
up_size_name TEXT,
|
|
50
|
+
list_count INTEGER DEFAULT 0,
|
|
51
|
+
last_price_krw INTEGER DEFAULT 0,
|
|
52
|
+
reg_day TEXT,
|
|
53
|
+
state TEXT,
|
|
54
|
+
audit_info TEXT,
|
|
55
|
+
order_warning_code TEXT,
|
|
56
|
+
company_class_name TEXT,
|
|
57
|
+
nxt_enable INTEGER DEFAULT 0,
|
|
58
|
+
updated_at TEXT NOT NULL
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_universe_market ON universe(market);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_universe_state ON universe(state);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS daily_candles (
|
|
65
|
+
code TEXT NOT NULL,
|
|
66
|
+
date TEXT NOT NULL, -- yyyyMMdd
|
|
67
|
+
open_krw INTEGER NOT NULL,
|
|
68
|
+
high_krw INTEGER NOT NULL,
|
|
69
|
+
low_krw INTEGER NOT NULL,
|
|
70
|
+
close_krw INTEGER NOT NULL,
|
|
71
|
+
volume INTEGER NOT NULL,
|
|
72
|
+
PRIMARY KEY (code, date)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_candles_date ON daily_candles(date);
|
|
76
|
+
|
|
77
|
+
CREATE TABLE IF NOT EXISTS sync_meta (
|
|
78
|
+
kind TEXT PRIMARY KEY, -- 'universe' / 'candles:<date>'
|
|
79
|
+
last_run_at TEXT NOT NULL,
|
|
80
|
+
item_count INTEGER DEFAULT 0,
|
|
81
|
+
status TEXT DEFAULT 'ok',
|
|
82
|
+
message TEXT
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
-- Watcher persistence (SPEC-11) — restart 시 active watcher 복원
|
|
86
|
+
CREATE TABLE IF NOT EXISTS watchers (
|
|
87
|
+
id TEXT PRIMARY KEY, -- wt_<8hex>
|
|
88
|
+
persona_slug TEXT NOT NULL,
|
|
89
|
+
spec_json TEXT NOT NULL, -- ConditionSpec JSON
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
expires_at TEXT NOT NULL,
|
|
92
|
+
trigger_count INTEGER DEFAULT 0,
|
|
93
|
+
status TEXT DEFAULT 'active' -- active / triggered / expired / revoked
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_watchers_persona ON watchers(persona_slug);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_watchers_status ON watchers(status);
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
export function upsertWatcher(row) {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
db.prepare(`
|
|
103
|
+
INSERT INTO watchers (id, persona_slug, spec_json, created_at, expires_at, trigger_count, status)
|
|
104
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
105
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
106
|
+
spec_json = excluded.spec_json,
|
|
107
|
+
expires_at = excluded.expires_at,
|
|
108
|
+
trigger_count = excluded.trigger_count,
|
|
109
|
+
status = excluded.status
|
|
110
|
+
`).run(row.id, row.persona_slug, row.spec_json, row.created_at, row.expires_at, row.trigger_count, row.status);
|
|
111
|
+
}
|
|
112
|
+
export function deleteWatcher(id) {
|
|
113
|
+
getDb().prepare("DELETE FROM watchers WHERE id = ?").run(id);
|
|
114
|
+
}
|
|
115
|
+
export function listActiveWatchers() {
|
|
116
|
+
return getDb()
|
|
117
|
+
.prepare("SELECT * FROM watchers WHERE status = 'active' ORDER BY created_at")
|
|
118
|
+
.all();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* watchers 테이블 vacuum — N일 이상 된 non-active row 자동 삭제.
|
|
122
|
+
*
|
|
123
|
+
* 정책:
|
|
124
|
+
* - status = 'active' row 는 절대 건드리지 않음 (운영 중)
|
|
125
|
+
* - status = 'triggered' / 'expired' / 'revoked' 중 created_at < (now - days) → DELETE
|
|
126
|
+
* - 1년+ 운영 시 수만 row 누적으로 restoreFromDb 성능 ↓ 방지 (P1-4)
|
|
127
|
+
*
|
|
128
|
+
* 호출:
|
|
129
|
+
* - boot 시 1회 (index.ts startup)
|
|
130
|
+
* - manual: POST /api/local/vacuum (향후)
|
|
131
|
+
*
|
|
132
|
+
* @returns 삭제된 row 수
|
|
133
|
+
*/
|
|
134
|
+
export function vacuumWatchersOlderThan(days) {
|
|
135
|
+
const cutoff = new Date(Date.now() - days * 86400_000).toISOString();
|
|
136
|
+
const r = getDb()
|
|
137
|
+
.prepare(`DELETE FROM watchers WHERE status != 'active' AND created_at < ?`)
|
|
138
|
+
.run(cutoff);
|
|
139
|
+
return r.changes;
|
|
140
|
+
}
|
|
141
|
+
/** watchers 테이블 통계 (모니터링용) */
|
|
142
|
+
export function watchersTableStats() {
|
|
143
|
+
const rows = getDb().prepare(`SELECT status, COUNT(*) as cnt FROM watchers GROUP BY status`).all();
|
|
144
|
+
const result = { total: 0, active: 0, triggered: 0, expired: 0, revoked: 0 };
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
result.total += row.cnt;
|
|
147
|
+
if (row.status === "active")
|
|
148
|
+
result.active = row.cnt;
|
|
149
|
+
else if (row.status === "triggered")
|
|
150
|
+
result.triggered = row.cnt;
|
|
151
|
+
else if (row.status === "expired")
|
|
152
|
+
result.expired = row.cnt;
|
|
153
|
+
else if (row.status === "revoked")
|
|
154
|
+
result.revoked = row.cnt;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
export function upsertUniverseBulk(rows) {
|
|
159
|
+
if (rows.length === 0)
|
|
160
|
+
return;
|
|
161
|
+
const db = getDb();
|
|
162
|
+
const stmt = db.prepare(`
|
|
163
|
+
INSERT INTO universe (code, name, market, market_code, up_name, up_size_name,
|
|
164
|
+
list_count, last_price_krw, reg_day, state, audit_info, order_warning_code,
|
|
165
|
+
company_class_name, nxt_enable, updated_at)
|
|
166
|
+
VALUES (@code, @name, @market, @market_code, @up_name, @up_size_name,
|
|
167
|
+
@list_count, @last_price_krw, @reg_day, @state, @audit_info, @order_warning_code,
|
|
168
|
+
@company_class_name, @nxt_enable, @updated_at)
|
|
169
|
+
ON CONFLICT(code) DO UPDATE SET
|
|
170
|
+
name = excluded.name,
|
|
171
|
+
market = excluded.market,
|
|
172
|
+
market_code = excluded.market_code,
|
|
173
|
+
up_name = excluded.up_name,
|
|
174
|
+
up_size_name = excluded.up_size_name,
|
|
175
|
+
list_count = excluded.list_count,
|
|
176
|
+
last_price_krw = excluded.last_price_krw,
|
|
177
|
+
reg_day = excluded.reg_day,
|
|
178
|
+
state = excluded.state,
|
|
179
|
+
audit_info = excluded.audit_info,
|
|
180
|
+
order_warning_code = excluded.order_warning_code,
|
|
181
|
+
company_class_name = excluded.company_class_name,
|
|
182
|
+
nxt_enable = excluded.nxt_enable,
|
|
183
|
+
updated_at = excluded.updated_at;
|
|
184
|
+
`);
|
|
185
|
+
const tx = db.transaction((list) => {
|
|
186
|
+
for (const r of list)
|
|
187
|
+
stmt.run(r);
|
|
188
|
+
});
|
|
189
|
+
tx(rows);
|
|
190
|
+
}
|
|
191
|
+
export function countUniverse(market) {
|
|
192
|
+
const db = getDb();
|
|
193
|
+
if (market) {
|
|
194
|
+
const row = db.prepare("SELECT COUNT(*) AS c FROM universe WHERE market = ?").get(market);
|
|
195
|
+
return row.c;
|
|
196
|
+
}
|
|
197
|
+
const row = db.prepare("SELECT COUNT(*) AS c FROM universe").get();
|
|
198
|
+
return row.c;
|
|
199
|
+
}
|
|
200
|
+
export function listUniverse(opts = {}) {
|
|
201
|
+
const db = getDb();
|
|
202
|
+
const where = opts.market ? "WHERE market = ?" : "";
|
|
203
|
+
const limit = opts.limit ? ` LIMIT ${Math.max(1, Math.floor(opts.limit))}` : "";
|
|
204
|
+
const stmt = db.prepare(`SELECT * FROM universe ${where}${limit}`);
|
|
205
|
+
return opts.market ? stmt.all(opts.market) : stmt.all();
|
|
206
|
+
}
|
|
207
|
+
export function upsertCandlesBulk(rows) {
|
|
208
|
+
if (rows.length === 0)
|
|
209
|
+
return;
|
|
210
|
+
const db = getDb();
|
|
211
|
+
const stmt = db.prepare(`
|
|
212
|
+
INSERT INTO daily_candles (code, date, open_krw, high_krw, low_krw, close_krw, volume)
|
|
213
|
+
VALUES (@code, @date, @open_krw, @high_krw, @low_krw, @close_krw, @volume)
|
|
214
|
+
ON CONFLICT(code, date) DO UPDATE SET
|
|
215
|
+
open_krw = excluded.open_krw,
|
|
216
|
+
high_krw = excluded.high_krw,
|
|
217
|
+
low_krw = excluded.low_krw,
|
|
218
|
+
close_krw = excluded.close_krw,
|
|
219
|
+
volume = excluded.volume;
|
|
220
|
+
`);
|
|
221
|
+
const tx = db.transaction((list) => {
|
|
222
|
+
for (const r of list)
|
|
223
|
+
stmt.run(r);
|
|
224
|
+
});
|
|
225
|
+
tx(rows);
|
|
226
|
+
}
|
|
227
|
+
export function getCandles(code, days = 120) {
|
|
228
|
+
const db = getDb();
|
|
229
|
+
return db
|
|
230
|
+
.prepare("SELECT * FROM daily_candles WHERE code = ? ORDER BY date DESC LIMIT ?")
|
|
231
|
+
.all(code, days);
|
|
232
|
+
}
|
|
233
|
+
/* ─────────── Sync meta ─────────── */
|
|
234
|
+
export function recordSync(kind, item_count, status = "ok", message) {
|
|
235
|
+
const db = getDb();
|
|
236
|
+
db.prepare(`
|
|
237
|
+
INSERT INTO sync_meta (kind, last_run_at, item_count, status, message)
|
|
238
|
+
VALUES (?, ?, ?, ?, ?)
|
|
239
|
+
ON CONFLICT(kind) DO UPDATE SET
|
|
240
|
+
last_run_at = excluded.last_run_at,
|
|
241
|
+
item_count = excluded.item_count,
|
|
242
|
+
status = excluded.status,
|
|
243
|
+
message = excluded.message;
|
|
244
|
+
`).run(kind, new Date().toISOString(), item_count, status, message ?? null);
|
|
245
|
+
}
|
|
246
|
+
export function getSyncMeta(kind) {
|
|
247
|
+
const db = getDb();
|
|
248
|
+
const row = db.prepare("SELECT * FROM sync_meta WHERE kind = ?").get(kind);
|
|
249
|
+
return row ?? null;
|
|
250
|
+
}
|
|
251
|
+
export { DB_PATH };
|
|
252
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../../src-server/db/sqlite.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 페르소나 일기 (Diary) writer
|
|
3
|
+
*
|
|
4
|
+
* 매일 1회 (장 마감 16:00 KST) 또는 사용자 트리거로 페르소나가
|
|
5
|
+
* 자기 하루를 마크다운 일기로 작성. taeeun-life agent-server/memory/strategy.md
|
|
6
|
+
* 톤 차용: AI 트레이더가 화자 · 시장 진단 + 종목별 상태 + 학습 노트 + 내일 계획.
|
|
7
|
+
*
|
|
8
|
+
* 저장: ~/.mulsok-traders/diary/<YYYY-MM-DD>-<persona>.md
|
|
9
|
+
*
|
|
10
|
+
* ADR-008 익명 원칙: 클라이언트 안에서만 종목 코드 자유롭게 사용.
|
|
11
|
+
* 서버 readiness 송신 시 종목 마스킹 (server-sync.ts 측에서).
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { loadConfig } from "../config.js";
|
|
17
|
+
import { createLlmProvider } from "../llm/index.js";
|
|
18
|
+
import { readEvents } from "../journal/trade-journal.js";
|
|
19
|
+
import { getConditionWatcher } from "../watchers/condition-watcher.js";
|
|
20
|
+
import { runPersonaScreening } from "../personas/runner.js";
|
|
21
|
+
const DIARY_DIR = path.join(os.homedir(), ".mulsok-traders", "diary");
|
|
22
|
+
function ensureDir() {
|
|
23
|
+
if (!fs.existsSync(DIARY_DIR))
|
|
24
|
+
fs.mkdirSync(DIARY_DIR, { recursive: true, mode: 0o700 });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* KST 기준 오늘 날짜 (YYYY-MM-DD).
|
|
28
|
+
*
|
|
29
|
+
* SPEC-6 line 170: `todayKST() = Date.now() + 9h`
|
|
30
|
+
* export — 단위 테스트 + 다른 모듈 일관성 검증 가능.
|
|
31
|
+
*/
|
|
32
|
+
export function todayKST() {
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const kst = new Date(now.getTime() + 9 * 3600_000);
|
|
35
|
+
return kst.toISOString().slice(0, 10);
|
|
36
|
+
}
|
|
37
|
+
export function diaryPath(date, slug) {
|
|
38
|
+
return path.join(DIARY_DIR, `${date}-${slug}.md`);
|
|
39
|
+
}
|
|
40
|
+
export function readDiary(date, slug) {
|
|
41
|
+
const p = diaryPath(date, slug);
|
|
42
|
+
if (!fs.existsSync(p))
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
46
|
+
const stat = fs.statSync(p);
|
|
47
|
+
return { date, personaSlug: slug, body: raw, writtenAt: stat.mtime.toISOString(), path: p };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function listDiariesForPersona(slug, limit = 30) {
|
|
54
|
+
ensureDir();
|
|
55
|
+
const files = fs
|
|
56
|
+
.readdirSync(DIARY_DIR)
|
|
57
|
+
.filter((f) => f.endsWith(`-${slug}.md`))
|
|
58
|
+
.sort()
|
|
59
|
+
.reverse()
|
|
60
|
+
.slice(0, limit);
|
|
61
|
+
return files
|
|
62
|
+
.map((f) => {
|
|
63
|
+
const date = f.slice(0, 10);
|
|
64
|
+
return readDiary(date, slug);
|
|
65
|
+
})
|
|
66
|
+
.filter((d) => !!d);
|
|
67
|
+
}
|
|
68
|
+
async function buildContext(personaSlug) {
|
|
69
|
+
const today = todayKST();
|
|
70
|
+
// 오늘 의사결정
|
|
71
|
+
const startOfDay = new Date();
|
|
72
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
73
|
+
const events = readEvents({
|
|
74
|
+
from: startOfDay.toISOString(),
|
|
75
|
+
eventTypes: ["decision"],
|
|
76
|
+
personaSlug,
|
|
77
|
+
limit: 20,
|
|
78
|
+
});
|
|
79
|
+
const todayDecisions = events.map((ev) => {
|
|
80
|
+
const raw = ev.llmContext?.reasoning ?? "";
|
|
81
|
+
let action;
|
|
82
|
+
let symbol;
|
|
83
|
+
let reasoning;
|
|
84
|
+
try {
|
|
85
|
+
const m = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
86
|
+
const obj = JSON.parse(m ? m[1] : raw);
|
|
87
|
+
action = obj.action;
|
|
88
|
+
symbol = obj.symbol_name ?? obj.symbol_code;
|
|
89
|
+
reasoning = typeof obj.reasoning === "string" ? obj.reasoning : undefined;
|
|
90
|
+
}
|
|
91
|
+
catch { /* ignore */ }
|
|
92
|
+
return {
|
|
93
|
+
time: new Date(ev.timestamp).toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" }),
|
|
94
|
+
action,
|
|
95
|
+
symbol,
|
|
96
|
+
reasoning,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
// 활성 watcher (페르소나의 것만)
|
|
100
|
+
const watcherList = getConditionWatcher().list({ personaSlug, activeOnly: true });
|
|
101
|
+
const watchers = watcherList.map((w) => ({
|
|
102
|
+
intent: w.spec.intent,
|
|
103
|
+
type: w.spec.condition.type,
|
|
104
|
+
triggerCount: w.triggerCount,
|
|
105
|
+
}));
|
|
106
|
+
// 오늘 스크리닝 (engine 실행 · 비용 무료 ~50ms)
|
|
107
|
+
let screening = null;
|
|
108
|
+
let personaName = personaSlug;
|
|
109
|
+
try {
|
|
110
|
+
const result = await runPersonaScreening(personaSlug);
|
|
111
|
+
if (result && !result.skipped) {
|
|
112
|
+
personaName = result.persona.displayName?.split(" — ")[0] ?? personaSlug;
|
|
113
|
+
screening = {
|
|
114
|
+
universeSize: result.screener.universeSize,
|
|
115
|
+
candidatesCount: result.screener.candidates.length,
|
|
116
|
+
topNames: result.screener.candidates.slice(0, 5).map((c) => `${c.name}(${c.code})`),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { /* ignore · screening 안 돼도 일기 작성 진행 */ }
|
|
121
|
+
// 어제 일기 (있으면 발췌)
|
|
122
|
+
const yesterday = new Date();
|
|
123
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
124
|
+
const yDate = yesterday.toISOString().slice(0, 10);
|
|
125
|
+
const ydiary = readDiary(yDate, personaSlug);
|
|
126
|
+
const yesterdayDigest = ydiary
|
|
127
|
+
? ydiary.body.split("\n").slice(0, 12).join("\n").slice(0, 600)
|
|
128
|
+
: null;
|
|
129
|
+
return { personaSlug, personaName, date: today, todayDecisions, watchers, screening, yesterdayDigest };
|
|
130
|
+
}
|
|
131
|
+
function buildPrompt(ctx) {
|
|
132
|
+
const lines = [];
|
|
133
|
+
lines.push(`당신은 한국 주식 AI 트레이더 "${ctx.personaName}" 입니다.`);
|
|
134
|
+
lines.push(`오늘 ${ctx.date} 하루를 마치는 매매 일기를 작성하세요.`);
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push("## 톤·형식");
|
|
137
|
+
lines.push("- 자기 자신이 화자 (예: '오늘 시장은...', '나는...')");
|
|
138
|
+
lines.push("- 마크다운 (h2/h3 구조)");
|
|
139
|
+
lines.push("- 진솔하되 절제 · 트레이더의 일기");
|
|
140
|
+
lines.push("- 분량: 12~25 줄 (너무 길게 X)");
|
|
141
|
+
lines.push("");
|
|
142
|
+
lines.push("## 작성 섹션 (모두 포함)");
|
|
143
|
+
lines.push("1. **오늘 시장** — 한 줄 진단 (강세/약세/혼조 등)");
|
|
144
|
+
lines.push("2. **내가 본 것** — 스크리닝 결과 요약 + 주목한 종목 1~3개와 이유");
|
|
145
|
+
lines.push("3. **나의 판단** — 매수/매도/관망 결정 + 그 이유 (의사결정이 있었으면)");
|
|
146
|
+
lines.push("4. **지켜보는 것** — 등록한 감시자 의도 풀이");
|
|
147
|
+
lines.push("5. **내일 계획** — 짧게");
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push("## 데이터");
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push("### 오늘 스크리닝");
|
|
152
|
+
if (ctx.screening) {
|
|
153
|
+
lines.push(`- 우주 ${ctx.screening.universeSize.toLocaleString()}개 종목 → 후보 ${ctx.screening.candidatesCount}개`);
|
|
154
|
+
if (ctx.screening.topNames.length > 0) {
|
|
155
|
+
lines.push(`- 상위 후보: ${ctx.screening.topNames.join(", ")}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
lines.push("- (오늘 스크리닝 없음)");
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
lines.push("### 오늘 의사결정");
|
|
163
|
+
if (ctx.todayDecisions.length === 0) {
|
|
164
|
+
lines.push("- (오늘 의사결정 없음)");
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
for (const d of ctx.todayDecisions) {
|
|
168
|
+
lines.push(`- ${d.time} · ${d.action ?? "?"} ${d.symbol ?? ""}`);
|
|
169
|
+
if (d.reasoning)
|
|
170
|
+
lines.push(` (생각: ${d.reasoning.slice(0, 200)})`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
lines.push("");
|
|
174
|
+
lines.push("### 지금 지켜보는 것 (감시자)");
|
|
175
|
+
if (ctx.watchers.length === 0) {
|
|
176
|
+
lines.push("- (없음)");
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
for (const w of ctx.watchers) {
|
|
180
|
+
lines.push(`- [${w.type}] ${w.intent}${w.triggerCount > 0 ? ` (${w.triggerCount}회 발화)` : ""}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (ctx.yesterdayDigest) {
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push("### 어제 일기 (이어쓰기 참고)");
|
|
186
|
+
lines.push(ctx.yesterdayDigest);
|
|
187
|
+
}
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push("위 데이터를 바탕으로 일기를 작성하세요. 첫 줄은 `# ${ctx.date} · ${ctx.personaName}` 으로 시작.");
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 페르소나 일기 작성 + 저장.
|
|
194
|
+
* 이미 오늘 일기가 있으면 (overwrite=false 면) skip.
|
|
195
|
+
*/
|
|
196
|
+
export async function writeDiary(personaSlug, opts = {}) {
|
|
197
|
+
ensureDir();
|
|
198
|
+
const today = todayKST();
|
|
199
|
+
const filePath = diaryPath(today, personaSlug);
|
|
200
|
+
if (fs.existsSync(filePath) && !opts.overwrite) {
|
|
201
|
+
const existing = readDiary(today, personaSlug);
|
|
202
|
+
return { ok: true, path: filePath, body: existing?.body, usedModel: "(cached)" };
|
|
203
|
+
}
|
|
204
|
+
const cfg = loadConfig();
|
|
205
|
+
const prov = createLlmProvider(cfg);
|
|
206
|
+
if (!prov)
|
|
207
|
+
return { ok: false, error: "LLM provider 미설정" };
|
|
208
|
+
const ctx = await buildContext(personaSlug);
|
|
209
|
+
if (!ctx)
|
|
210
|
+
return { ok: false, error: `persona ${personaSlug} 컨텍스트 빌드 실패` };
|
|
211
|
+
const prompt = buildPrompt(ctx);
|
|
212
|
+
const systemPrompt = `당신은 ${ctx.personaName} 라는 AI 한국 주식 트레이더입니다. 매매 일기를 작성합니다. 마크다운 형식. 분량 ≤25줄.`;
|
|
213
|
+
// maxTokens 명시 X → provider 의 모델별 default (Anthropic 8192 / OpenAI 16384 / Gemini 8192)
|
|
214
|
+
// 사용자 정정 (2026-05-03): 답변 잘림 → 사용자 overwrite:true 재호출 → 비용 더 큼 → 없어야 한다.
|
|
215
|
+
// 5 섹션 일기는 보통 800~1500 tokens · 8K cap 이라 충분. 잘리면 stopReason="max_tokens" 로 감지.
|
|
216
|
+
let llmRes;
|
|
217
|
+
try {
|
|
218
|
+
llmRes = await prov.complete({
|
|
219
|
+
systemPrompt,
|
|
220
|
+
userPrompt: prompt,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
return { ok: false, error: `LLM 호출 실패: ${String(e)}` };
|
|
225
|
+
}
|
|
226
|
+
if (llmRes.stopReason === "max_tokens") {
|
|
227
|
+
console.warn(`[diary] ⚠️ ${personaSlug} 일기가 max_tokens cap 에 막혀 잘림 (output=${llmRes.usage?.outputTokens ?? "?"} tokens)`);
|
|
228
|
+
}
|
|
229
|
+
const body = llmRes.text.trim();
|
|
230
|
+
fs.writeFileSync(filePath, body, { mode: 0o600 });
|
|
231
|
+
return { ok: true, path: filePath, body, usedModel: llmRes.model };
|
|
232
|
+
}
|
|
233
|
+
/* ─────────── 자동 트리거 (16:00 KST) ─────────── */
|
|
234
|
+
let cronTimer = null;
|
|
235
|
+
/**
|
|
236
|
+
* 매 분 시각 확인 → 16:00 KST 도달 시 모든 구독 페르소나의 일기 작성.
|
|
237
|
+
* 단순 polling (cron lib 없이) · 클라이언트는 24/7 구동 안 할 수 있어 안전.
|
|
238
|
+
*/
|
|
239
|
+
export function startDiaryCron() {
|
|
240
|
+
if (cronTimer)
|
|
241
|
+
return;
|
|
242
|
+
cronTimer = setInterval(async () => {
|
|
243
|
+
const now = new Date();
|
|
244
|
+
const kst = new Date(now.getTime() + 9 * 3600_000);
|
|
245
|
+
if (kst.getUTCHours() === 16 && kst.getUTCMinutes() === 0) {
|
|
246
|
+
const cfg = loadConfig();
|
|
247
|
+
const subs = cfg.subscriptions ?? [];
|
|
248
|
+
for (const slug of subs) {
|
|
249
|
+
try {
|
|
250
|
+
const r = await writeDiary(slug);
|
|
251
|
+
console.log(`[diary] ${slug} ${r.ok ? "✓" : "✗"} ${r.path ?? r.error}`);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
console.error(`[diary] ${slug} cron 실패:`, e);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}, 60_000); // 1분마다 체크
|
|
259
|
+
console.log("[diary] cron started · 매일 16:00 KST 자동 작성");
|
|
260
|
+
}
|
|
261
|
+
export function stopDiaryCron() {
|
|
262
|
+
if (cronTimer)
|
|
263
|
+
clearInterval(cronTimer);
|
|
264
|
+
cronTimer = null;
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../../../src-server/diary/writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,iBAAiB,EAAE,OAAO,CAAC,CAAC;AAEtE,SAAS,SAAS;IAChB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;IACnD,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC;AAUD,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAY;IAClD,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,IAAI,IAAI,KAAK,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAY;IAClD,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC9F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,KAAK,GAAG,EAAE;IAC5D,SAAS,EAAE,CAAC;IACZ,MAAM,KAAK,GAAG,EAAE;SACb,WAAW,CAAC,SAAS,CAAC;SACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC;SACxC,IAAI,EAAE;SACN,OAAO,EAAE;SACT,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACnB,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,OAAO,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAAkB,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxC,CAAC;AAkBD,KAAK,UAAU,YAAY,CAAC,WAAmB;IAC7C,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IAEzB,UAAU;IACV,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC;IAC9B,UAAU,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,UAAU,CAAC;QACxB,IAAI,EAAE,UAAU,CAAC,WAAW,EAAE;QAC9B,UAAU,EAAE,CAAC,UAAU,CAAC;QACxB,WAAW;QACX,KAAK,EAAE,EAAE;KACV,CAAC,CAAC;IACH,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,EAAE,CAAC,UAAU,EAAE,SAAS,IAAI,EAAE,CAAC;QAC3C,IAAI,MAA0B,CAAC;QAC/B,IAAI,MAA0B,CAAC;QAC/B,IAAI,SAA6B,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACvC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;YACpB,MAAM,GAAG,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC;YAC5C,SAAS,GAAG,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QAC5E,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QACxB,OAAO;YACL,IAAI,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;YAChG,MAAM;YACN,MAAM;YACN,SAAS;SACV,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,wBAAwB;IACxB,MAAM,WAAW,GAAG,mBAAmB,EAAE,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACvC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM;QACrB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI;QAC3B,YAAY,EAAE,CAAC,CAAC,YAAY;KAC7B,CAAC,CAAC,CAAC;IAEJ,oCAAoC;IACpC,IAAI,SAAS,GAA8B,IAAI,CAAC;IAChD,IAAI,WAAW,GAAG,WAAW,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,WAAW,CAAC,CAAC;QACtD,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAC9B,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC;YACzE,SAAS,GAAG;gBACV,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,YAAY;gBAC1C,eAAe,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM;gBAClD,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC;aACpF,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAC,sCAAsC,CAAC,CAAC;IAElD,iBAAiB;IACjB,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC;IAC7B,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAC7C,MAAM,eAAe,GAAG,MAAM;QAC5B,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;QAC/D,CAAC,CAAC,IAAI,CAAC;IAET,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;AACzG,CAAC;AAED,SAAS,WAAW,CAAC,GAAiB;IACpC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,sBAAsB,GAAG,CAAC,WAAW,QAAQ,CAAC,CAAC;IAC1D,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,wBAAwB,CAAC,CAAC;IACnD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtB,KAAK,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;IACpD,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAChC,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACnC,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IACtC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAC/B,KAAK,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;IACjD,KAAK,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;IAC3D,KAAK,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;IAC7D,KAAK,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;IAC5C,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAChC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1B,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;QAClB,KAAK,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,cAAc,EAAE,aAAa,GAAG,CAAC,SAAS,CAAC,eAAe,GAAG,CAAC,CAAC;QAC7G,IAAI,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtC,KAAK,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC/B,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1B,IAAI,GAAG,CAAC,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC/B,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,cAAc,EAAE,CAAC;YACnC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,CAAC,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAClC,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvB,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;IACD,IAAI,GAAG,CAAC,eAAe,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAClC,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,yEAAyE,CAAC,CAAC;IACtF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAUD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,WAAmB,EAAE,OAAgC,EAAE;IACtF,SAAS,EAAE,CAAC;IACZ,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAC/C,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QAC/C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;IACnF,CAAC;IAED,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAE3D,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;IAC5C,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,WAAW,aAAa,EAAE,CAAC;IAE3E,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,YAAY,GAAG,OAAO,GAAG,CAAC,WAAW,uDAAuD,CAAC;IAEnG,wFAAwF;IACxF,yEAAyE;IACzE,gFAAgF;IAChF,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC;YAC3B,YAAY;YACZ,UAAU,EAAE,MAAM;SACnB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACzD,CAAC;IAED,IAAI,MAAM,CAAC,UAAU,KAAK,YAAY,EAAE,CAAC;QACvC,OAAO,CAAC,IAAI,CAAC,eAAe,WAAW,uCAAuC,MAAM,CAAC,KAAK,EAAE,YAAY,IAAI,GAAG,UAAU,CAAC,CAAC;IAC7H,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAChC,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAClD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;AACrE,CAAC;AAED,gDAAgD;AAEhD,IAAI,SAAS,GAA0B,IAAI,CAAC;AAE5C;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,IAAI,SAAS;QAAE,OAAO;IACtB,SAAS,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACjC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;QACnD,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,GAAG,CAAC,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC;YAC1D,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC;YACrC,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;gBACxB,IAAI,CAAC;oBACH,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;oBACjC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;gBAC1E,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,OAAO,CAAC,KAAK,CAAC,WAAW,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC;gBAC/C,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU;IACtB,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,SAAS;QAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACxC,SAAS,GAAG,IAAI,CAAC;AACnB,CAAC"}
|