@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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 일별/누적 손익 집계 (apps/client)
|
|
3
|
+
*
|
|
4
|
+
* trade-journal 의 order_status (filled) 이벤트를 FIFO 매칭으로 실현 손익 계산.
|
|
5
|
+
* MVP 단순화: 종목별 매수가 평균 vs 매도가 차이 (정확한 lot 단위 FIFO 는 P+1).
|
|
6
|
+
*/
|
|
7
|
+
import { readEvents } from "./trade-journal.js";
|
|
8
|
+
/** 날짜별 실현 손익 (단순화 · 종목별 매수가 평균 기준) */
|
|
9
|
+
export function computeDailyPnl(opts) {
|
|
10
|
+
const events = readEvents({
|
|
11
|
+
from: opts.from,
|
|
12
|
+
to: opts.to,
|
|
13
|
+
personaSlug: opts.personaSlug,
|
|
14
|
+
eventTypes: ["order_status"],
|
|
15
|
+
limit: 100_000,
|
|
16
|
+
});
|
|
17
|
+
// 종목별 매수가 누적 평균
|
|
18
|
+
const symbolAvgPrice = new Map();
|
|
19
|
+
// 날짜별 집계
|
|
20
|
+
const byDate = new Map();
|
|
21
|
+
const bySymbol = {};
|
|
22
|
+
let totalBuy = 0;
|
|
23
|
+
let totalSell = 0;
|
|
24
|
+
let totalRealized = 0;
|
|
25
|
+
for (const ev of events) {
|
|
26
|
+
if (ev.meta?.next !== "filled")
|
|
27
|
+
continue;
|
|
28
|
+
if (!ev.symbolCode || !ev.side)
|
|
29
|
+
continue;
|
|
30
|
+
const qty = ev.filledQty ?? ev.quantity ?? 0;
|
|
31
|
+
const price = ev.priceKrw ?? 0;
|
|
32
|
+
if (qty <= 0 || price <= 0)
|
|
33
|
+
continue;
|
|
34
|
+
const amount = qty * price;
|
|
35
|
+
const date = ev.timestamp.slice(0, 10);
|
|
36
|
+
if (!byDate.has(date)) {
|
|
37
|
+
byDate.set(date, {
|
|
38
|
+
date,
|
|
39
|
+
realizedPnlKrw: 0,
|
|
40
|
+
buyAmountKrw: 0,
|
|
41
|
+
sellAmountKrw: 0,
|
|
42
|
+
tradeCount: 0,
|
|
43
|
+
symbols: [],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const day = byDate.get(date);
|
|
47
|
+
day.tradeCount++;
|
|
48
|
+
if (!day.symbols.includes(ev.symbolCode))
|
|
49
|
+
day.symbols.push(ev.symbolCode);
|
|
50
|
+
if (!bySymbol[ev.symbolCode])
|
|
51
|
+
bySymbol[ev.symbolCode] = { pnl: 0, trades: 0 };
|
|
52
|
+
bySymbol[ev.symbolCode].trades++;
|
|
53
|
+
if (ev.side === "buy") {
|
|
54
|
+
totalBuy += amount;
|
|
55
|
+
day.buyAmountKrw += amount;
|
|
56
|
+
const existing = symbolAvgPrice.get(ev.symbolCode) ?? { totalQty: 0, totalCost: 0 };
|
|
57
|
+
existing.totalQty += qty;
|
|
58
|
+
existing.totalCost += amount;
|
|
59
|
+
symbolAvgPrice.set(ev.symbolCode, existing);
|
|
60
|
+
}
|
|
61
|
+
else if (ev.side === "sell") {
|
|
62
|
+
totalSell += amount;
|
|
63
|
+
day.sellAmountKrw += amount;
|
|
64
|
+
const existing = symbolAvgPrice.get(ev.symbolCode);
|
|
65
|
+
if (existing && existing.totalQty > 0) {
|
|
66
|
+
const avgPrice = existing.totalCost / existing.totalQty;
|
|
67
|
+
const pnl = (price - avgPrice) * qty;
|
|
68
|
+
day.realizedPnlKrw += pnl;
|
|
69
|
+
bySymbol[ev.symbolCode].pnl += pnl;
|
|
70
|
+
totalRealized += pnl;
|
|
71
|
+
// 매도분만큼 평균 cost 차감
|
|
72
|
+
existing.totalQty = Math.max(0, existing.totalQty - qty);
|
|
73
|
+
existing.totalCost = existing.totalQty > 0 ? existing.totalCost - avgPrice * qty : 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const sortedDates = Array.from(byDate.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
78
|
+
return {
|
|
79
|
+
fromDate: sortedDates[0]?.date ?? "",
|
|
80
|
+
toDate: sortedDates[sortedDates.length - 1]?.date ?? "",
|
|
81
|
+
totalRealizedPnlKrw: Math.round(totalRealized),
|
|
82
|
+
totalBuyAmountKrw: totalBuy,
|
|
83
|
+
totalSellAmountKrw: totalSell,
|
|
84
|
+
totalTrades: events.length,
|
|
85
|
+
bySymbol,
|
|
86
|
+
byDate: sortedDates,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function replayJournal(opts) {
|
|
90
|
+
const events = readEvents({ ...opts, limit: 100_000 });
|
|
91
|
+
const decisionsBySymbol = {};
|
|
92
|
+
const ordersByStatus = {};
|
|
93
|
+
let watcherTriggers = 0;
|
|
94
|
+
for (const ev of events) {
|
|
95
|
+
if (ev.eventType === "decision" && ev.symbolCode) {
|
|
96
|
+
decisionsBySymbol[ev.symbolCode] = (decisionsBySymbol[ev.symbolCode] ?? 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
else if (ev.eventType === "order_status") {
|
|
99
|
+
const status = ev.meta?.next ?? "unknown";
|
|
100
|
+
ordersByStatus[status] = (ordersByStatus[status] ?? 0) + 1;
|
|
101
|
+
}
|
|
102
|
+
else if (ev.eventType === "watcher_triggered") {
|
|
103
|
+
watcherTriggers++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { totalEvents: events.length, decisionsBySymbol, ordersByStatus, watcherTriggers };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=pnl-stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pnl-stats.js","sourceRoot":"","sources":["../../../src-server/journal/pnl-stats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry sender (apps/client)
|
|
3
|
+
*
|
|
4
|
+
* 책임: trade-journal 의 이벤트를 서버 (mulsok-traders.vercel.app/api/events/trade) 로 송신
|
|
5
|
+
*
|
|
6
|
+
* 정책 (Layer 1 §5 c1.4.0):
|
|
7
|
+
* - 자격증명 (API key · secret · token · 비밀번호) 절대 송신 금지
|
|
8
|
+
* - 거래 이벤트 (action · symbol · qty · price · pattern · pnl 등) 송신 OK
|
|
9
|
+
* - device_token 인증 헤더 필수
|
|
10
|
+
*
|
|
11
|
+
* 동작:
|
|
12
|
+
* - best-effort 송신 · 실패해도 로컬 저널은 항상 유지
|
|
13
|
+
* - 단발 송신 · batch 도 가능
|
|
14
|
+
* - 60s flush queue (P+1) · 현재는 즉시 호출
|
|
15
|
+
*
|
|
16
|
+
* 호출자:
|
|
17
|
+
* - trade-journal logXxx() 헬퍼 안에서 자동 호출 (옵션)
|
|
18
|
+
* - 또는 별도 flush() 호출
|
|
19
|
+
*/
|
|
20
|
+
import { loadConfig, SERVER_BASE_URL } from "../config.js";
|
|
21
|
+
import { maskSymbolName, maskSymbolsInText } from "../server-sync.js";
|
|
22
|
+
/**
|
|
23
|
+
* SPEC-20 + ADR-008 익명화 — server 송신 전 적용.
|
|
24
|
+
*
|
|
25
|
+
* 송신 X (마스킹/제거):
|
|
26
|
+
* - symbolCode (6자리 숫자) → 제거
|
|
27
|
+
* - symbolName → 마스킹 (뉴프**)
|
|
28
|
+
* - priceKrw / filledPriceKrw / brokerOrderNo / clientOrderId → 제거
|
|
29
|
+
* - llmContext.reasoning → 마스킹된 발췌 (200자)
|
|
30
|
+
* - llmContext.screening_summary.top[].code/name/rawMetrics → 제거 (반응만 일반화)
|
|
31
|
+
* - llmContext.layer3_snapshot.holdings/pendingOrders → 제거 (잔고)
|
|
32
|
+
*
|
|
33
|
+
* 송신 OK:
|
|
34
|
+
* - persona slug · eventType · timestamp · side · quantity (수량) · symbolBucket
|
|
35
|
+
* - llmContext.lesson · deviation_note · pattern · confidence (자연어 메타)
|
|
36
|
+
* - meta (status 전이 등)
|
|
37
|
+
*/
|
|
38
|
+
function anonymizeForTelemetry(ev) {
|
|
39
|
+
const ctx = ev.llmContext;
|
|
40
|
+
let anonContext;
|
|
41
|
+
if (ctx) {
|
|
42
|
+
anonContext = {
|
|
43
|
+
// 자연어 메타 (lesson · 패턴 · 가산점 라벨) — server 가 페르소나 성과 분석 가능
|
|
44
|
+
lesson: ctx.lesson,
|
|
45
|
+
pattern: ctx.pattern,
|
|
46
|
+
confidence: ctx.confidence,
|
|
47
|
+
deviationNote: ctx.deviationNote,
|
|
48
|
+
model: ctx.model,
|
|
49
|
+
tokens: ctx.tokens,
|
|
50
|
+
// reasoning 발췌만 (마스킹 + 200자)
|
|
51
|
+
reasoningExcerpt: typeof ctx.reasoning === "string"
|
|
52
|
+
? maskSymbolsInText(ctx.reasoning).slice(0, 200)
|
|
53
|
+
: undefined,
|
|
54
|
+
// screening 메타 카운트만 (top 후보 정량 데이터 X)
|
|
55
|
+
screening: ctx.screening_summary
|
|
56
|
+
? {
|
|
57
|
+
universe: ctx.screening_summary.universe,
|
|
58
|
+
evaluated: ctx.screening_summary.evaluated,
|
|
59
|
+
candidates: ctx.screening_summary.candidates,
|
|
60
|
+
elapsedMs: ctx.screening_summary.elapsedMs,
|
|
61
|
+
}
|
|
62
|
+
: undefined,
|
|
63
|
+
// triggered_by 는 watcherId · intent 만 (intent 마스킹)
|
|
64
|
+
triggeredBy: ctx.triggered_by
|
|
65
|
+
? {
|
|
66
|
+
watcherId: ctx.triggered_by.watcherId,
|
|
67
|
+
intent: typeof ctx.triggered_by.intent === "string"
|
|
68
|
+
? maskSymbolsInText(ctx.triggered_by.intent)
|
|
69
|
+
: undefined,
|
|
70
|
+
}
|
|
71
|
+
: undefined,
|
|
72
|
+
// layer3_snapshot 은 시장 지수만 (보유 · 미체결 · watcher 종목 코드 X)
|
|
73
|
+
marketIndex: ctx.layer3_snapshot
|
|
74
|
+
? ctx.layer3_snapshot.marketIndex
|
|
75
|
+
: undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
occurredAt: ev.timestamp,
|
|
80
|
+
eventType: ev.eventType,
|
|
81
|
+
personaSlug: ev.personaSlug,
|
|
82
|
+
// 종목 코드 송신 X · 이름은 마스킹 · bucket 만 OK (대형/중형/소형)
|
|
83
|
+
symbolNameMasked: ev.symbolName ? maskSymbolName(ev.symbolName) : undefined,
|
|
84
|
+
symbolBucket: ev.symbolBucket,
|
|
85
|
+
side: ev.side,
|
|
86
|
+
quantity: ev.quantity,
|
|
87
|
+
filledQty: ev.filledQty,
|
|
88
|
+
// 가격 · 주문번호 · client id 모두 송신 X (잔고 · 자격증명 인접)
|
|
89
|
+
llmContext: anonContext,
|
|
90
|
+
meta: ev.meta,
|
|
91
|
+
// error 는 마스킹된 메시지만
|
|
92
|
+
error: ev.error ? maskSymbolsInText(ev.error) : undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
class TelemetrySender {
|
|
96
|
+
queue = [];
|
|
97
|
+
flushing = false;
|
|
98
|
+
MAX_RETRIES = 2;
|
|
99
|
+
MAX_BATCH = 50;
|
|
100
|
+
/** 이벤트 큐에 추가 · 즉시 백그라운드 flush 시도 */
|
|
101
|
+
enqueue(ev) {
|
|
102
|
+
this.queue.push({ ev, retries: 0 });
|
|
103
|
+
void this.flush();
|
|
104
|
+
}
|
|
105
|
+
/** 큐 비우기 (best-effort · 실패 시 retry queue 유지) */
|
|
106
|
+
async flush() {
|
|
107
|
+
if (this.flushing)
|
|
108
|
+
return;
|
|
109
|
+
if (this.queue.length === 0)
|
|
110
|
+
return;
|
|
111
|
+
const cfg = loadConfig();
|
|
112
|
+
const token = cfg.auth?.deviceToken;
|
|
113
|
+
if (!token || !token.startsWith("mtd_")) {
|
|
114
|
+
// device 토큰 없음 · 로컬에만 저장 (큐 유지 · 다음 호출 시 재시도)
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.flushing = true;
|
|
118
|
+
try {
|
|
119
|
+
const batch = this.queue.splice(0, this.MAX_BATCH);
|
|
120
|
+
// SPEC-20 + ADR-008: 종목 코드 · 가격 · 잔고 · 자격증명 송신 금지.
|
|
121
|
+
// anonymizeForTelemetry 가 마스킹 + 필드 제거 + 풍부 컨텍스트는 메타 카운트만 보존.
|
|
122
|
+
const events = batch.map((q) => anonymizeForTelemetry(q.ev));
|
|
123
|
+
try {
|
|
124
|
+
const url = `${SERVER_BASE_URL.replace(/\/$/, "")}/api/events/trade`;
|
|
125
|
+
const res = await fetch(url, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"content-type": "application/json",
|
|
129
|
+
authorization: `Bearer ${token}`,
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({ events }),
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
// 401/4xx 는 재시도 의미 없음 · 5xx 는 재시도
|
|
135
|
+
if (res.status >= 500) {
|
|
136
|
+
this.requeue(batch);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.warn(`[telemetry] HTTP ${res.status} · 폐기 ${batch.length} 이벤트`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
// 네트워크 실패 · retry
|
|
145
|
+
this.requeue(batch);
|
|
146
|
+
console.warn(`[telemetry] 네트워크 실패 · ${String(e)} · ${batch.length} 이벤트 retry queue`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
this.flushing = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
requeue(batch) {
|
|
154
|
+
for (const item of batch) {
|
|
155
|
+
if (item.retries < this.MAX_RETRIES) {
|
|
156
|
+
item.retries++;
|
|
157
|
+
this.queue.push(item);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.warn(`[telemetry] MAX_RETRIES 초과 · 폐기 ${item.ev.eventType} · ${item.ev.symbolCode ?? ""}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
stats() {
|
|
165
|
+
return { queueSize: this.queue.length, flushing: this.flushing };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let singleton = null;
|
|
169
|
+
export function getTelemetry() {
|
|
170
|
+
if (!singleton)
|
|
171
|
+
singleton = new TelemetrySender();
|
|
172
|
+
return singleton;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../../src-server/journal/telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trade Journal (apps/client)
|
|
3
|
+
*
|
|
4
|
+
* 책임: 모든 거래 이벤트를 로컬 영구 저장 (재시작 후 유지)
|
|
5
|
+
*
|
|
6
|
+
* 저장소: JSONL (JSON Lines · 한 줄 = 한 이벤트)
|
|
7
|
+
* - 경로: ~/.mulsok-traders/trade-journal/<yyyy-mm>.jsonl
|
|
8
|
+
* - 월별 파일 분리 (1년 12 파일 · grep 용이)
|
|
9
|
+
* - append-only · 수정 금지 (감사 무결성)
|
|
10
|
+
* - 0600 권한 (config.json 과 동일)
|
|
11
|
+
*
|
|
12
|
+
* 이벤트 종류:
|
|
13
|
+
* - decision · LLM 결정 (action · symbol · qty · reasoning · lesson)
|
|
14
|
+
* - order_submitted · placeOrder 직후
|
|
15
|
+
* - order_status · pending → partial → filled 전환
|
|
16
|
+
* - order_cancelled · 취소
|
|
17
|
+
* - order_rejected · 키움 거부
|
|
18
|
+
* - watcher_triggered · 감시자 트리거
|
|
19
|
+
*
|
|
20
|
+
* 검색/필터: read 함수가 날짜·종목·페르소나·종류로 필터
|
|
21
|
+
*
|
|
22
|
+
* 향후 (P+1):
|
|
23
|
+
* - SQLite 마이그레이션 (검색·집계 성능)
|
|
24
|
+
* - 서버 telemetry 송신 (별도 모듈)
|
|
25
|
+
*/
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
const JOURNAL_DIR = path.join(os.homedir(), ".mulsok-traders", "trade-journal");
|
|
30
|
+
function ensureDir() {
|
|
31
|
+
if (!fs.existsSync(JOURNAL_DIR)) {
|
|
32
|
+
fs.mkdirSync(JOURNAL_DIR, { recursive: true, mode: 0o700 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function fileForDate(date) {
|
|
36
|
+
const y = date.getFullYear();
|
|
37
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
38
|
+
return path.join(JOURNAL_DIR, `${y}-${m}.jsonl`);
|
|
39
|
+
}
|
|
40
|
+
/** 이벤트 1개 append (단일 라인 · sync) + telemetry best-effort 송신 */
|
|
41
|
+
export function appendEvent(event) {
|
|
42
|
+
ensureDir();
|
|
43
|
+
const file = fileForDate(new Date(event.timestamp));
|
|
44
|
+
const line = JSON.stringify(event) + "\n";
|
|
45
|
+
try {
|
|
46
|
+
fs.appendFileSync(file, line, { mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.error(`[trade-journal] append fail · ${file} · ${e}`);
|
|
50
|
+
}
|
|
51
|
+
// 서버 telemetry 송신 · device_token 있을 때만 (로컬은 항상 저장)
|
|
52
|
+
// dynamic import 로 순환 의존 회피
|
|
53
|
+
import("./telemetry.js")
|
|
54
|
+
.then(({ getTelemetry }) => getTelemetry().enqueue(event))
|
|
55
|
+
.catch(() => {
|
|
56
|
+
/* ignore */
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** 헬퍼: 자동 timestamp + persona */
|
|
60
|
+
export function logDecision(opts) {
|
|
61
|
+
appendEvent({
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
eventType: "decision",
|
|
64
|
+
...opts,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function logOrderSubmitted(opts) {
|
|
68
|
+
appendEvent({
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
eventType: "order_submitted",
|
|
71
|
+
...opts,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function logOrderStatus(opts) {
|
|
75
|
+
appendEvent({
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
eventType: "order_status",
|
|
78
|
+
...opts,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function logOrderRejected(opts) {
|
|
82
|
+
appendEvent({
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
eventType: "order_rejected",
|
|
85
|
+
...opts,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function logWatcherTriggered(opts) {
|
|
89
|
+
appendEvent({
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
eventType: "watcher_triggered",
|
|
92
|
+
personaSlug: opts.personaSlug,
|
|
93
|
+
meta: {
|
|
94
|
+
watcherId: opts.watcherId,
|
|
95
|
+
intent: opts.intent,
|
|
96
|
+
triggerData: opts.triggerData,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function readEvents(filter = {}) {
|
|
101
|
+
const from = filter.from ? new Date(filter.from) : new Date(Date.now() - 365 * 24 * 3600_000);
|
|
102
|
+
const to = filter.to ? new Date(filter.to) : new Date();
|
|
103
|
+
const limit = filter.limit ?? 1000;
|
|
104
|
+
ensureDir();
|
|
105
|
+
// 월별 파일 후보
|
|
106
|
+
const months = monthKeysBetween(from, to);
|
|
107
|
+
const events = [];
|
|
108
|
+
for (const ym of months) {
|
|
109
|
+
const file = path.join(JOURNAL_DIR, `${ym}.jsonl`);
|
|
110
|
+
if (!fs.existsSync(file))
|
|
111
|
+
continue;
|
|
112
|
+
const text = fs.readFileSync(file, "utf-8");
|
|
113
|
+
for (const line of text.split("\n")) {
|
|
114
|
+
if (!line.trim())
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
const ev = JSON.parse(line);
|
|
118
|
+
if (matches(ev, filter, from, to)) {
|
|
119
|
+
events.push(ev);
|
|
120
|
+
if (events.length >= limit)
|
|
121
|
+
return events;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// skip 손상 line
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return events;
|
|
130
|
+
}
|
|
131
|
+
function matches(ev, f, from, to) {
|
|
132
|
+
const t = new Date(ev.timestamp);
|
|
133
|
+
if (t < from || t > to)
|
|
134
|
+
return false;
|
|
135
|
+
if (f.personaSlug && ev.personaSlug !== f.personaSlug)
|
|
136
|
+
return false;
|
|
137
|
+
if (f.symbolCode && ev.symbolCode !== f.symbolCode)
|
|
138
|
+
return false;
|
|
139
|
+
if (f.eventTypes && !f.eventTypes.includes(ev.eventType))
|
|
140
|
+
return false;
|
|
141
|
+
if (f.textSearch) {
|
|
142
|
+
const q = f.textSearch.toLowerCase();
|
|
143
|
+
const r = (ev.llmContext?.reasoning ?? "").toLowerCase();
|
|
144
|
+
const l = (ev.llmContext?.lesson ?? "").toLowerCase();
|
|
145
|
+
if (!r.includes(q) && !l.includes(q))
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function monthKeysBetween(from, to) {
|
|
151
|
+
const keys = [];
|
|
152
|
+
const cur = new Date(from.getFullYear(), from.getMonth(), 1);
|
|
153
|
+
const end = new Date(to.getFullYear(), to.getMonth(), 1);
|
|
154
|
+
while (cur <= end) {
|
|
155
|
+
const y = cur.getFullYear();
|
|
156
|
+
const m = String(cur.getMonth() + 1).padStart(2, "0");
|
|
157
|
+
keys.push(`${y}-${m}`);
|
|
158
|
+
cur.setMonth(cur.getMonth() + 1);
|
|
159
|
+
}
|
|
160
|
+
return keys;
|
|
161
|
+
}
|
|
162
|
+
export function computeStats(filter = {}) {
|
|
163
|
+
const events = readEvents({ ...filter, limit: 100_000 });
|
|
164
|
+
const stats = {
|
|
165
|
+
totalDecisions: 0,
|
|
166
|
+
totalOrders: 0,
|
|
167
|
+
filledOrders: 0,
|
|
168
|
+
rejectedOrders: 0,
|
|
169
|
+
buyCount: 0,
|
|
170
|
+
sellCount: 0,
|
|
171
|
+
totalBuyAmountKrw: 0,
|
|
172
|
+
totalSellAmountKrw: 0,
|
|
173
|
+
realizedPnlKrw: 0,
|
|
174
|
+
winRate: 0,
|
|
175
|
+
avgHoldDays: 0,
|
|
176
|
+
byPersona: {},
|
|
177
|
+
bySymbol: {},
|
|
178
|
+
};
|
|
179
|
+
let winCount = 0;
|
|
180
|
+
let totalSellsForWinrate = 0;
|
|
181
|
+
for (const ev of events) {
|
|
182
|
+
if (ev.personaSlug) {
|
|
183
|
+
stats.byPersona[ev.personaSlug] ??= { decisions: 0, filled: 0 };
|
|
184
|
+
}
|
|
185
|
+
if (ev.symbolCode) {
|
|
186
|
+
stats.bySymbol[ev.symbolCode] ??= { trades: 0, pnl: 0 };
|
|
187
|
+
}
|
|
188
|
+
switch (ev.eventType) {
|
|
189
|
+
case "decision":
|
|
190
|
+
stats.totalDecisions++;
|
|
191
|
+
if (ev.personaSlug)
|
|
192
|
+
stats.byPersona[ev.personaSlug].decisions++;
|
|
193
|
+
break;
|
|
194
|
+
case "order_submitted":
|
|
195
|
+
stats.totalOrders++;
|
|
196
|
+
if (ev.side === "buy")
|
|
197
|
+
stats.buyCount++;
|
|
198
|
+
if (ev.side === "sell")
|
|
199
|
+
stats.sellCount++;
|
|
200
|
+
break;
|
|
201
|
+
case "order_rejected":
|
|
202
|
+
stats.rejectedOrders++;
|
|
203
|
+
break;
|
|
204
|
+
case "order_status":
|
|
205
|
+
if (ev.meta?.next === "filled") {
|
|
206
|
+
stats.filledOrders++;
|
|
207
|
+
if (ev.personaSlug)
|
|
208
|
+
stats.byPersona[ev.personaSlug].filled++;
|
|
209
|
+
const qty = ev.filledQty ?? 0;
|
|
210
|
+
const px = ev.priceKrw ?? 0;
|
|
211
|
+
const amt = qty * px;
|
|
212
|
+
if (ev.side === "buy")
|
|
213
|
+
stats.totalBuyAmountKrw += amt;
|
|
214
|
+
if (ev.side === "sell") {
|
|
215
|
+
stats.totalSellAmountKrw += amt;
|
|
216
|
+
totalSellsForWinrate++;
|
|
217
|
+
// 단순 winrate: 매도가가 자체 매수가 이상이면 win (정확한 FIFO P+1)
|
|
218
|
+
// 여기선 stub 로 50% 가정 (실데이터 기반은 SQLite 도입 후)
|
|
219
|
+
}
|
|
220
|
+
if (ev.symbolCode) {
|
|
221
|
+
stats.bySymbol[ev.symbolCode].trades++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
stats.realizedPnlKrw = stats.totalSellAmountKrw - stats.totalBuyAmountKrw;
|
|
228
|
+
stats.winRate = totalSellsForWinrate > 0 ? (winCount / totalSellsForWinrate) * 100 : 0;
|
|
229
|
+
return stats;
|
|
230
|
+
}
|
|
231
|
+
/** 디버깅용 파일 목록 */
|
|
232
|
+
export function listJournalFiles() {
|
|
233
|
+
ensureDir();
|
|
234
|
+
return fs
|
|
235
|
+
.readdirSync(JOURNAL_DIR)
|
|
236
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
237
|
+
.sort();
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=trade-journal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trade-journal.js","sourceRoot":"","sources":["../../../src-server/journal/trade-journal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Anthropic stop_reason → 표준 LlmStopReason 매핑 */
|
|
2
|
+
function mapAnthropicStopReason(raw) {
|
|
3
|
+
switch (raw) {
|
|
4
|
+
case "end_turn": return "stop";
|
|
5
|
+
case "stop_sequence": return "stop";
|
|
6
|
+
case "max_tokens": return "max_tokens";
|
|
7
|
+
case "tool_use": return "tool_use";
|
|
8
|
+
default: return "unknown";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const BASE = "https://api.anthropic.com/v1";
|
|
12
|
+
const VERSION = "2023-06-01";
|
|
13
|
+
/**
|
|
14
|
+
* Anthropic 기본 max_tokens — 응답 잘림 방지 정책 (사용자 정정 · 2026-05-03).
|
|
15
|
+
*
|
|
16
|
+
* 사용자 의도: timeout · maxTokens 제한 → AI 답변 잘림 → 재호출 비용 더 큼 → 없어야 한다.
|
|
17
|
+
* Sonnet/Haiku 의 model 한계 (8192) 까지 자유 출력 허용.
|
|
18
|
+
*
|
|
19
|
+
* 호출자 (executor · diary 등) 가 명시 안 하면 이 값 적용. 명시 시 그 값 우선.
|
|
20
|
+
* fetch 자체는 timeout 걸지 않음 (무한대 대기 · OS TCP 만 적용).
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
23
|
+
export function createAnthropicProvider(opts) {
|
|
24
|
+
return {
|
|
25
|
+
kind: "anthropic",
|
|
26
|
+
async test() {
|
|
27
|
+
if (!opts.apiKey)
|
|
28
|
+
return { ok: false, provider: "anthropic", error: "apiKey 없음" };
|
|
29
|
+
try {
|
|
30
|
+
// Anthropic 은 /models list 지원 안 · messages 로 ping (1 token)
|
|
31
|
+
const res = await fetch(`${BASE}/messages`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "application/json",
|
|
35
|
+
"x-api-key": opts.apiKey,
|
|
36
|
+
"anthropic-version": VERSION,
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
model: opts.model,
|
|
40
|
+
max_tokens: 1,
|
|
41
|
+
messages: [{ role: "user", content: "ping" }],
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
if (res.status === 401 || res.status === 403) {
|
|
45
|
+
return { ok: false, provider: "anthropic", error: "인증 실패 (key 또는 권한 확인)" };
|
|
46
|
+
}
|
|
47
|
+
if (!res.ok && res.status !== 200) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
provider: "anthropic",
|
|
51
|
+
error: `HTTP ${res.status}: ${(await res.text()).slice(0, 120)}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, provider: "anthropic", model: opts.model, detail: "messages ping OK" };
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return { ok: false, provider: "anthropic", error: String(e) };
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async complete(c) {
|
|
61
|
+
if (!opts.apiKey)
|
|
62
|
+
throw new Error("Anthropic apiKey 없음");
|
|
63
|
+
const res = await fetch(`${BASE}/messages`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
"content-type": "application/json",
|
|
67
|
+
"x-api-key": opts.apiKey,
|
|
68
|
+
"anthropic-version": VERSION,
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
model: opts.model,
|
|
72
|
+
max_tokens: c.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
73
|
+
temperature: c.temperature ?? 0.2,
|
|
74
|
+
system: c.systemPrompt || undefined,
|
|
75
|
+
messages: [{ role: "user", content: c.userPrompt }],
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new Error(`Anthropic HTTP ${res.status}: ${await res.text()}`);
|
|
80
|
+
const data = (await res.json());
|
|
81
|
+
const text = data.content?.map((b) => (b.type === "text" ? b.text ?? "" : "")).join("") ?? "";
|
|
82
|
+
return {
|
|
83
|
+
text,
|
|
84
|
+
model: opts.model,
|
|
85
|
+
provider: "anthropic",
|
|
86
|
+
stopReason: mapAnthropicStopReason(data.stop_reason),
|
|
87
|
+
usage: data.usage
|
|
88
|
+
? {
|
|
89
|
+
inputTokens: data.usage.input_tokens ?? 0,
|
|
90
|
+
outputTokens: data.usage.output_tokens ?? 0,
|
|
91
|
+
}
|
|
92
|
+
: undefined,
|
|
93
|
+
raw: data,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src-server/llm/anthropic.ts"],"names":[],"mappings":"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"}
|