opencandle 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (527) hide show
  1. package/README.md +164 -187
  2. package/dist/analysts/contracts.d.ts +1 -3
  3. package/dist/analysts/contracts.js +1 -11
  4. package/dist/analysts/contracts.js.map +1 -1
  5. package/dist/analysts/orchestrator.d.ts +1 -3
  6. package/dist/analysts/orchestrator.js +1 -26
  7. package/dist/analysts/orchestrator.js.map +1 -1
  8. package/dist/cli.js +30 -7
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +3 -3
  11. package/dist/config.js +12 -5
  12. package/dist/config.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/infra/browser.js +3 -1
  17. package/dist/infra/browser.js.map +1 -1
  18. package/dist/infra/cache.d.ts +8 -11
  19. package/dist/infra/cache.js +17 -15
  20. package/dist/infra/cache.js.map +1 -1
  21. package/dist/infra/http-client.d.ts +4 -1
  22. package/dist/infra/http-client.js +59 -6
  23. package/dist/infra/http-client.js.map +1 -1
  24. package/dist/infra/index.d.ts +3 -3
  25. package/dist/infra/index.js +3 -3
  26. package/dist/infra/index.js.map +1 -1
  27. package/dist/infra/native-dependencies.js +2 -2
  28. package/dist/infra/native-dependencies.js.map +1 -1
  29. package/dist/infra/node-version.js.map +1 -1
  30. package/dist/infra/opencandle-paths.d.ts +0 -3
  31. package/dist/infra/opencandle-paths.js +4 -11
  32. package/dist/infra/opencandle-paths.js.map +1 -1
  33. package/dist/infra/rate-limiter.js +12 -9
  34. package/dist/infra/rate-limiter.js.map +1 -1
  35. package/dist/market-state/alert-conditions.d.ts +34 -0
  36. package/dist/market-state/alert-conditions.js +23 -0
  37. package/dist/market-state/alert-conditions.js.map +1 -0
  38. package/dist/market-state/alert-runner.d.ts +55 -0
  39. package/dist/market-state/alert-runner.js +634 -0
  40. package/dist/market-state/alert-runner.js.map +1 -0
  41. package/dist/market-state/daily-report.d.ts +26 -0
  42. package/dist/market-state/daily-report.js +179 -0
  43. package/dist/market-state/daily-report.js.map +1 -0
  44. package/dist/market-state/local-automation-service.d.ts +25 -0
  45. package/dist/market-state/local-automation-service.js +119 -0
  46. package/dist/market-state/local-automation-service.js.map +1 -0
  47. package/dist/market-state/notification-delivery.d.ts +14 -0
  48. package/dist/market-state/notification-delivery.js +139 -0
  49. package/dist/market-state/notification-delivery.js.map +1 -0
  50. package/dist/market-state/resolve-for-mutation.d.ts +10 -0
  51. package/dist/market-state/resolve-for-mutation.js +15 -0
  52. package/dist/market-state/resolve-for-mutation.js.map +1 -0
  53. package/dist/market-state/resolve.d.ts +14 -0
  54. package/dist/market-state/resolve.js +89 -0
  55. package/dist/market-state/resolve.js.map +1 -0
  56. package/dist/market-state/service.d.ts +527 -0
  57. package/dist/market-state/service.js +1099 -0
  58. package/dist/market-state/service.js.map +1 -0
  59. package/dist/memory/index.d.ts +7 -7
  60. package/dist/memory/index.js +6 -6
  61. package/dist/memory/index.js.map +1 -1
  62. package/dist/memory/manager.js +11 -11
  63. package/dist/memory/manager.js.map +1 -1
  64. package/dist/memory/retrieval.js +7 -4
  65. package/dist/memory/retrieval.js.map +1 -1
  66. package/dist/memory/sqlite.js +385 -3
  67. package/dist/memory/sqlite.js.map +1 -1
  68. package/dist/memory/storage.js +1 -2
  69. package/dist/memory/storage.js.map +1 -1
  70. package/dist/memory/tool-defaults.js +64 -28
  71. package/dist/memory/tool-defaults.js.map +1 -1
  72. package/dist/memory/types.js.map +1 -1
  73. package/dist/monitor.d.ts +2 -0
  74. package/dist/monitor.js +104 -0
  75. package/dist/monitor.js.map +1 -0
  76. package/dist/onboarding/connect.js +4 -6
  77. package/dist/onboarding/connect.js.map +1 -1
  78. package/dist/onboarding/credential-interceptor.js +1 -1
  79. package/dist/onboarding/credential-interceptor.js.map +1 -1
  80. package/dist/onboarding/degradation-accumulator.js +1 -3
  81. package/dist/onboarding/degradation-accumulator.js.map +1 -1
  82. package/dist/onboarding/providers.js +3 -16
  83. package/dist/onboarding/providers.js.map +1 -1
  84. package/dist/onboarding/state.js.map +1 -1
  85. package/dist/onboarding/tool-helpers.js +1 -1
  86. package/dist/onboarding/tool-helpers.js.map +1 -1
  87. package/dist/onboarding/tool-tags.js +6 -4
  88. package/dist/onboarding/tool-tags.js.map +1 -1
  89. package/dist/onboarding/validation.js +1 -1
  90. package/dist/onboarding/validation.js.map +1 -1
  91. package/dist/pi/opencandle-extension.d.ts +8 -0
  92. package/dist/pi/opencandle-extension.js +412 -28
  93. package/dist/pi/opencandle-extension.js.map +1 -1
  94. package/dist/pi/session.d.ts +1 -1
  95. package/dist/pi/session.js +3 -1
  96. package/dist/pi/session.js.map +1 -1
  97. package/dist/pi/setup.js +8 -3
  98. package/dist/pi/setup.js.map +1 -1
  99. package/dist/pi/tool-adapter.js +5 -2
  100. package/dist/pi/tool-adapter.js.map +1 -1
  101. package/dist/prompts/context-builder.d.ts +1 -1
  102. package/dist/prompts/context-builder.js +19 -6
  103. package/dist/prompts/context-builder.js.map +1 -1
  104. package/dist/prompts/policy-cards.d.ts +1 -1
  105. package/dist/prompts/policy-cards.js +1 -1
  106. package/dist/prompts/policy-cards.js.map +1 -1
  107. package/dist/prompts/sections.d.ts +1 -1
  108. package/dist/prompts/symbol-preflight.d.ts +20 -0
  109. package/dist/prompts/symbol-preflight.js +49 -0
  110. package/dist/prompts/symbol-preflight.js.map +1 -0
  111. package/dist/prompts/workflow-prompts.d.ts +1 -1
  112. package/dist/prompts/workflow-prompts.js +54 -16
  113. package/dist/prompts/workflow-prompts.js.map +1 -1
  114. package/dist/providers/alpha-vantage.d.ts +1 -1
  115. package/dist/providers/alpha-vantage.js +26 -7
  116. package/dist/providers/alpha-vantage.js.map +1 -1
  117. package/dist/providers/coingecko.js +1 -1
  118. package/dist/providers/coingecko.js.map +1 -1
  119. package/dist/providers/errors.d.ts +5 -0
  120. package/dist/providers/errors.js +11 -0
  121. package/dist/providers/errors.js.map +1 -0
  122. package/dist/providers/exa-search.d.ts +2 -2
  123. package/dist/providers/exa-search.js +19 -11
  124. package/dist/providers/exa-search.js.map +1 -1
  125. package/dist/providers/fear-greed.js +1 -1
  126. package/dist/providers/fear-greed.js.map +1 -1
  127. package/dist/providers/finnhub.js +3 -5
  128. package/dist/providers/finnhub.js.map +1 -1
  129. package/dist/providers/fred.js +2 -2
  130. package/dist/providers/fred.js.map +1 -1
  131. package/dist/providers/index.d.ts +7 -6
  132. package/dist/providers/index.js +6 -5
  133. package/dist/providers/index.js.map +1 -1
  134. package/dist/providers/reddit.js +2 -2
  135. package/dist/providers/reddit.js.map +1 -1
  136. package/dist/providers/sec-edgar.d.ts +1 -0
  137. package/dist/providers/sec-edgar.js +12 -4
  138. package/dist/providers/sec-edgar.js.map +1 -1
  139. package/dist/providers/tradingview.d.ts +47 -0
  140. package/dist/providers/tradingview.js +275 -0
  141. package/dist/providers/tradingview.js.map +1 -0
  142. package/dist/providers/twitter.js +6 -8
  143. package/dist/providers/twitter.js.map +1 -1
  144. package/dist/providers/web-search.js +26 -12
  145. package/dist/providers/web-search.js.map +1 -1
  146. package/dist/providers/with-fallback.js +4 -2
  147. package/dist/providers/with-fallback.js.map +1 -1
  148. package/dist/providers/wrap-provider.d.ts +2 -3
  149. package/dist/providers/wrap-provider.js +14 -8
  150. package/dist/providers/wrap-provider.js.map +1 -1
  151. package/dist/providers/yahoo-finance.d.ts +1 -1
  152. package/dist/providers/yahoo-finance.js +101 -17
  153. package/dist/providers/yahoo-finance.js.map +1 -1
  154. package/dist/routing/classify-intent.d.ts +6 -0
  155. package/dist/routing/classify-intent.js +78 -7
  156. package/dist/routing/classify-intent.js.map +1 -1
  157. package/dist/routing/defaults.d.ts +1 -1
  158. package/dist/routing/entity-extractor.d.ts +1 -0
  159. package/dist/routing/entity-extractor.js +234 -29
  160. package/dist/routing/entity-extractor.js.map +1 -1
  161. package/dist/routing/fund-symbols.d.ts +2 -0
  162. package/dist/routing/fund-symbols.js +55 -0
  163. package/dist/routing/fund-symbols.js.map +1 -0
  164. package/dist/routing/horizon.d.ts +1 -0
  165. package/dist/routing/horizon.js +10 -0
  166. package/dist/routing/horizon.js.map +1 -0
  167. package/dist/routing/index.d.ts +10 -10
  168. package/dist/routing/index.js +6 -6
  169. package/dist/routing/index.js.map +1 -1
  170. package/dist/routing/planning.d.ts +1 -1
  171. package/dist/routing/planning.js +65 -34
  172. package/dist/routing/planning.js.map +1 -1
  173. package/dist/routing/route-manifest.d.ts +2 -2
  174. package/dist/routing/route-manifest.js +25 -4
  175. package/dist/routing/route-manifest.js.map +1 -1
  176. package/dist/routing/router-llm-client.js.map +1 -1
  177. package/dist/routing/router-prompt.js +7 -9
  178. package/dist/routing/router-prompt.js.map +1 -1
  179. package/dist/routing/router-types.d.ts +1 -0
  180. package/dist/routing/router.js +137 -22
  181. package/dist/routing/router.js.map +1 -1
  182. package/dist/routing/slot-resolver.d.ts +1 -1
  183. package/dist/routing/slot-resolver.js +2 -4
  184. package/dist/routing/slot-resolver.js.map +1 -1
  185. package/dist/routing/symbol-disambiguator.d.ts +11 -0
  186. package/dist/routing/symbol-disambiguator.js +52 -0
  187. package/dist/routing/symbol-disambiguator.js.map +1 -0
  188. package/dist/routing/turn-context.d.ts +1 -1
  189. package/dist/routing/turn-context.js +1 -1
  190. package/dist/routing/turn-context.js.map +1 -1
  191. package/dist/routing/types.d.ts +2 -0
  192. package/dist/runtime/answer-contracts.js +36 -8
  193. package/dist/runtime/answer-contracts.js.map +1 -1
  194. package/dist/runtime/artifact-contracts.js.map +1 -1
  195. package/dist/runtime/planning-evidence.js +47 -26
  196. package/dist/runtime/planning-evidence.js.map +1 -1
  197. package/dist/runtime/prompt-step.d.ts +1 -9
  198. package/dist/runtime/prompt-step.js +0 -10
  199. package/dist/runtime/prompt-step.js.map +1 -1
  200. package/dist/runtime/run-context.d.ts +5 -2
  201. package/dist/runtime/run-context.js +8 -1
  202. package/dist/runtime/run-context.js.map +1 -1
  203. package/dist/runtime/session-coordinator.d.ts +13 -5
  204. package/dist/runtime/session-coordinator.js +160 -20
  205. package/dist/runtime/session-coordinator.js.map +1 -1
  206. package/dist/runtime/session-title.d.ts +14 -0
  207. package/dist/runtime/session-title.js +50 -0
  208. package/dist/runtime/session-title.js.map +1 -0
  209. package/dist/runtime/tool-defaults-wrapper.js +1 -3
  210. package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
  211. package/dist/runtime/validation.js.map +1 -1
  212. package/dist/runtime/workflow-events.js.map +1 -1
  213. package/dist/runtime/workflow-runner.d.ts +3 -3
  214. package/dist/runtime/workflow-runner.js +1 -1
  215. package/dist/runtime/workflow-runner.js.map +1 -1
  216. package/dist/sentiment/adapters/finnhub.d.ts +1 -1
  217. package/dist/sentiment/adapters/finnhub.js +6 -1
  218. package/dist/sentiment/adapters/finnhub.js.map +1 -1
  219. package/dist/sentiment/adapters/reddit.d.ts +2 -2
  220. package/dist/sentiment/adapters/twitter.d.ts +1 -1
  221. package/dist/sentiment/adapters/web.d.ts +1 -1
  222. package/dist/sentiment/index.d.ts +9 -11
  223. package/dist/sentiment/index.js +9 -20
  224. package/dist/sentiment/index.js.map +1 -1
  225. package/dist/sentiment/keywords.js +26 -4
  226. package/dist/sentiment/keywords.js.map +1 -1
  227. package/dist/sentiment/pipeline.d.ts +2 -2
  228. package/dist/sentiment/pipeline.js +1 -1
  229. package/dist/sentiment/pipeline.js.map +1 -1
  230. package/dist/sentiment/scorer.js +1 -1
  231. package/dist/sentiment/store.d.ts +1 -1
  232. package/dist/sentiment/store.js +1 -1
  233. package/dist/sentiment/store.js.map +1 -1
  234. package/dist/sentiment/trends.d.ts +1 -1
  235. package/dist/sentiment/trends.js.map +1 -1
  236. package/dist/sentiment/types.js.map +1 -1
  237. package/dist/system-prompt.js +3 -2
  238. package/dist/system-prompt.js.map +1 -1
  239. package/dist/tool-kit.d.ts +7 -7
  240. package/dist/tool-kit.js +4 -4
  241. package/dist/tool-kit.js.map +1 -1
  242. package/dist/tools/fundamentals/company-overview.js +11 -6
  243. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  244. package/dist/tools/fundamentals/comps.js +18 -9
  245. package/dist/tools/fundamentals/comps.js.map +1 -1
  246. package/dist/tools/fundamentals/dcf.js +23 -11
  247. package/dist/tools/fundamentals/dcf.js.map +1 -1
  248. package/dist/tools/fundamentals/earnings.js +8 -3
  249. package/dist/tools/fundamentals/earnings.js.map +1 -1
  250. package/dist/tools/fundamentals/financials.js +8 -3
  251. package/dist/tools/fundamentals/financials.js.map +1 -1
  252. package/dist/tools/fundamentals/sec-filings.js +21 -6
  253. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  254. package/dist/tools/index.d.ts +23 -19
  255. package/dist/tools/index.js +51 -39
  256. package/dist/tools/index.js.map +1 -1
  257. package/dist/tools/interaction/ask-user.js +15 -3
  258. package/dist/tools/interaction/ask-user.js.map +1 -1
  259. package/dist/tools/interaction/twitter-login.js +13 -3
  260. package/dist/tools/interaction/twitter-login.js.map +1 -1
  261. package/dist/tools/macro/fear-greed.js.map +1 -1
  262. package/dist/tools/macro/fred-data.d.ts +1 -1
  263. package/dist/tools/macro/fred-data.js +17 -6
  264. package/dist/tools/macro/fred-data.js.map +1 -1
  265. package/dist/tools/market/crypto-history.js +3 -1
  266. package/dist/tools/market/crypto-history.js.map +1 -1
  267. package/dist/tools/market/crypto-price.js +3 -1
  268. package/dist/tools/market/crypto-price.js.map +1 -1
  269. package/dist/tools/market/screen-stocks.d.ts +18 -0
  270. package/dist/tools/market/screen-stocks.js +252 -0
  271. package/dist/tools/market/screen-stocks.js.map +1 -0
  272. package/dist/tools/market/search-ticker.js +160 -8
  273. package/dist/tools/market/search-ticker.js.map +1 -1
  274. package/dist/tools/market/stock-history.d.ts +2 -2
  275. package/dist/tools/market/stock-history.js +26 -7
  276. package/dist/tools/market/stock-history.js.map +1 -1
  277. package/dist/tools/market/stock-quote.js +5 -3
  278. package/dist/tools/market/stock-quote.js.map +1 -1
  279. package/dist/tools/options/greeks.js +1 -1
  280. package/dist/tools/options/greeks.js.map +1 -1
  281. package/dist/tools/options/option-chain.js +19 -6
  282. package/dist/tools/options/option-chain.js.map +1 -1
  283. package/dist/tools/portfolio/alerts.d.ts +15 -0
  284. package/dist/tools/portfolio/alerts.js +357 -0
  285. package/dist/tools/portfolio/alerts.js.map +1 -0
  286. package/dist/tools/portfolio/correlation.d.ts +1 -1
  287. package/dist/tools/portfolio/correlation.js +33 -13
  288. package/dist/tools/portfolio/correlation.js.map +1 -1
  289. package/dist/tools/portfolio/daily-report.d.ts +8 -0
  290. package/dist/tools/portfolio/daily-report.js +83 -0
  291. package/dist/tools/portfolio/daily-report.js.map +1 -0
  292. package/dist/tools/portfolio/holdings-overlap.js +10 -3
  293. package/dist/tools/portfolio/holdings-overlap.js.map +1 -1
  294. package/dist/tools/portfolio/notifications.d.ts +7 -0
  295. package/dist/tools/portfolio/notifications.js +43 -0
  296. package/dist/tools/portfolio/notifications.js.map +1 -0
  297. package/dist/tools/portfolio/predictions.d.ts +12 -6
  298. package/dist/tools/portfolio/predictions.js +337 -87
  299. package/dist/tools/portfolio/predictions.js.map +1 -1
  300. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  301. package/dist/tools/portfolio/risk-analysis.js +45 -6
  302. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  303. package/dist/tools/portfolio/tracker.d.ts +4 -3
  304. package/dist/tools/portfolio/tracker.js +246 -101
  305. package/dist/tools/portfolio/tracker.js.map +1 -1
  306. package/dist/tools/portfolio/watchlist.d.ts +6 -4
  307. package/dist/tools/portfolio/watchlist.js +208 -108
  308. package/dist/tools/portfolio/watchlist.js.map +1 -1
  309. package/dist/tools/sentiment/reddit-sentiment.js +23 -10
  310. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  311. package/dist/tools/sentiment/sentiment-summary.js +15 -13
  312. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  313. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  314. package/dist/tools/sentiment/sentiment-trend.js +12 -2
  315. package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
  316. package/dist/tools/sentiment/twitter-sentiment.js +12 -5
  317. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  318. package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
  319. package/dist/tools/sentiment/untrusted-text.js +17 -0
  320. package/dist/tools/sentiment/untrusted-text.js.map +1 -0
  321. package/dist/tools/sentiment/web-search.js +9 -13
  322. package/dist/tools/sentiment/web-search.js.map +1 -1
  323. package/dist/tools/sentiment/web-sentiment.js +15 -3
  324. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  325. package/dist/tools/technical/backtest.d.ts +1 -1
  326. package/dist/tools/technical/backtest.js +27 -20
  327. package/dist/tools/technical/backtest.js.map +1 -1
  328. package/dist/tools/technical/indicators.js +23 -5
  329. package/dist/tools/technical/indicators.js.map +1 -1
  330. package/dist/types/index.d.ts +3 -3
  331. package/dist/types/index.js.map +1 -1
  332. package/dist/types/market.d.ts +1 -0
  333. package/dist/types/portfolio.d.ts +14 -4
  334. package/dist/workflows/compare-assets.d.ts +0 -3
  335. package/dist/workflows/compare-assets.js +20 -11
  336. package/dist/workflows/compare-assets.js.map +1 -1
  337. package/dist/workflows/index.d.ts +3 -4
  338. package/dist/workflows/index.js +3 -3
  339. package/dist/workflows/index.js.map +1 -1
  340. package/dist/workflows/options-screener.d.ts +0 -3
  341. package/dist/workflows/options-screener.js +4 -11
  342. package/dist/workflows/options-screener.js.map +1 -1
  343. package/dist/workflows/portfolio-builder.d.ts +0 -3
  344. package/dist/workflows/portfolio-builder.js +0 -8
  345. package/dist/workflows/portfolio-builder.js.map +1 -1
  346. package/gui/server/ask-user-bridge.ts +1 -1
  347. package/gui/server/automation-heartbeat.ts +97 -0
  348. package/gui/server/background-quotes.ts +97 -1
  349. package/gui/server/chat-event-adapter.ts +32 -10
  350. package/gui/server/chat-run-session.ts +16 -0
  351. package/gui/server/invoke-tool.ts +144 -1
  352. package/gui/server/live-chat-event-adapter.ts +21 -6
  353. package/gui/server/market-state-api.ts +315 -0
  354. package/gui/server/model-setup.ts +149 -2
  355. package/gui/server/private-api-access.ts +62 -0
  356. package/gui/server/projector.ts +12 -7
  357. package/gui/server/prompt-observation.ts +4 -7
  358. package/gui/server/quote-snapshot-store.ts +50 -0
  359. package/gui/server/server.ts +200 -451
  360. package/gui/server/session-actions.ts +186 -1
  361. package/gui/server/shutdown.ts +47 -0
  362. package/gui/server/tool-invoke-ack.ts +49 -0
  363. package/gui/server/tool-metadata.ts +23 -10
  364. package/gui/server/websocket.ts +13 -3
  365. package/gui/server/writer-lock.ts +6 -2
  366. package/gui/server/ws-hub.ts +292 -0
  367. package/gui/shared/chat-events.ts +16 -1
  368. package/gui/shared/event-reducer.ts +24 -6
  369. package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +1 -0
  370. package/gui/web/dist/assets/index-2KZtKBmu.css +1 -0
  371. package/gui/web/dist/assets/index-CveNgtDg.js +69 -0
  372. package/gui/web/dist/index.html +2 -2
  373. package/package.json +5 -1
  374. package/src/analysts/contracts.ts +10 -23
  375. package/src/analysts/orchestrator.ts +8 -43
  376. package/src/cli.ts +35 -12
  377. package/src/config.ts +17 -9
  378. package/src/index.ts +1 -1
  379. package/src/infra/browser.ts +3 -1
  380. package/src/infra/cache.ts +41 -30
  381. package/src/infra/http-client.ts +72 -6
  382. package/src/infra/index.ts +7 -10
  383. package/src/infra/native-dependencies.ts +8 -3
  384. package/src/infra/node-version.ts +3 -1
  385. package/src/infra/opencandle-paths.ts +3 -14
  386. package/src/infra/rate-limiter.ts +22 -19
  387. package/src/market-state/alert-conditions.ts +82 -0
  388. package/src/market-state/alert-runner.ts +863 -0
  389. package/src/market-state/daily-report.ts +247 -0
  390. package/src/market-state/local-automation-service.ts +162 -0
  391. package/src/market-state/notification-delivery.ts +158 -0
  392. package/src/market-state/resolve-for-mutation.ts +24 -0
  393. package/src/market-state/resolve.ts +112 -0
  394. package/src/market-state/service.ts +2344 -0
  395. package/src/memory/index.ts +7 -7
  396. package/src/memory/manager.ts +14 -16
  397. package/src/memory/retrieval.ts +8 -7
  398. package/src/memory/sqlite.ts +407 -6
  399. package/src/memory/storage.ts +5 -15
  400. package/src/memory/tool-defaults.ts +60 -39
  401. package/src/memory/types.ts +3 -3
  402. package/src/monitor.ts +121 -0
  403. package/src/onboarding/connect.ts +10 -33
  404. package/src/onboarding/credential-interceptor.ts +3 -15
  405. package/src/onboarding/degradation-accumulator.ts +1 -3
  406. package/src/onboarding/providers.ts +9 -40
  407. package/src/onboarding/state.ts +4 -15
  408. package/src/onboarding/tool-helpers.ts +2 -9
  409. package/src/onboarding/tool-tags.ts +6 -6
  410. package/src/onboarding/validation.ts +14 -20
  411. package/src/pi/opencandle-extension.ts +529 -85
  412. package/src/pi/session.ts +7 -5
  413. package/src/pi/setup.ts +61 -43
  414. package/src/pi/tool-adapter.ts +5 -2
  415. package/src/prompts/context-builder.ts +23 -12
  416. package/src/prompts/policy-cards.ts +2 -2
  417. package/src/prompts/sections.ts +1 -1
  418. package/src/prompts/symbol-preflight.ts +80 -0
  419. package/src/prompts/workflow-prompts.ts +77 -28
  420. package/src/providers/alpha-vantage.ts +58 -39
  421. package/src/providers/coingecko.ts +2 -5
  422. package/src/providers/errors.ts +9 -0
  423. package/src/providers/exa-search.ts +24 -22
  424. package/src/providers/fear-greed.ts +1 -1
  425. package/src/providers/finnhub.ts +7 -6
  426. package/src/providers/fred.ts +3 -3
  427. package/src/providers/index.ts +14 -6
  428. package/src/providers/reddit.ts +17 -6
  429. package/src/providers/sec-edgar.ts +20 -6
  430. package/src/providers/tradingview.ts +399 -0
  431. package/src/providers/twitter.ts +6 -8
  432. package/src/providers/web-search.ts +30 -20
  433. package/src/providers/with-fallback.ts +8 -7
  434. package/src/providers/wrap-provider.ts +15 -10
  435. package/src/providers/yahoo-finance.ts +140 -35
  436. package/src/routing/classify-intent.ts +101 -10
  437. package/src/routing/defaults.ts +1 -1
  438. package/src/routing/entity-extractor.ts +287 -38
  439. package/src/routing/fund-symbols.ts +58 -0
  440. package/src/routing/horizon.ts +7 -0
  441. package/src/routing/index.ts +48 -48
  442. package/src/routing/planning.ts +144 -53
  443. package/src/routing/route-manifest.ts +37 -15
  444. package/src/routing/router-llm-client.ts +4 -4
  445. package/src/routing/router-prompt.ts +15 -19
  446. package/src/routing/router-types.ts +2 -5
  447. package/src/routing/router.ts +251 -53
  448. package/src/routing/slot-resolver.ts +34 -11
  449. package/src/routing/symbol-disambiguator.ts +72 -0
  450. package/src/routing/turn-context.ts +6 -9
  451. package/src/routing/types.ts +2 -0
  452. package/src/runtime/answer-contracts.ts +82 -43
  453. package/src/runtime/artifact-contracts.ts +2 -1
  454. package/src/runtime/planning-evidence.ts +157 -66
  455. package/src/runtime/prompt-step.ts +1 -16
  456. package/src/runtime/run-context.ts +12 -2
  457. package/src/runtime/session-coordinator.ts +238 -63
  458. package/src/runtime/session-title.ts +60 -0
  459. package/src/runtime/tool-defaults-wrapper.ts +1 -3
  460. package/src/runtime/validation.ts +1 -4
  461. package/src/runtime/workflow-events.ts +7 -7
  462. package/src/runtime/workflow-runner.ts +5 -11
  463. package/src/sentiment/adapters/finnhub.ts +7 -2
  464. package/src/sentiment/adapters/reddit.ts +2 -2
  465. package/src/sentiment/adapters/twitter.ts +1 -1
  466. package/src/sentiment/adapters/web.ts +1 -1
  467. package/src/sentiment/index.ts +16 -26
  468. package/src/sentiment/keywords.ts +26 -4
  469. package/src/sentiment/pipeline.ts +15 -4
  470. package/src/sentiment/scorer.ts +1 -1
  471. package/src/sentiment/store.ts +2 -2
  472. package/src/sentiment/trends.ts +9 -3
  473. package/src/sentiment/types.ts +5 -4
  474. package/src/system-prompt.ts +3 -2
  475. package/src/tool-kit.ts +10 -9
  476. package/src/tools/fundamentals/company-overview.ts +19 -9
  477. package/src/tools/fundamentals/comps.ts +68 -55
  478. package/src/tools/fundamentals/dcf.ts +145 -95
  479. package/src/tools/fundamentals/earnings.ts +16 -6
  480. package/src/tools/fundamentals/financials.ts +16 -7
  481. package/src/tools/fundamentals/sec-filings.ts +37 -16
  482. package/src/tools/index.ts +51 -39
  483. package/src/tools/interaction/ask-user.ts +22 -10
  484. package/src/tools/interaction/twitter-login.ts +17 -5
  485. package/src/tools/macro/fear-greed.ts +1 -1
  486. package/src/tools/macro/fred-data.ts +58 -46
  487. package/src/tools/market/crypto-history.ts +8 -3
  488. package/src/tools/market/crypto-price.ts +6 -6
  489. package/src/tools/market/screen-stocks.ts +279 -0
  490. package/src/tools/market/search-ticker.ts +218 -17
  491. package/src/tools/market/stock-history.ts +37 -12
  492. package/src/tools/market/stock-quote.ts +10 -7
  493. package/src/tools/options/greeks.ts +5 -5
  494. package/src/tools/options/option-chain.ts +41 -17
  495. package/src/tools/portfolio/alerts.ts +457 -0
  496. package/src/tools/portfolio/correlation.ts +47 -20
  497. package/src/tools/portfolio/daily-report.ts +101 -0
  498. package/src/tools/portfolio/holdings-overlap.ts +31 -15
  499. package/src/tools/portfolio/notifications.ts +45 -0
  500. package/src/tools/portfolio/predictions.ts +406 -106
  501. package/src/tools/portfolio/risk-analysis.ts +46 -7
  502. package/src/tools/portfolio/tracker.ts +270 -109
  503. package/src/tools/portfolio/watchlist.ts +250 -121
  504. package/src/tools/sentiment/reddit-sentiment.ts +50 -24
  505. package/src/tools/sentiment/sentiment-summary.ts +62 -41
  506. package/src/tools/sentiment/sentiment-trend.ts +24 -7
  507. package/src/tools/sentiment/twitter-sentiment.ts +22 -15
  508. package/src/tools/sentiment/untrusted-text.ts +21 -0
  509. package/src/tools/sentiment/web-search.ts +21 -18
  510. package/src/tools/sentiment/web-sentiment.ts +26 -10
  511. package/src/tools/technical/backtest.ts +32 -22
  512. package/src/tools/technical/indicators.ts +39 -14
  513. package/src/types/index.ts +8 -3
  514. package/src/types/market.ts +1 -0
  515. package/src/types/portfolio.ts +14 -4
  516. package/src/types/sentiment.ts +2 -2
  517. package/src/workflows/compare-assets.ts +33 -21
  518. package/src/workflows/index.ts +3 -4
  519. package/src/workflows/options-screener.ts +27 -29
  520. package/src/workflows/portfolio-builder.ts +34 -27
  521. package/dist/workflows/types.d.ts +0 -4
  522. package/dist/workflows/types.js +0 -2
  523. package/dist/workflows/types.js.map +0 -1
  524. package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +0 -1
  525. package/gui/web/dist/assets/index-Bxt9QpLX.css +0 -1
  526. package/gui/web/dist/assets/index-CZ9DHZYy.js +0 -67
  527. package/src/workflows/types.ts +0 -4
@@ -1,11 +1,15 @@
1
- import { Type } from "@sinclair/typebox";
2
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
3
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
4
- import { getQuote } from "../../providers/yahoo-finance.js";
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";
5
7
  import { wrapProvider } from "../../providers/wrap-provider.js";
6
- import { ensureParentDir, getPredictionsPath } from "../../infra/opencandle-paths.js";
8
+ import { getQuote } from "../../providers/yahoo-finance.js";
7
9
 
8
10
  export interface Prediction {
11
+ id?: number;
12
+ instrumentId?: number;
9
13
  symbol: string;
10
14
  direction: "bullish" | "bearish" | "neutral";
11
15
  conviction: number; // 1-10
@@ -28,56 +32,40 @@ export interface PredictionCheckResult {
28
32
  direction: string;
29
33
  conviction: number;
30
34
  entryPrice: number;
31
- currentPrice: number;
32
- pnlPercent: number;
35
+ targetPrice?: number;
36
+ currentPrice: number | null;
37
+ pnlPercent: number | null;
33
38
  correct: boolean;
34
39
  status: "open" | "resolved";
40
+ targetHit?: boolean;
41
+ dataGap?: string;
35
42
  }>;
36
43
  }
37
44
 
38
- function loadPredictions(): Prediction[] {
39
- const predictionsPath = getPredictionsPath();
40
- if (!existsSync(predictionsPath)) return [];
41
- try {
42
- return JSON.parse(readFileSync(predictionsPath, "utf-8"));
43
- } catch {
44
- return [];
45
- }
46
- }
47
-
48
- function savePredictions(predictions: Prediction[]): void {
49
- const predictionsPath = getPredictionsPath();
50
- ensureParentDir(predictionsPath);
51
- writeFileSync(predictionsPath, JSON.stringify(predictions, null, 2));
52
- }
53
-
54
- export function recordPrediction(params: {
45
+ export async function recordPrediction(params: {
55
46
  symbol: string;
56
47
  direction: "bullish" | "bearish" | "neutral";
57
48
  conviction: number;
58
49
  entryPrice: number;
59
50
  targetPrice?: number;
60
51
  timeframeDays: number;
61
- }): Prediction {
62
- const predictions = loadPredictions();
63
- const now = new Date();
64
- const expires = new Date(now);
65
- expires.setDate(expires.getDate() + params.timeframeDays);
66
-
67
- const prediction: Prediction = {
68
- symbol: params.symbol.toUpperCase(),
69
- direction: params.direction,
70
- conviction: params.conviction,
71
- entryPrice: params.entryPrice,
72
- targetPrice: params.targetPrice,
73
- date: now.toISOString().split("T")[0],
74
- expiresAt: expires.toISOString().split("T")[0],
75
- timeframeDays: params.timeframeDays,
76
- };
77
-
78
- predictions.push(prediction);
79
- savePredictions(predictions);
80
- return prediction;
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
+ }
81
69
  }
82
70
 
83
71
  export function checkPredictions(
@@ -98,22 +86,59 @@ export function checkPredictions(
98
86
 
99
87
  for (const p of predictions) {
100
88
  const currentPrice = currentPrices.get(p.symbol);
101
- if (currentPrice == null) continue;
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
+ }
102
121
 
103
122
  const isExpired = p.expiresAt <= nowStr;
104
123
  const pnlPercent = (currentPrice - p.entryPrice) / p.entryPrice;
105
124
 
106
125
  if (!isExpired) {
107
126
  openCount++;
127
+ const targetHit =
128
+ p.targetPrice != null &&
129
+ ((p.direction === "bullish" && currentPrice >= p.targetPrice) ||
130
+ (p.direction === "bearish" && currentPrice <= p.targetPrice));
108
131
  details.push({
109
132
  symbol: p.symbol,
110
133
  direction: p.direction,
111
134
  conviction: p.conviction,
112
135
  entryPrice: p.entryPrice,
136
+ targetPrice: p.targetPrice,
113
137
  currentPrice,
114
138
  pnlPercent,
115
139
  correct: false,
116
140
  status: "open",
141
+ targetHit,
117
142
  });
118
143
  continue;
119
144
  }
@@ -153,28 +178,37 @@ export function checkPredictions(
153
178
  }
154
179
 
155
180
  const params = Type.Object({
156
- action: Type.Union(
157
- [Type.Literal("record"), Type.Literal("check")],
158
- { description: "record: save a new prediction. check: evaluate all predictions against current prices." },
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)" }),
159
187
  ),
160
188
  symbol: Type.Optional(Type.String({ description: "Ticker symbol (required for record)" })),
161
189
  direction: Type.Optional(
162
- Type.Union(
163
- [Type.Literal("bullish"), Type.Literal("bearish"), Type.Literal("neutral")],
164
- { description: "Predicted direction (required for record)" },
165
- ),
190
+ Type.Union([Type.Literal("bullish"), Type.Literal("bearish"), Type.Literal("neutral")], {
191
+ description: "Predicted direction (required for record)",
192
+ }),
166
193
  ),
167
194
  conviction: Type.Optional(
168
- Type.Number({ description: "Conviction 1-10 (required for record)" }),
195
+ Type.Integer({ minimum: 1, maximum: 10, description: "Conviction 1-10 (required for record)" }),
169
196
  ),
170
197
  entry_price: Type.Optional(
171
- Type.Number({ description: "Entry price at time of prediction (required for record)" }),
198
+ Type.Number({
199
+ exclusiveMinimum: 0,
200
+ description: "Entry price at time of prediction (required for record)",
201
+ }),
172
202
  ),
173
203
  target_price: Type.Optional(
174
- Type.Number({ description: "Optional target price" }),
204
+ Type.Number({ exclusiveMinimum: 0, description: "Optional target price" }),
175
205
  ),
176
206
  timeframe_days: Type.Optional(
177
- Type.Number({ description: "Timeframe in days for the prediction (default: 30)" }),
207
+ Type.Integer({
208
+ minimum: 1,
209
+ maximum: 3650,
210
+ description: "Timeframe in days for the prediction (default: 30)",
211
+ }),
178
212
  ),
179
213
  });
180
214
 
@@ -185,69 +219,335 @@ export const predictionsTool: AgentTool<typeof params> = {
185
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.",
186
220
  parameters: params,
187
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
+
188
262
  if (args.action === "record") {
189
- if (!args.symbol || !args.direction || !args.conviction || !args.entry_price) {
190
- throw new Error("symbol, direction, conviction, and entry_price are required for record action.");
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.");
191
284
  }
192
285
 
193
- const prediction = recordPrediction({
194
- symbol: args.symbol,
195
- direction: args.direction,
196
- conviction: args.conviction,
197
- entryPrice: args.entry_price,
198
- targetPrice: args.target_price,
199
- timeframeDays: args.timeframe_days ?? 30,
200
- });
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
+ }
201
315
 
202
316
  return {
203
- content: [{ type: "text", text: `Recorded: ${prediction.symbol} ${prediction.direction} (conviction ${prediction.conviction}/10) at $${prediction.entryPrice}. Expires ${prediction.expiresAt}.` }],
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
+ ],
204
323
  details: prediction,
205
324
  };
206
325
  }
207
326
 
208
- // Check action
209
- const predictions = loadPredictions();
210
- if (predictions.length === 0) {
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
+
211
374
  return {
212
- content: [{ type: "text", text: "No predictions recorded yet. Use record action to track your calls." }],
213
- details: null,
375
+ content: [{ type: "text", text: formatPredictionScorecard(result) }],
376
+ details: result,
214
377
  };
378
+ } finally {
379
+ db.close();
215
380
  }
381
+ },
382
+ };
216
383
 
217
- // Fetch current prices for all symbols
218
- const symbols = [...new Set(predictions.map((p) => p.symbol))];
219
- const priceMap = new Map<string, number>();
220
- await Promise.all(
221
- symbols.map(async (sym) => {
222
- const result = await wrapProvider("yahoo", () => getQuote(sym));
223
- if (result.status === "ok") {
224
- priceMap.set(sym, result.data.price);
225
- } else {
226
- // Skip symbols that are unavailable
227
- }
228
- }),
229
- );
230
-
231
- const result = checkPredictions(predictions, priceMap);
232
-
233
- const resolved = result.correct + result.wrong;
234
- const lines = [
235
- `**Prediction Scorecard** — ${result.total} predictions (${resolved} resolved, ${result.open} open)`,
236
- ``,
237
- `Hit Rate: ${(result.hitRate * 100).toFixed(0)}% (${result.correct}/${resolved})`,
238
- `Weighted Hit Rate: ${(result.weightedHitRate * 100).toFixed(0)}% (by conviction)`,
239
- ``,
240
- ...result.details.map((d) => {
241
- const icon = d.status === "open" ? "~" : d.correct ? "+" : "-";
242
- const sign = d.pnlPercent >= 0 ? "+" : "";
243
- const label = d.status === "open" ? " (open)" : "";
244
- return ` [${icon}] ${d.symbol}: ${d.direction} (conv ${d.conviction}) → $${d.entryPrice.toFixed(2)} → $${d.currentPrice.toFixed(2)} (${sign}${(d.pnlPercent * 100).toFixed(1)}%)${label}`;
245
- }),
246
- ];
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>();
247
391
 
248
- return {
249
- content: [{ type: "text", text: lines.join("\n") }],
250
- details: result,
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;
251
437
  };
252
- },
253
- };
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
+ }