opencandle 0.10.0 → 0.11.1
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 +12 -9
- package/dist/analysts/orchestrator.d.ts +3 -4
- package/dist/analysts/orchestrator.js +34 -44
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli-main.js +242 -9
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -9
- package/dist/config.js +7 -10
- package/dist/config.js.map +1 -1
- package/dist/doctor/report.js +1 -1
- package/dist/doctor/report.js.map +1 -1
- package/dist/infra/cache.d.ts +6 -0
- package/dist/infra/cache.js +16 -6
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/freshness.d.ts +21 -0
- package/dist/infra/freshness.js +119 -0
- package/dist/infra/freshness.js.map +1 -0
- package/dist/infra/market-calendar.d.ts +14 -0
- package/dist/infra/market-calendar.js +137 -0
- package/dist/infra/market-calendar.js.map +1 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/daily-report.d.ts +1 -1
- package/dist/market-state/daily-report.js +5 -16
- package/dist/market-state/daily-report.js.map +1 -1
- package/dist/market-state/service.d.ts +0 -35
- package/dist/market-state/service.js +0 -63
- package/dist/market-state/service.js.map +1 -1
- package/dist/market-state/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- package/dist/memory/sqlite.js +20 -19
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/onboarding/providers.d.ts +25 -1
- package/dist/onboarding/providers.js +26 -0
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/pi/opencandle-extension.js +57 -226
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-action-dedupe.d.ts +6 -0
- package/dist/pi/session-action-dedupe.js +126 -0
- package/dist/pi/session-action-dedupe.js.map +1 -0
- package/dist/pi/session-writer-lock.d.ts +26 -2
- package/dist/pi/session-writer-lock.js +230 -18
- package/dist/pi/session-writer-lock.js.map +1 -1
- package/dist/pi/setup.js +5 -5
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tui-session-coordinator.d.ts +15 -0
- package/dist/pi/tui-session-coordinator.js +283 -0
- package/dist/pi/tui-session-coordinator.js.map +1 -0
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +1 -1
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/providers/alpha-vantage.js +6 -0
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -0
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/polymarket.d.ts +2 -0
- package/dist/providers/polymarket.js +163 -0
- package/dist/providers/polymarket.js.map +1 -0
- package/dist/providers/wrap-provider.js +5 -3
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -0
- package/dist/providers/yahoo-finance.js +206 -10
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +4 -5
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -2
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js +18 -3
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +1 -1
- package/dist/routing/router.d.ts +1 -1
- package/dist/routing/router.js +309 -9
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -0
- package/dist/routing/slot-resolver.js +1 -1
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/numeric-claims.d.ts +23 -0
- package/dist/runtime/numeric-claims.js +100 -0
- package/dist/runtime/numeric-claims.js.map +1 -0
- package/dist/runtime/planning-evidence.js +1 -76
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +10 -1
- package/dist/runtime/prompt-step.js +116 -2
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +11 -1
- package/dist/runtime/session-coordinator.js +354 -62
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/validation.d.ts +3 -1
- package/dist/runtime/workflow-events.d.ts +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-types.d.ts +3 -0
- package/dist/runtime/workflow-types.js.map +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -0
- package/dist/tools/fundamentals/comps.js +20 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +190 -76
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/macro/event-probabilities.d.ts +8 -0
- package/dist/tools/macro/event-probabilities.js +121 -0
- package/dist/tools/macro/event-probabilities.js.map +1 -0
- package/dist/tools/market/crypto-price.d.ts +4 -1
- package/dist/tools/market/crypto-price.js +10 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.js +14 -3
- package/dist/tools/market/screen-stocks.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +4 -1
- package/dist/tools/market/stock-quote.js +9 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +4 -1
- package/dist/tools/options/option-chain.js +12 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/daily-report.js +10 -4
- package/dist/tools/portfolio/daily-report.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +23 -5
- package/dist/tools/technical/backtest.js +131 -94
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/fundamentals.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/market.d.ts +2 -0
- package/dist/types/options.d.ts +1 -0
- package/dist/types/prediction-markets.d.ts +13 -0
- package/dist/types/prediction-markets.js +2 -0
- package/dist/types/prediction-markets.js.map +1 -0
- package/gui/server/chat-event-adapter.ts +89 -5
- package/gui/server/http-routes.ts +661 -37
- package/gui/server/invoke-tool.ts +165 -16
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/local-session-coordinator.ts +97 -0
- package/gui/server/market-state-api.ts +64 -48
- package/gui/server/projector.ts +132 -3
- package/gui/server/server.ts +51 -6
- package/gui/server/session-actions.ts +146 -45
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/tool-metadata.ts +3 -1
- package/gui/server/ws-hub.ts +77 -10
- package/gui/shared/chat-events.ts +10 -1
- package/gui/shared/event-reducer.ts +1 -0
- package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
- package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
- package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +13 -15
- package/src/analysts/orchestrator.ts +35 -56
- package/src/cli-main.ts +253 -13
- package/src/config.ts +12 -20
- package/src/doctor/report.ts +1 -1
- package/src/infra/cache.ts +24 -11
- package/src/infra/freshness.ts +165 -0
- package/src/infra/market-calendar.ts +193 -0
- package/src/infra/rate-limiter.ts +1 -0
- package/src/market-state/daily-report.ts +6 -16
- package/src/market-state/service.ts +0 -136
- package/src/market-state/summaries.ts +75 -0
- package/src/memory/sqlite.ts +23 -19
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +67 -270
- package/src/pi/session-action-dedupe.ts +155 -0
- package/src/pi/session-writer-lock.ts +290 -20
- package/src/pi/setup.ts +6 -6
- package/src/pi/tui-session-coordinator.ts +351 -0
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +1 -1
- package/src/providers/alpha-vantage.ts +6 -0
- package/src/providers/coingecko.ts +2 -0
- package/src/providers/polymarket.ts +214 -0
- package/src/providers/wrap-provider.ts +5 -3
- package/src/providers/yahoo-finance.ts +339 -27
- package/src/routing/classify-intent.ts +4 -5
- package/src/routing/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -2
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router-prompt.ts +1 -1
- package/src/routing/router.ts +379 -8
- package/src/routing/slot-resolver.ts +1 -1
- package/src/runtime/evidence.ts +1 -0
- package/src/runtime/numeric-claims.ts +108 -0
- package/src/runtime/planning-evidence.ts +6 -91
- package/src/runtime/prompt-step.ts +127 -2
- package/src/runtime/session-coordinator.ts +464 -74
- package/src/runtime/validation.ts +1 -1
- package/src/runtime/workflow-events.ts +2 -0
- package/src/runtime/workflow-types.ts +3 -0
- package/src/system-prompt.ts +1 -1
- package/src/tools/AGENTS.md +1 -1
- package/src/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +217 -87
- package/src/tools/index.ts +3 -3
- package/src/tools/macro/event-probabilities.ts +141 -0
- package/src/tools/market/crypto-price.ts +14 -2
- package/src/tools/market/screen-stocks.ts +14 -3
- package/src/tools/market/stock-quote.ts +49 -43
- package/src/tools/options/option-chain.ts +16 -2
- package/src/tools/portfolio/daily-report.ts +10 -4
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/technical/backtest.ts +167 -108
- package/src/tools/technical/indicators.ts +1 -1
- package/src/types/fundamentals.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/market.ts +2 -0
- package/src/types/options.ts +1 -0
- package/src/types/prediction-markets.ts +13 -0
- package/dist/tools/portfolio/predictions.d.ts +0 -55
- package/dist/tools/portfolio/predictions.js +0 -422
- package/dist/tools/portfolio/predictions.js.map +0 -1
- package/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
- package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
- package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
- package/src/tools/portfolio/predictions.ts +0 -553
|
@@ -1,553 +0,0 @@
|
|
|
1
|
-
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { isZeroFilledQuote, resolveYahooInstrument } from "../../market-state/resolve.js";
|
|
4
|
-
import { resolveInstrumentForMutation } from "../../market-state/resolve-for-mutation.js";
|
|
5
|
-
import { MarketStateService, type PredictionRecord } from "../../market-state/service.js";
|
|
6
|
-
import { initDefaultDatabase } from "../../memory/sqlite.js";
|
|
7
|
-
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
8
|
-
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
9
|
-
|
|
10
|
-
export interface Prediction {
|
|
11
|
-
id?: number;
|
|
12
|
-
instrumentId?: number;
|
|
13
|
-
symbol: string;
|
|
14
|
-
direction: "bullish" | "bearish" | "neutral";
|
|
15
|
-
conviction: number; // 1-10
|
|
16
|
-
entryPrice: number;
|
|
17
|
-
targetPrice?: number;
|
|
18
|
-
date: string;
|
|
19
|
-
expiresAt: string;
|
|
20
|
-
timeframeDays: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface PredictionCheckResult {
|
|
24
|
-
total: number;
|
|
25
|
-
open: number;
|
|
26
|
-
correct: number;
|
|
27
|
-
wrong: number;
|
|
28
|
-
hitRate: number;
|
|
29
|
-
weightedHitRate: number;
|
|
30
|
-
details: Array<{
|
|
31
|
-
symbol: string;
|
|
32
|
-
direction: string;
|
|
33
|
-
conviction: number;
|
|
34
|
-
entryPrice: number;
|
|
35
|
-
targetPrice?: number;
|
|
36
|
-
currentPrice: number | null;
|
|
37
|
-
pnlPercent: number | null;
|
|
38
|
-
correct: boolean;
|
|
39
|
-
status: "open" | "resolved";
|
|
40
|
-
targetHit?: boolean;
|
|
41
|
-
dataGap?: string;
|
|
42
|
-
}>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function recordPrediction(params: {
|
|
46
|
-
symbol: string;
|
|
47
|
-
direction: "bullish" | "bearish" | "neutral";
|
|
48
|
-
conviction: number;
|
|
49
|
-
entryPrice: number;
|
|
50
|
-
targetPrice?: number;
|
|
51
|
-
timeframeDays: number;
|
|
52
|
-
}): Promise<Prediction> {
|
|
53
|
-
const db = initDefaultDatabase();
|
|
54
|
-
const service = new MarketStateService(db);
|
|
55
|
-
try {
|
|
56
|
-
const instrument = await resolveYahooInstrument(params.symbol);
|
|
57
|
-
const record = service.recordPrediction({
|
|
58
|
-
instrument,
|
|
59
|
-
direction: params.direction,
|
|
60
|
-
conviction: params.conviction,
|
|
61
|
-
entryPrice: params.entryPrice,
|
|
62
|
-
targetPrice: params.targetPrice,
|
|
63
|
-
timeframeDays: params.timeframeDays,
|
|
64
|
-
});
|
|
65
|
-
return predictionRecordToPrediction(record, params.timeframeDays);
|
|
66
|
-
} finally {
|
|
67
|
-
db.close();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function checkPredictions(
|
|
72
|
-
predictions: Prediction[],
|
|
73
|
-
currentPrices: Map<string, number>,
|
|
74
|
-
now: Date = new Date(),
|
|
75
|
-
): PredictionCheckResult {
|
|
76
|
-
if (predictions.length === 0) {
|
|
77
|
-
return { total: 0, open: 0, correct: 0, wrong: 0, hitRate: 0, weightedHitRate: 0, details: [] };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const details: PredictionCheckResult["details"] = [];
|
|
81
|
-
let totalConviction = 0;
|
|
82
|
-
let correctConviction = 0;
|
|
83
|
-
let openCount = 0;
|
|
84
|
-
|
|
85
|
-
const nowStr = now.toISOString().split("T")[0];
|
|
86
|
-
|
|
87
|
-
for (const p of predictions) {
|
|
88
|
-
const currentPrice = currentPrices.get(p.symbol);
|
|
89
|
-
if (currentPrice == null) {
|
|
90
|
-
openCount++;
|
|
91
|
-
details.push({
|
|
92
|
-
symbol: p.symbol,
|
|
93
|
-
direction: p.direction,
|
|
94
|
-
conviction: p.conviction,
|
|
95
|
-
entryPrice: p.entryPrice,
|
|
96
|
-
currentPrice: null,
|
|
97
|
-
pnlPercent: null,
|
|
98
|
-
correct: false,
|
|
99
|
-
status: "open",
|
|
100
|
-
dataGap: "quote unavailable",
|
|
101
|
-
});
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!Number.isFinite(p.entryPrice) || p.entryPrice <= 0) {
|
|
106
|
-
openCount++;
|
|
107
|
-
details.push({
|
|
108
|
-
symbol: p.symbol,
|
|
109
|
-
direction: p.direction,
|
|
110
|
-
conviction: p.conviction,
|
|
111
|
-
entryPrice: p.entryPrice,
|
|
112
|
-
targetPrice: p.targetPrice,
|
|
113
|
-
currentPrice,
|
|
114
|
-
pnlPercent: null,
|
|
115
|
-
correct: false,
|
|
116
|
-
status: "open",
|
|
117
|
-
dataGap: "invalid entry price",
|
|
118
|
-
});
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const isExpired = p.expiresAt <= nowStr;
|
|
123
|
-
const pnlPercent = (currentPrice - p.entryPrice) / p.entryPrice;
|
|
124
|
-
|
|
125
|
-
if (!isExpired) {
|
|
126
|
-
openCount++;
|
|
127
|
-
const targetHit =
|
|
128
|
-
p.targetPrice != null &&
|
|
129
|
-
((p.direction === "bullish" && currentPrice >= p.targetPrice) ||
|
|
130
|
-
(p.direction === "bearish" && currentPrice <= p.targetPrice));
|
|
131
|
-
details.push({
|
|
132
|
-
symbol: p.symbol,
|
|
133
|
-
direction: p.direction,
|
|
134
|
-
conviction: p.conviction,
|
|
135
|
-
entryPrice: p.entryPrice,
|
|
136
|
-
targetPrice: p.targetPrice,
|
|
137
|
-
currentPrice,
|
|
138
|
-
pnlPercent,
|
|
139
|
-
correct: false,
|
|
140
|
-
status: "open",
|
|
141
|
-
targetHit,
|
|
142
|
-
});
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const correct =
|
|
147
|
-
(p.direction === "bullish" && currentPrice > p.entryPrice) ||
|
|
148
|
-
(p.direction === "bearish" && currentPrice < p.entryPrice) ||
|
|
149
|
-
(p.direction === "neutral" && Math.abs(pnlPercent) < 0.02);
|
|
150
|
-
|
|
151
|
-
details.push({
|
|
152
|
-
symbol: p.symbol,
|
|
153
|
-
direction: p.direction,
|
|
154
|
-
conviction: p.conviction,
|
|
155
|
-
entryPrice: p.entryPrice,
|
|
156
|
-
currentPrice,
|
|
157
|
-
pnlPercent,
|
|
158
|
-
correct,
|
|
159
|
-
status: "resolved",
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
totalConviction += p.conviction;
|
|
163
|
-
if (correct) correctConviction += p.conviction;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const resolved = details.filter((d) => d.status === "resolved");
|
|
167
|
-
const correctCount = resolved.filter((d) => d.correct).length;
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
total: details.length,
|
|
171
|
-
open: openCount,
|
|
172
|
-
correct: correctCount,
|
|
173
|
-
wrong: resolved.length - correctCount,
|
|
174
|
-
hitRate: resolved.length > 0 ? correctCount / resolved.length : 0,
|
|
175
|
-
weightedHitRate: totalConviction > 0 ? correctConviction / totalConviction : 0,
|
|
176
|
-
details,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const params = Type.Object({
|
|
181
|
-
action: Type.Union([Type.Literal("record"), Type.Literal("check"), Type.Literal("cancel")], {
|
|
182
|
-
description:
|
|
183
|
-
"record: save a new prediction. check: evaluate all predictions against current prices. cancel: close an open prediction without scoring it.",
|
|
184
|
-
}),
|
|
185
|
-
id: Type.Optional(
|
|
186
|
-
Type.Integer({ minimum: 1, description: "Prediction id (required for cancel)" }),
|
|
187
|
-
),
|
|
188
|
-
symbol: Type.Optional(Type.String({ description: "Ticker symbol (required for record)" })),
|
|
189
|
-
direction: Type.Optional(
|
|
190
|
-
Type.Union([Type.Literal("bullish"), Type.Literal("bearish"), Type.Literal("neutral")], {
|
|
191
|
-
description: "Predicted direction (required for record)",
|
|
192
|
-
}),
|
|
193
|
-
),
|
|
194
|
-
conviction: Type.Optional(
|
|
195
|
-
Type.Integer({ minimum: 1, maximum: 10, description: "Conviction 1-10 (required for record)" }),
|
|
196
|
-
),
|
|
197
|
-
entry_price: Type.Optional(
|
|
198
|
-
Type.Number({
|
|
199
|
-
exclusiveMinimum: 0,
|
|
200
|
-
description: "Entry price at time of prediction (required for record)",
|
|
201
|
-
}),
|
|
202
|
-
),
|
|
203
|
-
target_price: Type.Optional(
|
|
204
|
-
Type.Number({ exclusiveMinimum: 0, description: "Optional target price" }),
|
|
205
|
-
),
|
|
206
|
-
timeframe_days: Type.Optional(
|
|
207
|
-
Type.Integer({
|
|
208
|
-
minimum: 1,
|
|
209
|
-
maximum: 3650,
|
|
210
|
-
description: "Timeframe in days for the prediction (default: 30)",
|
|
211
|
-
}),
|
|
212
|
-
),
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
export const predictionsTool: AgentTool<typeof params> = {
|
|
216
|
-
name: "track_prediction",
|
|
217
|
-
label: "Prediction Tracker",
|
|
218
|
-
description:
|
|
219
|
-
"Track your analysis predictions and measure accuracy over time. Record: save a directional prediction with conviction. Check: evaluate all predictions against current prices, compute hit rate and conviction-weighted accuracy. Inspired by ATLAS's Darwinian scoring approach.",
|
|
220
|
-
parameters: params,
|
|
221
|
-
async execute(_toolCallId, args) {
|
|
222
|
-
if (args.action === "cancel") {
|
|
223
|
-
if (args.id == null) {
|
|
224
|
-
throw new Error("id is required for cancel action.");
|
|
225
|
-
}
|
|
226
|
-
const db = initDefaultDatabase();
|
|
227
|
-
try {
|
|
228
|
-
const service = new MarketStateService(db);
|
|
229
|
-
const existing = service.listPredictions().find((record) => record.id === args.id);
|
|
230
|
-
if (existing == null) {
|
|
231
|
-
return {
|
|
232
|
-
content: [{ type: "text", text: `Prediction #${args.id} not found.` }],
|
|
233
|
-
details: null,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
if (existing.status !== "open") {
|
|
237
|
-
return {
|
|
238
|
-
content: [
|
|
239
|
-
{ type: "text", text: `Prediction #${args.id} is already ${existing.status}.` },
|
|
240
|
-
],
|
|
241
|
-
details: existing,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
const now = new Date().toISOString();
|
|
245
|
-
const cancelled = service.updatePredictionOutcome({
|
|
246
|
-
id: args.id,
|
|
247
|
-
status: "cancelled",
|
|
248
|
-
resolvedAt: now,
|
|
249
|
-
result: { reason: "user_cancelled" },
|
|
250
|
-
});
|
|
251
|
-
return {
|
|
252
|
-
content: [
|
|
253
|
-
{ type: "text", text: `Cancelled prediction #${args.id} for ${cancelled.symbol}.` },
|
|
254
|
-
],
|
|
255
|
-
details: cancelled,
|
|
256
|
-
};
|
|
257
|
-
} finally {
|
|
258
|
-
db.close();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (args.action === "record") {
|
|
263
|
-
if (!args.symbol || !args.direction || args.conviction == null || args.entry_price == null) {
|
|
264
|
-
throw new Error(
|
|
265
|
-
"symbol, direction, conviction, and entry_price are required for record action.",
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
if (!Number.isInteger(args.conviction) || args.conviction < 1 || args.conviction > 10) {
|
|
269
|
-
throw new Error("conviction must be between 1 and 10.");
|
|
270
|
-
}
|
|
271
|
-
if (args.entry_price <= 0) {
|
|
272
|
-
throw new Error("entry_price must be greater than 0.");
|
|
273
|
-
}
|
|
274
|
-
if (args.target_price != null && args.target_price <= 0) {
|
|
275
|
-
throw new Error("target_price must be greater than 0.");
|
|
276
|
-
}
|
|
277
|
-
if (
|
|
278
|
-
args.timeframe_days != null &&
|
|
279
|
-
(!Number.isInteger(args.timeframe_days) ||
|
|
280
|
-
args.timeframe_days < 1 ||
|
|
281
|
-
args.timeframe_days > 3650)
|
|
282
|
-
) {
|
|
283
|
-
throw new Error("timeframe_days must be an integer between 1 and 3650.");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const resolution = await resolveInstrumentForMutation(args.symbol);
|
|
287
|
-
if (resolution.status === "needs_selection") {
|
|
288
|
-
return {
|
|
289
|
-
content: [
|
|
290
|
-
{
|
|
291
|
-
type: "text",
|
|
292
|
-
text: `Could not verify ${resolution.query}. Choose one of the returned candidates before recording the prediction.`,
|
|
293
|
-
},
|
|
294
|
-
],
|
|
295
|
-
details: resolution,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const db = initDefaultDatabase();
|
|
300
|
-
let prediction: Prediction;
|
|
301
|
-
try {
|
|
302
|
-
const service = new MarketStateService(db);
|
|
303
|
-
const record = service.recordPrediction({
|
|
304
|
-
instrument: resolution.instrument,
|
|
305
|
-
direction: args.direction,
|
|
306
|
-
conviction: args.conviction,
|
|
307
|
-
entryPrice: args.entry_price,
|
|
308
|
-
targetPrice: args.target_price,
|
|
309
|
-
timeframeDays: args.timeframe_days ?? 30,
|
|
310
|
-
});
|
|
311
|
-
prediction = predictionRecordToPrediction(record, args.timeframe_days ?? 30);
|
|
312
|
-
} finally {
|
|
313
|
-
db.close();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
content: [
|
|
318
|
-
{
|
|
319
|
-
type: "text",
|
|
320
|
-
text: `Recorded: ${prediction.symbol} ${prediction.direction} (conviction ${prediction.conviction}/10) at $${prediction.entryPrice}. Expires ${prediction.expiresAt}.`,
|
|
321
|
-
},
|
|
322
|
-
],
|
|
323
|
-
details: prediction,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const db = initDefaultDatabase();
|
|
328
|
-
try {
|
|
329
|
-
const service = new MarketStateService(db);
|
|
330
|
-
const records = service.listPredictions();
|
|
331
|
-
const openRecords = records.filter((record) => record.status === "open");
|
|
332
|
-
const predictions = openRecords.map((record) => predictionRecordToPrediction(record));
|
|
333
|
-
const historicalDetails = storedResolvedPredictionDetails(records);
|
|
334
|
-
if (records.length === 0) {
|
|
335
|
-
return {
|
|
336
|
-
content: [
|
|
337
|
-
{
|
|
338
|
-
type: "text",
|
|
339
|
-
text: "No predictions recorded yet. Use record action to track your calls.",
|
|
340
|
-
},
|
|
341
|
-
],
|
|
342
|
-
details: null,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
if (openRecords.length === 0) {
|
|
346
|
-
if (historicalDetails.length > 0) {
|
|
347
|
-
const result = predictionResultFromDetails(historicalDetails);
|
|
348
|
-
return {
|
|
349
|
-
content: [{ type: "text", text: formatPredictionScorecard(result) }],
|
|
350
|
-
details: result,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
return {
|
|
354
|
-
content: [{ type: "text", text: "No open predictions to check." }],
|
|
355
|
-
details: checkPredictions([], new Map()),
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const symbols = [...new Set(predictions.map((p) => p.symbol))];
|
|
360
|
-
const priceMap = new Map<string, number>();
|
|
361
|
-
await Promise.all(
|
|
362
|
-
symbols.map(async (sym) => {
|
|
363
|
-
const result = await wrapProvider("yahoo", () => getQuote(sym));
|
|
364
|
-
if (result.status === "ok" && !result.stale && !isZeroFilledQuote(result.data)) {
|
|
365
|
-
priceMap.set(sym, result.data.price);
|
|
366
|
-
}
|
|
367
|
-
}),
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
const currentResult = checkPredictions(predictions, priceMap);
|
|
371
|
-
persistPredictionOutcomes(service, openRecords, currentResult);
|
|
372
|
-
const result = predictionResultFromDetails([...historicalDetails, ...currentResult.details]);
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
content: [{ type: "text", text: formatPredictionScorecard(result) }],
|
|
376
|
-
details: result,
|
|
377
|
-
};
|
|
378
|
-
} finally {
|
|
379
|
-
db.close();
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
function persistPredictionOutcomes(
|
|
385
|
-
service: MarketStateService,
|
|
386
|
-
records: PredictionRecord[],
|
|
387
|
-
result: PredictionCheckResult,
|
|
388
|
-
): void {
|
|
389
|
-
const now = new Date().toISOString();
|
|
390
|
-
const usedRecordIds = new Set<number>();
|
|
391
|
-
|
|
392
|
-
for (const detail of result.details) {
|
|
393
|
-
if (detail.status !== "resolved") continue;
|
|
394
|
-
const record = findMatchingOpenRecord(records, detail, usedRecordIds);
|
|
395
|
-
if (record == null) continue;
|
|
396
|
-
usedRecordIds.add(record.id);
|
|
397
|
-
service.updatePredictionOutcome({
|
|
398
|
-
id: record.id,
|
|
399
|
-
status: "resolved",
|
|
400
|
-
resolvedAt: now,
|
|
401
|
-
result: {
|
|
402
|
-
currentPrice: detail.currentPrice,
|
|
403
|
-
pnlPercent: detail.pnlPercent,
|
|
404
|
-
correct: detail.correct,
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
for (const record of records) {
|
|
410
|
-
if (usedRecordIds.has(record.id)) continue;
|
|
411
|
-
if (record.expiresAt > now) continue;
|
|
412
|
-
if (
|
|
413
|
-
result.details.some(
|
|
414
|
-
(detail) => detail.status === "open" && matchesPredictionRecord(record, detail),
|
|
415
|
-
)
|
|
416
|
-
) {
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
service.updatePredictionOutcome({
|
|
420
|
-
id: record.id,
|
|
421
|
-
status: "expired",
|
|
422
|
-
resolvedAt: now,
|
|
423
|
-
result: { reason: "quote_unavailable" },
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function storedResolvedPredictionDetails(
|
|
429
|
-
records: PredictionRecord[],
|
|
430
|
-
): PredictionCheckResult["details"] {
|
|
431
|
-
return records.flatMap((record) => {
|
|
432
|
-
if (record.status !== "resolved" || record.resultJson == null) return [];
|
|
433
|
-
const result = JSON.parse(record.resultJson) as {
|
|
434
|
-
currentPrice?: unknown;
|
|
435
|
-
pnlPercent?: unknown;
|
|
436
|
-
correct?: unknown;
|
|
437
|
-
};
|
|
438
|
-
if (
|
|
439
|
-
typeof result.currentPrice !== "number" ||
|
|
440
|
-
typeof result.pnlPercent !== "number" ||
|
|
441
|
-
typeof result.correct !== "boolean"
|
|
442
|
-
) {
|
|
443
|
-
return [];
|
|
444
|
-
}
|
|
445
|
-
return [
|
|
446
|
-
{
|
|
447
|
-
symbol: record.symbol,
|
|
448
|
-
direction: record.direction,
|
|
449
|
-
conviction: record.conviction,
|
|
450
|
-
entryPrice: record.entryPrice,
|
|
451
|
-
currentPrice: result.currentPrice,
|
|
452
|
-
pnlPercent: result.pnlPercent,
|
|
453
|
-
correct: result.correct,
|
|
454
|
-
status: "resolved" as const,
|
|
455
|
-
},
|
|
456
|
-
];
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function predictionResultFromDetails(
|
|
461
|
-
details: PredictionCheckResult["details"],
|
|
462
|
-
): PredictionCheckResult {
|
|
463
|
-
const resolved = details.filter((detail) => detail.status === "resolved");
|
|
464
|
-
const correct = resolved.filter((detail) => detail.correct);
|
|
465
|
-
const totalConviction = resolved.reduce((sum, detail) => sum + detail.conviction, 0);
|
|
466
|
-
const correctConviction = correct.reduce((sum, detail) => sum + detail.conviction, 0);
|
|
467
|
-
|
|
468
|
-
return {
|
|
469
|
-
total: details.length,
|
|
470
|
-
open: details.filter((detail) => detail.status === "open").length,
|
|
471
|
-
correct: correct.length,
|
|
472
|
-
wrong: resolved.length - correct.length,
|
|
473
|
-
hitRate: resolved.length > 0 ? correct.length / resolved.length : 0,
|
|
474
|
-
weightedHitRate: totalConviction > 0 ? correctConviction / totalConviction : 0,
|
|
475
|
-
details,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function formatPredictionScorecard(result: PredictionCheckResult): string {
|
|
480
|
-
const resolved = result.correct + result.wrong;
|
|
481
|
-
const lines = [
|
|
482
|
-
`**Prediction Scorecard** — ${result.total} predictions (${resolved} resolved, ${result.open} open)`,
|
|
483
|
-
``,
|
|
484
|
-
`Hit Rate: ${(result.hitRate * 100).toFixed(0)}% (${result.correct}/${resolved})`,
|
|
485
|
-
`Weighted Hit Rate: ${(result.weightedHitRate * 100).toFixed(0)}% (by conviction)`,
|
|
486
|
-
``,
|
|
487
|
-
...result.details.map((d) => {
|
|
488
|
-
const icon = d.status === "open" ? "~" : d.correct ? "+" : "-";
|
|
489
|
-
if (d.currentPrice == null || d.pnlPercent == null) {
|
|
490
|
-
return `${icon} ${d.symbol} ${d.direction}: quote unavailable (open)`;
|
|
491
|
-
}
|
|
492
|
-
const sign = d.pnlPercent >= 0 ? "+" : "";
|
|
493
|
-
const label =
|
|
494
|
-
d.status === "open"
|
|
495
|
-
? d.targetHit && d.targetPrice != null
|
|
496
|
-
? ` (open — target hit: $${d.targetPrice.toFixed(2)} reached before expiry; resolve or let it ride)`
|
|
497
|
-
: " (open)"
|
|
498
|
-
: "";
|
|
499
|
-
return ` [${icon}] ${d.symbol}: ${d.direction} (conv ${d.conviction}) → $${d.entryPrice.toFixed(2)} → $${d.currentPrice.toFixed(2)} (${sign}${(d.pnlPercent * 100).toFixed(1)}%)${label}`;
|
|
500
|
-
}),
|
|
501
|
-
];
|
|
502
|
-
return lines.join("\n");
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function findMatchingOpenRecord(
|
|
506
|
-
records: PredictionRecord[],
|
|
507
|
-
detail: PredictionCheckResult["details"][number],
|
|
508
|
-
usedRecordIds: Set<number>,
|
|
509
|
-
): PredictionRecord | null {
|
|
510
|
-
return (
|
|
511
|
-
records.find(
|
|
512
|
-
(record) => !usedRecordIds.has(record.id) && matchesPredictionRecord(record, detail),
|
|
513
|
-
) ?? null
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function matchesPredictionRecord(
|
|
518
|
-
record: PredictionRecord,
|
|
519
|
-
detail: Pick<
|
|
520
|
-
PredictionCheckResult["details"][number],
|
|
521
|
-
"symbol" | "direction" | "conviction" | "entryPrice"
|
|
522
|
-
>,
|
|
523
|
-
): boolean {
|
|
524
|
-
return (
|
|
525
|
-
record.symbol === detail.symbol &&
|
|
526
|
-
record.direction === detail.direction &&
|
|
527
|
-
record.conviction === detail.conviction &&
|
|
528
|
-
record.entryPrice === detail.entryPrice
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function predictionRecordToPrediction(
|
|
533
|
-
record: PredictionRecord,
|
|
534
|
-
explicitTimeframeDays?: number,
|
|
535
|
-
): Prediction {
|
|
536
|
-
const openedAt = new Date(record.openedAt);
|
|
537
|
-
const expiresAt = new Date(record.expiresAt);
|
|
538
|
-
const timeframeDays =
|
|
539
|
-
explicitTimeframeDays ?? Math.round((expiresAt.getTime() - openedAt.getTime()) / 86_400_000);
|
|
540
|
-
|
|
541
|
-
return {
|
|
542
|
-
id: record.id,
|
|
543
|
-
instrumentId: record.instrumentId,
|
|
544
|
-
symbol: record.symbol,
|
|
545
|
-
direction: record.direction,
|
|
546
|
-
conviction: record.conviction,
|
|
547
|
-
entryPrice: record.entryPrice,
|
|
548
|
-
targetPrice: record.targetPrice ?? undefined,
|
|
549
|
-
date: record.openedAt.split("T")[0],
|
|
550
|
-
expiresAt: record.expiresAt.split("T")[0],
|
|
551
|
-
timeframeDays,
|
|
552
|
-
};
|
|
553
|
-
}
|