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.
Files changed (223) hide show
  1. package/README.md +12 -9
  2. package/dist/analysts/orchestrator.d.ts +3 -4
  3. package/dist/analysts/orchestrator.js +34 -44
  4. package/dist/analysts/orchestrator.js.map +1 -1
  5. package/dist/cli-main.js +242 -9
  6. package/dist/cli-main.js.map +1 -1
  7. package/dist/config.d.ts +4 -9
  8. package/dist/config.js +7 -10
  9. package/dist/config.js.map +1 -1
  10. package/dist/doctor/report.js +1 -1
  11. package/dist/doctor/report.js.map +1 -1
  12. package/dist/infra/cache.d.ts +6 -0
  13. package/dist/infra/cache.js +16 -6
  14. package/dist/infra/cache.js.map +1 -1
  15. package/dist/infra/freshness.d.ts +21 -0
  16. package/dist/infra/freshness.js +119 -0
  17. package/dist/infra/freshness.js.map +1 -0
  18. package/dist/infra/market-calendar.d.ts +14 -0
  19. package/dist/infra/market-calendar.js +137 -0
  20. package/dist/infra/market-calendar.js.map +1 -0
  21. package/dist/infra/rate-limiter.js +1 -0
  22. package/dist/infra/rate-limiter.js.map +1 -1
  23. package/dist/market-state/daily-report.d.ts +1 -1
  24. package/dist/market-state/daily-report.js +5 -16
  25. package/dist/market-state/daily-report.js.map +1 -1
  26. package/dist/market-state/service.d.ts +0 -35
  27. package/dist/market-state/service.js +0 -63
  28. package/dist/market-state/service.js.map +1 -1
  29. package/dist/market-state/summaries.d.ts +8 -0
  30. package/dist/market-state/summaries.js +70 -0
  31. package/dist/market-state/summaries.js.map +1 -0
  32. package/dist/memory/sqlite.js +20 -19
  33. package/dist/memory/sqlite.js.map +1 -1
  34. package/dist/onboarding/providers.d.ts +25 -1
  35. package/dist/onboarding/providers.js +26 -0
  36. package/dist/onboarding/providers.js.map +1 -1
  37. package/dist/pi/opencandle-extension.js +57 -226
  38. package/dist/pi/opencandle-extension.js.map +1 -1
  39. package/dist/pi/session-action-dedupe.d.ts +6 -0
  40. package/dist/pi/session-action-dedupe.js +126 -0
  41. package/dist/pi/session-action-dedupe.js.map +1 -0
  42. package/dist/pi/session-writer-lock.d.ts +26 -2
  43. package/dist/pi/session-writer-lock.js +230 -18
  44. package/dist/pi/session-writer-lock.js.map +1 -1
  45. package/dist/pi/setup.js +5 -5
  46. package/dist/pi/setup.js.map +1 -1
  47. package/dist/pi/tui-session-coordinator.d.ts +15 -0
  48. package/dist/pi/tui-session-coordinator.js +283 -0
  49. package/dist/pi/tui-session-coordinator.js.map +1 -0
  50. package/dist/prompts/context-builder.js +1 -1
  51. package/dist/prompts/policy-cards.js +1 -1
  52. package/dist/prompts/policy-cards.js.map +1 -1
  53. package/dist/providers/alpha-vantage.js +6 -0
  54. package/dist/providers/alpha-vantage.js.map +1 -1
  55. package/dist/providers/coingecko.js +1 -0
  56. package/dist/providers/coingecko.js.map +1 -1
  57. package/dist/providers/polymarket.d.ts +2 -0
  58. package/dist/providers/polymarket.js +163 -0
  59. package/dist/providers/polymarket.js.map +1 -0
  60. package/dist/providers/wrap-provider.js +5 -3
  61. package/dist/providers/wrap-provider.js.map +1 -1
  62. package/dist/providers/yahoo-finance.d.ts +3 -0
  63. package/dist/providers/yahoo-finance.js +206 -10
  64. package/dist/providers/yahoo-finance.js.map +1 -1
  65. package/dist/routing/classify-intent.js +4 -5
  66. package/dist/routing/classify-intent.js.map +1 -1
  67. package/dist/routing/entity-extractor.js +16 -0
  68. package/dist/routing/entity-extractor.js.map +1 -1
  69. package/dist/routing/route-manifest.js +1 -2
  70. package/dist/routing/route-manifest.js.map +1 -1
  71. package/dist/routing/router-llm-client.js +18 -3
  72. package/dist/routing/router-llm-client.js.map +1 -1
  73. package/dist/routing/router-prompt.js +1 -1
  74. package/dist/routing/router.d.ts +1 -1
  75. package/dist/routing/router.js +309 -9
  76. package/dist/routing/router.js.map +1 -1
  77. package/dist/routing/slot-resolver.d.ts +1 -0
  78. package/dist/routing/slot-resolver.js +1 -1
  79. package/dist/routing/slot-resolver.js.map +1 -1
  80. package/dist/runtime/evidence.d.ts +1 -0
  81. package/dist/runtime/evidence.js.map +1 -1
  82. package/dist/runtime/numeric-claims.d.ts +23 -0
  83. package/dist/runtime/numeric-claims.js +100 -0
  84. package/dist/runtime/numeric-claims.js.map +1 -0
  85. package/dist/runtime/planning-evidence.js +1 -76
  86. package/dist/runtime/planning-evidence.js.map +1 -1
  87. package/dist/runtime/prompt-step.d.ts +10 -1
  88. package/dist/runtime/prompt-step.js +116 -2
  89. package/dist/runtime/prompt-step.js.map +1 -1
  90. package/dist/runtime/session-coordinator.d.ts +11 -1
  91. package/dist/runtime/session-coordinator.js +354 -62
  92. package/dist/runtime/session-coordinator.js.map +1 -1
  93. package/dist/runtime/validation.d.ts +3 -1
  94. package/dist/runtime/workflow-events.d.ts +1 -1
  95. package/dist/runtime/workflow-events.js.map +1 -1
  96. package/dist/runtime/workflow-types.d.ts +3 -0
  97. package/dist/runtime/workflow-types.js.map +1 -1
  98. package/dist/system-prompt.js +1 -1
  99. package/dist/tools/fundamentals/comps.d.ts +1 -0
  100. package/dist/tools/fundamentals/comps.js +20 -9
  101. package/dist/tools/fundamentals/comps.js.map +1 -1
  102. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  103. package/dist/tools/fundamentals/dcf.js +190 -76
  104. package/dist/tools/fundamentals/dcf.js.map +1 -1
  105. package/dist/tools/index.d.ts +1 -1
  106. package/dist/tools/index.js +3 -3
  107. package/dist/tools/index.js.map +1 -1
  108. package/dist/tools/macro/event-probabilities.d.ts +8 -0
  109. package/dist/tools/macro/event-probabilities.js +121 -0
  110. package/dist/tools/macro/event-probabilities.js.map +1 -0
  111. package/dist/tools/market/crypto-price.d.ts +4 -1
  112. package/dist/tools/market/crypto-price.js +10 -1
  113. package/dist/tools/market/crypto-price.js.map +1 -1
  114. package/dist/tools/market/screen-stocks.js +14 -3
  115. package/dist/tools/market/screen-stocks.js.map +1 -1
  116. package/dist/tools/market/stock-quote.d.ts +4 -1
  117. package/dist/tools/market/stock-quote.js +9 -4
  118. package/dist/tools/market/stock-quote.js.map +1 -1
  119. package/dist/tools/options/option-chain.d.ts +4 -1
  120. package/dist/tools/options/option-chain.js +12 -1
  121. package/dist/tools/options/option-chain.js.map +1 -1
  122. package/dist/tools/portfolio/daily-report.js +10 -4
  123. package/dist/tools/portfolio/daily-report.js.map +1 -1
  124. package/dist/tools/portfolio/risk-analysis.js +1 -1
  125. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  126. package/dist/tools/technical/backtest.d.ts +23 -5
  127. package/dist/tools/technical/backtest.js +131 -94
  128. package/dist/tools/technical/backtest.js.map +1 -1
  129. package/dist/tools/technical/indicators.js +1 -1
  130. package/dist/tools/technical/indicators.js.map +1 -1
  131. package/dist/types/fundamentals.d.ts +1 -0
  132. package/dist/types/index.d.ts +1 -0
  133. package/dist/types/market.d.ts +2 -0
  134. package/dist/types/options.d.ts +1 -0
  135. package/dist/types/prediction-markets.d.ts +13 -0
  136. package/dist/types/prediction-markets.js +2 -0
  137. package/dist/types/prediction-markets.js.map +1 -0
  138. package/gui/server/chat-event-adapter.ts +89 -5
  139. package/gui/server/http-routes.ts +661 -37
  140. package/gui/server/invoke-tool.ts +165 -16
  141. package/gui/server/live-chat-event-adapter.ts +37 -2
  142. package/gui/server/local-session-coordinator.ts +97 -0
  143. package/gui/server/market-state-api.ts +64 -48
  144. package/gui/server/projector.ts +132 -3
  145. package/gui/server/server.ts +51 -6
  146. package/gui/server/session-actions.ts +146 -45
  147. package/gui/server/session-entry-wait.ts +22 -3
  148. package/gui/server/tool-metadata.ts +3 -1
  149. package/gui/server/ws-hub.ts +77 -10
  150. package/gui/shared/chat-events.ts +10 -1
  151. package/gui/shared/event-reducer.ts +1 -0
  152. package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
  153. package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
  154. package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
  155. package/gui/web/dist/index.html +2 -2
  156. package/package.json +13 -15
  157. package/src/analysts/orchestrator.ts +35 -56
  158. package/src/cli-main.ts +253 -13
  159. package/src/config.ts +12 -20
  160. package/src/doctor/report.ts +1 -1
  161. package/src/infra/cache.ts +24 -11
  162. package/src/infra/freshness.ts +165 -0
  163. package/src/infra/market-calendar.ts +193 -0
  164. package/src/infra/rate-limiter.ts +1 -0
  165. package/src/market-state/daily-report.ts +6 -16
  166. package/src/market-state/service.ts +0 -136
  167. package/src/market-state/summaries.ts +75 -0
  168. package/src/memory/sqlite.ts +23 -19
  169. package/src/onboarding/providers.ts +29 -1
  170. package/src/pi/opencandle-extension.ts +67 -270
  171. package/src/pi/session-action-dedupe.ts +155 -0
  172. package/src/pi/session-writer-lock.ts +290 -20
  173. package/src/pi/setup.ts +6 -6
  174. package/src/pi/tui-session-coordinator.ts +351 -0
  175. package/src/prompts/context-builder.ts +1 -1
  176. package/src/prompts/policy-cards.ts +1 -1
  177. package/src/providers/alpha-vantage.ts +6 -0
  178. package/src/providers/coingecko.ts +2 -0
  179. package/src/providers/polymarket.ts +214 -0
  180. package/src/providers/wrap-provider.ts +5 -3
  181. package/src/providers/yahoo-finance.ts +339 -27
  182. package/src/routing/classify-intent.ts +4 -5
  183. package/src/routing/entity-extractor.ts +17 -0
  184. package/src/routing/route-manifest.ts +1 -2
  185. package/src/routing/router-llm-client.ts +31 -19
  186. package/src/routing/router-prompt.ts +1 -1
  187. package/src/routing/router.ts +379 -8
  188. package/src/routing/slot-resolver.ts +1 -1
  189. package/src/runtime/evidence.ts +1 -0
  190. package/src/runtime/numeric-claims.ts +108 -0
  191. package/src/runtime/planning-evidence.ts +6 -91
  192. package/src/runtime/prompt-step.ts +127 -2
  193. package/src/runtime/session-coordinator.ts +464 -74
  194. package/src/runtime/validation.ts +1 -1
  195. package/src/runtime/workflow-events.ts +2 -0
  196. package/src/runtime/workflow-types.ts +3 -0
  197. package/src/system-prompt.ts +1 -1
  198. package/src/tools/AGENTS.md +1 -1
  199. package/src/tools/fundamentals/comps.ts +25 -14
  200. package/src/tools/fundamentals/dcf.ts +217 -87
  201. package/src/tools/index.ts +3 -3
  202. package/src/tools/macro/event-probabilities.ts +141 -0
  203. package/src/tools/market/crypto-price.ts +14 -2
  204. package/src/tools/market/screen-stocks.ts +14 -3
  205. package/src/tools/market/stock-quote.ts +49 -43
  206. package/src/tools/options/option-chain.ts +16 -2
  207. package/src/tools/portfolio/daily-report.ts +10 -4
  208. package/src/tools/portfolio/risk-analysis.ts +1 -1
  209. package/src/tools/technical/backtest.ts +167 -108
  210. package/src/tools/technical/indicators.ts +1 -1
  211. package/src/types/fundamentals.ts +1 -0
  212. package/src/types/index.ts +1 -0
  213. package/src/types/market.ts +2 -0
  214. package/src/types/options.ts +1 -0
  215. package/src/types/prediction-markets.ts +13 -0
  216. package/dist/tools/portfolio/predictions.d.ts +0 -55
  217. package/dist/tools/portfolio/predictions.js +0 -422
  218. package/dist/tools/portfolio/predictions.js.map +0 -1
  219. package/gui/server/chat-run-session.ts +0 -16
  220. package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
  221. package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
  222. package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
  223. package/src/tools/portfolio/predictions.ts +0 -553
@@ -12,21 +12,27 @@ export interface StaleResult<T> {
12
12
  cachedAt: number;
13
13
  }
14
14
 
15
- interface StaleMetadata {
16
- stale: boolean;
15
+ interface CacheMetadata {
16
+ status: "none" | "cached" | "stale";
17
17
  cachedAt: number;
18
18
  }
19
19
 
20
- const staleMetadataStorage = new AsyncLocalStorage<StaleMetadata>();
20
+ const cacheMetadataStorage = new AsyncLocalStorage<CacheMetadata>();
21
21
 
22
- export async function runWithStaleMetadata<T>(
23
- fn: () => Promise<T>,
24
- ): Promise<{ value: T; stale?: { cachedAt: number } }> {
25
- const metadata: StaleMetadata = { stale: false, cachedAt: 0 };
26
- const value = await staleMetadataStorage.run(metadata, fn);
22
+ export async function runWithStaleMetadata<T>(fn: () => Promise<T>): Promise<{
23
+ value: T;
24
+ cache?: { status: "cached" | "stale"; cachedAt: number };
25
+ stale?: { cachedAt: number };
26
+ }> {
27
+ const metadata: CacheMetadata = { status: "none", cachedAt: 0 };
28
+ const value = await cacheMetadataStorage.run(metadata, fn);
27
29
  return {
28
30
  value,
29
- stale: metadata.stale ? { cachedAt: metadata.cachedAt } : undefined,
31
+ cache:
32
+ metadata.status === "cached" || metadata.status === "stale"
33
+ ? { status: metadata.status, cachedAt: metadata.cachedAt }
34
+ : undefined,
35
+ stale: metadata.status === "stale" ? { cachedAt: metadata.cachedAt } : undefined,
30
36
  };
31
37
  }
32
38
 
@@ -37,6 +43,11 @@ export class Cache {
37
43
  const entry = this.store.get(key);
38
44
  if (!entry) return undefined;
39
45
  if (Date.now() > entry.expiresAt) return undefined;
46
+ const metadata = cacheMetadataStorage.getStore();
47
+ if (metadata && metadata.status === "none") {
48
+ metadata.status = "cached";
49
+ metadata.cachedAt = entry.cachedAt;
50
+ }
40
51
  return entry.value as T;
41
52
  }
42
53
 
@@ -54,9 +65,9 @@ export class Cache {
54
65
  return undefined;
55
66
  }
56
67
 
57
- const metadata = staleMetadataStorage.getStore();
68
+ const metadata = cacheMetadataStorage.getStore();
58
69
  if (metadata) {
59
- metadata.stale = true;
70
+ metadata.status = "stale";
60
71
  metadata.cachedAt = entry.cachedAt;
61
72
  }
62
73
  return { value: entry.value as T, stale: true, cachedAt: entry.cachedAt };
@@ -98,6 +109,7 @@ export const TTL = {
98
109
  CRUMB: 900_000, // 15 minutes
99
110
  WEB_SEARCH: 300_000, // 5 minutes
100
111
  FINNHUB_NEWS: 300_000, // 5 minutes
112
+ PREDICTION_MARKETS: 300_000, // 5 minutes
101
113
  } as const;
102
114
 
103
115
  // Stale limits — how long past TTL expiry a cached value is still useful as fallback
@@ -111,4 +123,5 @@ export const STALE_LIMIT = {
111
123
  SCREENER: 15 * 60_000, // 15 minutes
112
124
  WEB_SEARCH: 3_600_000, // 1 hour
113
125
  FINNHUB_NEWS: 3_600_000, // 1 hour
126
+ PREDICTION_MARKETS: 3_600_000, // 1 hour
114
127
  } as const;
@@ -0,0 +1,165 @@
1
+ import {
2
+ classifyMarketStatusAt,
3
+ lastTradingDay,
4
+ localDateTimeParts,
5
+ type MarketSession,
6
+ } from "./market-calendar.js";
7
+
8
+ export interface FreshnessStamp {
9
+ fetchedAt: string;
10
+ providerDataAt?: string;
11
+ providerDataDate?: string;
12
+ cacheStatus: "live" | "cached" | "stale";
13
+ cachedAt?: string;
14
+ marketSession: MarketSession;
15
+ dataDelayMs?: number;
16
+ isStaleForSession: boolean;
17
+ }
18
+
19
+ export function buildFreshnessStamp(input: {
20
+ asOf?: string | number | Date;
21
+ cached?: boolean;
22
+ stale?: boolean;
23
+ cachedAt?: string;
24
+ dataDelayMs?: number;
25
+ now?: Date;
26
+ assetClass?: "equity" | "crypto";
27
+ }): FreshnessStamp {
28
+ const now = input.now ?? new Date();
29
+ const parsedAsOf = normalizeAsOf(input.asOf);
30
+ const fetchedAt = now.toISOString();
31
+ const cacheStatus = input.stale ? "stale" : input.cached ? "cached" : "live";
32
+ const marketSession = input.assetClass === "crypto" ? "unknown" : classifyMarketStatusAt(now);
33
+ const cachedAt = input.cachedAt ? normalizeDate(input.cachedAt)?.toISOString() : undefined;
34
+ const providerDataAt = parsedAsOf?.date.toISOString();
35
+ const providerDataDate = parsedAsOf?.dateOnly;
36
+ const isStaleForSession =
37
+ input.assetClass === "crypto"
38
+ ? isCryptoStale(parsedAsOf?.date, cachedAt, now, cacheStatus)
39
+ : isEquityStale(parsedAsOf, cacheStatus, now);
40
+
41
+ return {
42
+ fetchedAt,
43
+ providerDataAt,
44
+ providerDataDate,
45
+ cacheStatus,
46
+ cachedAt,
47
+ marketSession,
48
+ dataDelayMs: input.dataDelayMs,
49
+ isStaleForSession,
50
+ };
51
+ }
52
+
53
+ export function formatAsOfLine(stamp: FreshnessStamp): string {
54
+ if (stamp.isStaleForSession && stamp.providerDataAt) {
55
+ const date = stamp.providerDataDate ?? formatEtDate(new Date(stamp.providerDataAt));
56
+ const parenthetical = staleSessionParenthetical(stamp.marketSession);
57
+ const cached = stamp.cachedAt
58
+ ? ` Cached from ${formatEtDateTime(new Date(stamp.cachedAt))} ET.`
59
+ : "";
60
+ return `Last available price as of ${date}${parenthetical}. This is not a live quote.${cached}`;
61
+ }
62
+
63
+ if (stamp.cacheStatus === "stale" && stamp.cachedAt) {
64
+ const notLive = stamp.isStaleForSession ? " This is not a live quote." : "";
65
+ return `Using cached data from ${formatEtDateTime(new Date(stamp.cachedAt))} ET (provider unavailable).${notLive}`;
66
+ }
67
+
68
+ if (stamp.providerDataAt || stamp.dataDelayMs !== undefined) {
69
+ const reference = stamp.providerDataAt ?? stamp.cachedAt ?? stamp.fetchedAt;
70
+ const asOf = stamp.providerDataDate ?? formatEtDateTime(new Date(reference));
71
+ if (stamp.dataDelayMs !== undefined)
72
+ return `As of ${asOf} ET (~${Math.round(stamp.dataDelayMs / 60_000)}m delayed).`;
73
+ return `As of ${asOf} ET (${marketSessionLabel(stamp.marketSession)}).`;
74
+ }
75
+
76
+ return `As of ${formatEtDateTime(new Date(stamp.fetchedAt))} ET (${marketSessionLabel(stamp.marketSession)}).`;
77
+ }
78
+
79
+ function normalizeAsOf(
80
+ asOf: string | number | Date | undefined,
81
+ ): { date: Date; dateOnly?: string } | undefined {
82
+ if (asOf === undefined) return undefined;
83
+ if (asOf instanceof Date) return Number.isNaN(asOf.getTime()) ? undefined : { date: asOf };
84
+ if (typeof asOf === "number") {
85
+ const milliseconds = asOf < 10_000_000_000 ? asOf * 1000 : asOf;
86
+ const date = new Date(milliseconds);
87
+ return Number.isNaN(date.getTime()) ? undefined : { date };
88
+ }
89
+ if (/^\d{4}-\d{2}-\d{2}$/.test(asOf)) {
90
+ const date = new Date(`${asOf}T00:00:00.000Z`);
91
+ return Number.isNaN(date.getTime()) ? undefined : { date, dateOnly: asOf };
92
+ }
93
+ const date = new Date(asOf);
94
+ return Number.isNaN(date.getTime()) ? undefined : { date };
95
+ }
96
+
97
+ function normalizeDate(value: string): Date | undefined {
98
+ const date = new Date(value);
99
+ return Number.isNaN(date.getTime()) ? undefined : date;
100
+ }
101
+
102
+ function isEquityStale(
103
+ parsedAsOf: { date: Date; dateOnly?: string } | undefined,
104
+ cacheStatus: FreshnessStamp["cacheStatus"],
105
+ now: Date,
106
+ ): boolean {
107
+ if (!parsedAsOf) return cacheStatus === "stale";
108
+ const localNow = localDateTimeParts(now, "America/New_York");
109
+ const providerTradingDate =
110
+ parsedAsOf.dateOnly ?? localDateTimeParts(parsedAsOf.date, "America/New_York").date;
111
+ const marketSession = classifyMarketStatusAt(now);
112
+ const requiredTradingDate =
113
+ marketSession === "open" ||
114
+ marketSession === "after_close" ||
115
+ marketSession === "closed_after_hours"
116
+ ? localNow.date
117
+ : lastTradingDay(localNow.date);
118
+ return providerTradingDate < requiredTradingDate;
119
+ }
120
+
121
+ function isCryptoStale(
122
+ providerDate: Date | undefined,
123
+ cachedAt: string | undefined,
124
+ now: Date,
125
+ cacheStatus: FreshnessStamp["cacheStatus"],
126
+ ): boolean {
127
+ const reference =
128
+ providerDate ?? (cacheStatus === "stale" && cachedAt ? new Date(cachedAt) : undefined);
129
+ if (!reference) return cacheStatus === "stale";
130
+ return now.getTime() - reference.getTime() > 15 * 60_000;
131
+ }
132
+
133
+ function formatEtDate(date: Date): string {
134
+ return localDateTimeParts(date, "America/New_York").date;
135
+ }
136
+
137
+ function formatEtDateTime(date: Date): string {
138
+ const parts = localDateTimeParts(date, "America/New_York");
139
+ return `${parts.date} ${parts.time}`;
140
+ }
141
+
142
+ function staleSessionParenthetical(marketSession: MarketSession): string {
143
+ if (marketSession === "closed_weekend") return " (market closed — weekend)";
144
+ if (marketSession === "closed_holiday") return " (market closed — holiday)";
145
+ return "";
146
+ }
147
+
148
+ function marketSessionLabel(marketSession: MarketSession): string {
149
+ switch (marketSession) {
150
+ case "open":
151
+ return "market open";
152
+ case "pre_market":
153
+ return "pre-market";
154
+ case "closed_weekend":
155
+ return "market closed — weekend";
156
+ case "closed_holiday":
157
+ return "market closed — holiday";
158
+ case "after_close":
159
+ return "after close";
160
+ case "closed_after_hours":
161
+ return "market closed";
162
+ case "unknown":
163
+ return "market session unknown";
164
+ }
165
+ }
@@ -0,0 +1,193 @@
1
+ export type MarketSession =
2
+ | "closed_weekend"
3
+ | "closed_holiday"
4
+ | "pre_market"
5
+ | "open"
6
+ | "after_close"
7
+ | "closed_after_hours"
8
+ | "unknown";
9
+
10
+ export interface LocalDateTimeParts {
11
+ date: string;
12
+ time: string;
13
+ weekday: string;
14
+ minutesSinceMidnight: number;
15
+ }
16
+
17
+ export const KNOWN_US_MARKET_HOLIDAYS: Record<string, string> = new Proxy(
18
+ {} as Record<string, string>,
19
+ {
20
+ get(_target, property) {
21
+ return typeof property === "string" ? marketHolidayName(property) : undefined;
22
+ },
23
+ has(_target, property) {
24
+ return typeof property === "string" && marketHolidayName(property) !== undefined;
25
+ },
26
+ },
27
+ );
28
+
29
+ export function marketHolidayName(key: string): string | undefined {
30
+ const date = dateFromKey(key);
31
+ const year = date.getUTCFullYear();
32
+ for (const candidateYear of [year - 1, year, year + 1]) {
33
+ const holiday = marketHolidaysForYear(candidateYear).find((entry) => entry.date === key);
34
+ if (holiday) return holiday.name;
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ export function classifyMarketStatus(
40
+ local: LocalDateTimeParts,
41
+ isWeekend: boolean,
42
+ isMarketHoliday: boolean,
43
+ temporalReferences: string[],
44
+ ): Exclude<MarketSession, "unknown"> {
45
+ if (isWeekend) return "closed_weekend";
46
+ if (isMarketHoliday) return "closed_holiday";
47
+ if (local.minutesSinceMidnight < 9 * 60 + 30) return "pre_market";
48
+ if (local.minutesSinceMidnight < 16 * 60) return "open";
49
+ if (temporalReferences.includes("after_close")) return "after_close";
50
+ return "closed_after_hours";
51
+ }
52
+
53
+ export function classifyMarketStatusAt(now: Date): MarketSession {
54
+ const local = localDateTimeParts(now, "America/New_York");
55
+ return classifyMarketStatus(
56
+ local,
57
+ local.weekday === "Sat" || local.weekday === "Sun",
58
+ marketHolidayName(local.date) !== undefined,
59
+ [],
60
+ );
61
+ }
62
+
63
+ export function localDateTimeParts(date: Date, timezone: string): LocalDateTimeParts {
64
+ const formatter = new Intl.DateTimeFormat("en-US", {
65
+ timeZone: timezone,
66
+ year: "numeric",
67
+ month: "2-digit",
68
+ day: "2-digit",
69
+ hour: "2-digit",
70
+ minute: "2-digit",
71
+ hour12: false,
72
+ weekday: "short",
73
+ });
74
+ const parts = formatter.formatToParts(date).reduce<Record<string, string>>((acc, part) => {
75
+ acc[part.type] = part.value;
76
+ return acc;
77
+ }, {});
78
+ const hour = Number(parts.hour);
79
+ const minute = Number(parts.minute);
80
+ return {
81
+ date: `${parts.year}-${parts.month}-${parts.day}`,
82
+ time: `${parts.hour}:${parts.minute}`,
83
+ weekday: parts.weekday ?? "",
84
+ minutesSinceMidnight: hour * 60 + minute,
85
+ };
86
+ }
87
+
88
+ export function lastTradingDay(localDate: string): string {
89
+ let cursor = dateFromKey(localDate);
90
+ do {
91
+ cursor = addDays(cursor, -1);
92
+ } while (isWeekendOrKnownHoliday(dateKey(cursor)));
93
+ return dateKey(cursor);
94
+ }
95
+
96
+ export function isWeekendOrKnownHoliday(key: string): boolean {
97
+ const date = dateFromKey(key);
98
+ const day = date.getUTCDay();
99
+ return day === 0 || day === 6 || marketHolidayName(key) !== undefined;
100
+ }
101
+
102
+ function marketHolidaysForYear(year: number): Array<{ date: string; name: string }> {
103
+ return [
104
+ observedFixedHoliday(year, 1, 1, "New Year's Day"),
105
+ nthWeekdayHoliday(year, 1, 1, 3, "Martin Luther King Jr. Day"),
106
+ nthWeekdayHoliday(year, 2, 1, 3, "Washington's Birthday"),
107
+ { date: dateKey(addDays(easterDate(year), -2)), name: "Good Friday" },
108
+ lastWeekdayHoliday(year, 5, 1, "Memorial Day"),
109
+ observedFixedHoliday(year, 6, 19, "Juneteenth"),
110
+ observedFixedHoliday(year, 7, 4, "Independence Day"),
111
+ nthWeekdayHoliday(year, 9, 1, 1, "Labor Day"),
112
+ nthWeekdayHoliday(year, 11, 4, 4, "Thanksgiving Day"),
113
+ observedFixedHoliday(year, 12, 25, "Christmas Day"),
114
+ ];
115
+ }
116
+
117
+ function observedFixedHoliday(
118
+ year: number,
119
+ month: number,
120
+ day: number,
121
+ name: string,
122
+ ): {
123
+ date: string;
124
+ name: string;
125
+ } {
126
+ const actual = new Date(Date.UTC(year, month - 1, day));
127
+ const weekday = actual.getUTCDay();
128
+ const observed =
129
+ weekday === 0 ? addDays(actual, 1) : weekday === 6 ? addDays(actual, -1) : actual;
130
+ return {
131
+ date: dateKey(observed),
132
+ name: observed.getTime() === actual.getTime() ? name : `${name} observed`,
133
+ };
134
+ }
135
+
136
+ function nthWeekdayHoliday(
137
+ year: number,
138
+ month: number,
139
+ weekday: number,
140
+ occurrence: number,
141
+ name: string,
142
+ ): { date: string; name: string } {
143
+ const first = new Date(Date.UTC(year, month - 1, 1));
144
+ const offset = (weekday - first.getUTCDay() + 7) % 7;
145
+ return { date: dateKey(addDays(first, offset + (occurrence - 1) * 7)), name };
146
+ }
147
+
148
+ function lastWeekdayHoliday(
149
+ year: number,
150
+ month: number,
151
+ weekday: number,
152
+ name: string,
153
+ ): { date: string; name: string } {
154
+ const last = new Date(Date.UTC(year, month, 0));
155
+ const offset = (last.getUTCDay() - weekday + 7) % 7;
156
+ return { date: dateKey(addDays(last, -offset)), name };
157
+ }
158
+
159
+ function easterDate(year: number): Date {
160
+ const a = year % 19;
161
+ const b = Math.floor(year / 100);
162
+ const c = year % 100;
163
+ const d = Math.floor(b / 4);
164
+ const e = b % 4;
165
+ const f = Math.floor((b + 8) / 25);
166
+ const g = Math.floor((b - f + 1) / 3);
167
+ const h = (19 * a + b - d - g + 15) % 30;
168
+ const i = Math.floor(c / 4);
169
+ const k = c % 4;
170
+ const l = (32 + 2 * e + 2 * i - h - k) % 7;
171
+ const m = Math.floor((a + 11 * h + 22 * l) / 451);
172
+ const easterMonth = Math.floor((h + l - 7 * m + 114) / 31);
173
+ const easterDay = ((h + l - 7 * m + 114) % 31) + 1;
174
+ return new Date(Date.UTC(year, easterMonth - 1, easterDay));
175
+ }
176
+
177
+ function dateFromKey(key: string): Date {
178
+ const [year, month, day] = key.split("-").map(Number);
179
+ return new Date(Date.UTC(year, month - 1, day));
180
+ }
181
+
182
+ function addDays(date: Date, days: number): Date {
183
+ const next = new Date(date);
184
+ next.setUTCDate(next.getUTCDate() + days);
185
+ return next;
186
+ }
187
+
188
+ function dateKey(date: Date): string {
189
+ const year = date.getUTCFullYear();
190
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
191
+ const day = String(date.getUTCDate()).padStart(2, "0");
192
+ return `${year}-${month}-${day}`;
193
+ }
@@ -72,5 +72,6 @@ rateLimiter.configure("exa", 5, 0.1); // 5 req, ~6 req/min
72
72
  rateLimiter.configure("finnhub", 60, 1); // 60 req/min (free tier)
73
73
  // TradingView scanner is undocumented; keep usage batch-first and paced.
74
74
  rateLimiter.configure("tradingview", 5, 1); // 5 burst, 1 req/s sustained
75
+ rateLimiter.configure("polymarket", 10, 5); // 10 burst, 5 req/s sustained
75
76
  // SEC EDGAR fair-access guideline is 10 req/s; stay below it.
76
77
  rateLimiter.configure("sec_edgar", 5, 5); // 5 burst, 5 req/s sustained
@@ -17,25 +17,18 @@ export interface DailyWatchlistReport {
17
17
  dataGaps: string[];
18
18
  }
19
19
 
20
- export function getOrCreateDefaultWatchlistReportTemplate(
20
+ // Returns the configured default-watchlist morning template if one exists.
21
+ // Manual report runs must never create a schedule template as a side effect;
22
+ // only the explicit configure flow stores schedule intent.
23
+ export function findDefaultWatchlistReportTemplate(
21
24
  service: MarketStateService,
22
- ): ReportTemplateRecord {
23
- const existing = service
25
+ ): ReportTemplateRecord | undefined {
26
+ return service
24
27
  .listReportTemplates()
25
28
  .find(
26
29
  (template) =>
27
30
  template.reportType === "watchlist_daily" && targetsDefaultWatchlist(template.configJson),
28
31
  );
29
- if (existing) return existing;
30
- return service.createReportTemplate({
31
- name: "Morning watchlist",
32
- reportType: "watchlist_daily",
33
- cadence: "daily",
34
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
35
- localTime: "08:00",
36
- config: { targets: { default_watchlist: true } },
37
- enabled: true,
38
- });
39
32
  }
40
33
 
41
34
  export async function recordDailyWatchlistReportRun(
@@ -129,9 +122,6 @@ export async function generateDailyWatchlistReport(
129
122
  `Recent alerts`,
130
123
  ` ${service.listAlertEvents().length} recorded alert event(s).`,
131
124
  ``,
132
- `Technical snapshot`,
133
- ` Deferred unless quote/history data is available through a later section builder.`,
134
- ``,
135
125
  `Data gaps`,
136
126
  ...dataGapLines,
137
127
  ];
@@ -1,8 +1,6 @@
1
1
  import type Database from "better-sqlite3";
2
2
 
3
3
  export type AssetType = "equity" | "etf" | "fund" | "crypto" | "index" | "option" | "unknown";
4
- export type PredictionDirection = "bullish" | "bearish" | "neutral";
5
- export type PredictionStatus = "open" | "resolved" | "expired" | "cancelled";
6
4
  export type AlertScopeType = "instrument" | "watchlist" | "portfolio";
7
5
 
8
6
  export interface InstrumentInput {
@@ -100,23 +98,6 @@ export interface PortfolioLotRecord {
100
98
  updatedAt: string;
101
99
  }
102
100
 
103
- export interface PredictionRecord {
104
- id: number;
105
- instrumentId: number;
106
- symbol: string;
107
- direction: PredictionDirection;
108
- conviction: number;
109
- entryPrice: number;
110
- targetPrice: number | null;
111
- openedAt: string;
112
- expiresAt: string;
113
- status: PredictionStatus;
114
- resolvedAt: string | null;
115
- resultJson: string | null;
116
- createdAt: string;
117
- updatedAt: string;
118
- }
119
-
120
101
  export interface AlertRuleRecord {
121
102
  id: number;
122
103
  scopeType: AlertScopeType;
@@ -337,23 +318,6 @@ type PortfolioLotRow = {
337
318
  updated_at: string;
338
319
  };
339
320
 
340
- type PredictionRow = {
341
- id: number;
342
- instrument_id: number;
343
- symbol: string;
344
- direction: PredictionDirection;
345
- conviction: number;
346
- entry_price: number;
347
- target_price: number | null;
348
- opened_at: string;
349
- expires_at: string;
350
- status: PredictionStatus;
351
- resolved_at: string | null;
352
- result_json: string | null;
353
- created_at: string;
354
- updated_at: string;
355
- };
356
-
357
321
  type AlertRuleRow = {
358
322
  id: number;
359
323
  scope_type: AlertScopeType;
@@ -877,75 +841,6 @@ export class MarketStateService {
877
841
  });
878
842
  }
879
843
 
880
- recordPrediction(params: {
881
- instrument: InstrumentInput;
882
- direction: PredictionDirection;
883
- conviction: number;
884
- entryPrice: number;
885
- targetPrice?: number;
886
- timeframeDays: number;
887
- now?: Date;
888
- }): PredictionRecord {
889
- const tx = this.db.transaction(() => {
890
- const instrument = this.upsertInstrument(params.instrument);
891
- const opened = params.now ?? new Date();
892
- const expires = new Date(opened);
893
- expires.setDate(expires.getDate() + params.timeframeDays);
894
- const nowIso = opened.toISOString();
895
- const result = this.db
896
- .prepare(
897
- `INSERT INTO prediction_records (
898
- instrument_id, direction, conviction, entry_price, target_price,
899
- opened_at, expires_at, status, created_at, updated_at
900
- )
901
- VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, ?)`,
902
- )
903
- .run(
904
- instrument.id,
905
- params.direction,
906
- params.conviction,
907
- params.entryPrice,
908
- params.targetPrice ?? null,
909
- nowIso,
910
- expires.toISOString(),
911
- nowIso,
912
- nowIso,
913
- );
914
- return Number(result.lastInsertRowid);
915
- });
916
-
917
- return this.getPrediction(tx());
918
- }
919
-
920
- listPredictions(): PredictionRecord[] {
921
- const rows = this.db
922
- .prepare(
923
- `SELECT pr.*, i.symbol
924
- FROM prediction_records pr
925
- JOIN instruments i ON i.id = pr.instrument_id
926
- ORDER BY pr.opened_at, pr.id`,
927
- )
928
- .all() as PredictionRow[];
929
- return rows.map(mapPrediction);
930
- }
931
-
932
- updatePredictionOutcome(params: {
933
- id: number;
934
- status: Exclude<PredictionStatus, "open">;
935
- resolvedAt: string;
936
- result: unknown;
937
- }): PredictionRecord {
938
- const now = new Date().toISOString();
939
- this.db
940
- .prepare(
941
- `UPDATE prediction_records
942
- SET status = ?, resolved_at = ?, result_json = ?, updated_at = ?
943
- WHERE id = ?`,
944
- )
945
- .run(params.status, params.resolvedAt, JSON.stringify(params.result), now, params.id);
946
- return this.getPrediction(params.id);
947
- }
948
-
949
844
  createAlertRule(params: {
950
845
  scopeType: AlertScopeType;
951
846
  scopeId?: number;
@@ -1998,18 +1893,6 @@ export class MarketStateService {
1998
1893
  return row == null ? null : mapPortfolioLot(row);
1999
1894
  }
2000
1895
 
2001
- private getPrediction(id: number): PredictionRecord {
2002
- const row = this.db
2003
- .prepare(
2004
- `SELECT pr.*, i.symbol
2005
- FROM prediction_records pr
2006
- JOIN instruments i ON i.id = pr.instrument_id
2007
- WHERE pr.id = ?`,
2008
- )
2009
- .get(id) as PredictionRow;
2010
- return mapPrediction(row);
2011
- }
2012
-
2013
1896
  getAlertRule(id: number): AlertRuleRecord {
2014
1897
  const row = this.db.prepare("SELECT * FROM alert_rules WHERE id = ?").get(id) as AlertRuleRow;
2015
1898
  return mapAlertRule(row);
@@ -2118,25 +2001,6 @@ function mapPortfolioLot(row: PortfolioLotRow): PortfolioLotRecord {
2118
2001
  };
2119
2002
  }
2120
2003
 
2121
- function mapPrediction(row: PredictionRow): PredictionRecord {
2122
- return {
2123
- id: row.id,
2124
- instrumentId: row.instrument_id,
2125
- symbol: row.symbol,
2126
- direction: row.direction,
2127
- conviction: row.conviction,
2128
- entryPrice: row.entry_price,
2129
- targetPrice: row.target_price,
2130
- openedAt: row.opened_at,
2131
- expiresAt: row.expires_at,
2132
- status: row.status,
2133
- resolvedAt: row.resolved_at,
2134
- resultJson: row.result_json,
2135
- createdAt: row.created_at,
2136
- updatedAt: row.updated_at,
2137
- };
2138
- }
2139
-
2140
2004
  function mapAlertRule(row: AlertRuleRow): AlertRuleRecord {
2141
2005
  return {
2142
2006
  id: row.id,