diff --git a/.gitignore b/.gitignore index d3c60b4..dd7aa7e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,5 @@ market-calls.json ui/.svelte-kit ui/build -# Reports -screener-report.html -finance-report.html \ No newline at end of file +# Runtime cache +.benchmark-cache.json \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 85a310a..5e69b5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -553,7 +553,7 @@ This section is the single reference for where code lives and how to add feature - Class instances don't survive `JSON.stringify`. Call `getDisplayMetrics()` server-side before returning from API routes (see `serializeAssets()` in `screener.controller.ts`). - Controllers use constructor injection — dependencies are wired in `server/app.ts`, not created inside handlers. - The `$types` alias in the UI resolves to `server/types/` — use it instead of duplicating type definitions. -- Ticker normalisation (`BRK.B → BRK-B`) currently only happens in `FinanceController.normalizeYahoo()`. Submitting `BRK.B` directly to `/api/screen` will fail. Fix target: move normalisation into `YahooFinanceClient.fetchSummary()`. +- Ticker normalisation (`BRK.B → BRK-B`) happens in `YahooFinanceClient.normalise()` and applies to all callers via `fetchSummary()` and `fetchCalendarEvents()`. ### Adding a new scoring metric — step-by-step @@ -622,17 +622,6 @@ Add one Fastify `inject()` smoke test per route using a fixture for `ScreenerEng `MarketCallRepository` has zero test coverage. Add `tests/MarketCallRepository.test.js` using a temp file path (inject via constructor or env var) to test `list`, `create`, `delete`, and concurrent-write safety. -#### 8e — Ticker normalisation in `YahooFinanceClient` - -`BRK.B → BRK-B` normalisation lives only in `FinanceController`. Move it to `YahooFinanceClient.fetchSummary()` so it applies to all callers including `/api/screen`. - -```ts -async fetchSummary(ticker: string, ...): Promise { - const normalized = ticker.replace(/\./g, '-'); - return await this.lib.quoteSummary(normalized, { modules: YAHOO_MODULES }); -} -``` - #### 8f — Persistent benchmark cache `BenchmarkProvider`'s 1-hour cache is in-memory only — cold start after every restart adds 2–4s latency to the first request. Write the cached `MarketContext` to `.benchmark-cache.json` (or a single-row SQLite table). Read it on boot; only re-fetch if stale. diff --git a/finance-report.html b/finance-report.html new file mode 100644 index 0000000..1133636 --- /dev/null +++ b/finance-report.html @@ -0,0 +1,211 @@ + + + + + +Personal Finance — 2026-06-03 + + + +
+

💰 Personal Finance

+
Date 2026-06-03
+
+
+ + + + +
+

Portfolio — Hold / Sell / Add Advice

+
+
+
Total Value
+
$41,451
+ +
+
+
Total Cost
+
$25,180
+ +
+
+
Total G/L
+
$16,271
+
64.6%
+
+
+
S&P 500 P/E
+
28.5x
+
Live benchmark
+
+
+ + +

Stocks & ETFs

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TickerSourceTypeSharesCost BasisCurrentValueG/LSignalAdviceReason
AAPLRobinhoodstock10$150.00$315.20$3,152.00110.1%⚠️ Speculation🟠 Reduce PositionIn profit on a speculative position — take partial profits.
PLTRRobinhoodstock50$18.50$152.17$7,608.50722.5%❌ Avoid🔴 Sell (Take Profits)Fails both analyses — you're in profit, take it.
TSLARobinhoodstock3$200.00$423.74$1,271.22111.9%❌ Avoid🔴 Sell (Take Profits)Fails both analyses — you're in profit, take it.
MSFTRobinhoodstock5$300.00$441.31$2,206.5547.1%✅ Strong Buy🟢 Hold & AddPasses both analyses. Strong conviction.
VOOVanguardetf8$380.00$698.26$5,586.0883.8%⚡ Momentum🟡 HoldUp significantly on momentum — consider partial profit-taking.
BNDVanguardetf15$75.00$73.20$1,098.00-2.4%🔄 Neutral🟡 HoldNo clear edge. Review on any catalyst.
ORobinhoodstock20$52.00$59.91$1,198.2015.2%✅ Strong Buy🟢 Hold & AddPasses both analyses. Strong conviction.
+ + +

Crypto

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
TickerSourceSharesCost BasisCurrentValueG/LAdviceNote
BTC-USDCoinbase0.25$45,000.00$66,289.92$16,572.4847.3%🟡 HoldCrypto — no fundamental analysis. Track price and manage risk manually.
ETH-USDCoinbase1.5$2,800.00$1,838.88$2,758.32-34.3%🔴 Review positionCrypto — no fundamental analysis. Track price and manage risk manually.
+
+ + + + + +
+ + \ No newline at end of file diff --git a/screener-report.html b/screener-report.html new file mode 100644 index 0000000..8b6b4c4 --- /dev/null +++ b/screener-report.html @@ -0,0 +1,292 @@ + + + + + +Market Screener — 2026-06-03 + + + + +
+

📊 Market Screener

+
+
Date 2026-06-03
+
Rate NORMAL
+
Volatility NORMAL
+
+
+ +
+ +
+
10Y Yield
4.46%
+
VIX
15.8
+
S&P 500
7,609.78
+
S&P 500 P/E
28.5x
+
Tech P/E
43.4x
+
REIT Yield
3.50%
+
IG Spread
0.10%
+
+ +
+

Signal Summary

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TickerTypeSignalInflated VerdictFundamental Verdict
NVDASTOCK✅ Strong Buy🟢 BUY (High Conviction)🟢 BUY (High Conviction)
OPENSTOCK🔄 Neutral🟡 HOLD🟡 HOLD
OPADSTOCK🔄 Neutral🟡 HOLD🟡 HOLD
SNSSTOCK🔄 Neutral🟡 HOLD🟡 HOLD
AAPLSTOCK⚠️ Speculation🟢 BUY (High Conviction)🔴 REJECT
GOOGSTOCK⚠️ Speculation🟢 BUY (High Conviction)🔴 REJECT
AMZNSTOCK⚠️ Speculation🟢 BUY (Speculative)🔴 REJECT
AMKRSTOCK⚠️ Speculation🟢 BUY (Speculative)🔴 REJECT
MRVLSTOCK❌ Avoid🔴 REJECT🔴 REJECT
CRDOSTOCK❌ Avoid🔴 REJECT🔴 REJECT
CATSTOCK❌ Avoid🔴 REJECT🔴 REJECT
MCHPSTOCK❌ Avoid🔴 REJECT🔴 REJECT
MPWRSTOCK❌ Avoid🔴 REJECT🔴 REJECT
HPESTOCK❌ Avoid🔴 REJECT🔴 REJECT
PANWSTOCK❌ Avoid🔴 REJECT🔴 REJECT
CSCOSTOCK❌ Avoid🔴 REJECT🔴 REJECT
SHOPSTOCK❌ Avoid🔴 REJECT🔴 REJECT
VLOSTOCK❌ Avoid🔴 REJECT🔴 REJECT
DOCUSTOCK❌ Avoid🔴 REJECT🔴 REJECT
BBCPSTOCK❌ Avoid🔴 REJECT🔴 REJECT
WMTSTOCK❌ Avoid🔴 REJECT🔴 REJECT
COSTSTOCK❌ Avoid🔴 REJECT🔴 REJECT
TGTSTOCK❌ Avoid🔴 REJECT🔴 REJECT
FIGSTOCK❌ Avoid🔴 REJECT🔴 REJECT
INTCSTOCK❌ Avoid🔴 REJECT🔴 REJECT
RBRKSTOCK❌ Avoid🔴 REJECT🔴 REJECT
+
+ + +
+

STOCKS

+
+
Market-Adjusted (P/E gate: ~43x from live data)
+
Fundamental (Graham-style)
+
+
+ + + +
TickerPriceVerdictScoreSectorP/EPEGP/BROE%OpMgn%NetMgn%Rev%FCF Yld%Div%D/EQuickBeta52W PosP/FFORisk Flags
NVDA$222.82🟢 BUY (High Conviction)Score: 13TECHNOLOGY34.10.69 34.43114.3% 65.6% 63.0% 85.2% 0.9% 0.02% 0.072.142.2486%43.0 ⚠ High volatility (β 2.24)
OPEN$5.41🟡 HOLDScore: 3REIT-456.95.15-173.6% -22.1% -35.2% -37.6% 22.6% 0.00% 1.403.1547%4.8
OPAD$0.82🟡 HOLDScore: 3REIT-3.40.85-103.6% -11.3% -8.5% -50.2% 287.2% 0.00% 2.060.642.464%0.5 ⚠ High volatility (β 2.46)⚠ Near 52-week low — potential opportunity
SNSN/A🟡 HOLDScore: 0GENERAL
AAPL$315.20🟢 BUY (High Conviction)Score: 8TECHNOLOGY38.22.72 43.42141.5% 32.3% 27.2% 16.6% 2.2% 0.34% 0.800.911.06100%33.0 ⚠ Near 52-week high — crowded trade
GOOG$358.39🟢 BUY (High Conviction)Score: 10COMMUNICATION27.31.48 9.0738.9% 36.1% 37.9% 21.8% 1.4% 0.23% 0.201.711.2781%11.2
AMZN$256.52🟢 BUY (Speculative)Score: 7CONSUMER_DISCRETIONARY31.61.83 6.2424.3% 13.1% 12.2% 16.6% 0.4% 0.00% 0.530.971.4773%18.6
AMKR$74.74🟢 BUY (Speculative)Score: 5TECHNOLOGY43.00.76 4.0910.0% 6.0% 6.2% 27.5% -0.3% 0.46% 0.351.702.3193%15.2 ⚠ High volatility (β 2.31)⚠ Near 52-week high — crowded trade
MRVL$290.79🔴 REJECTGate failed: P/E 100 > 56TECHNOLOGY99.91.17 17.2216.0% 14.5% 29.0% 27.6% 0.9% 0.11% 0.292.512.25100%123.7
CRDO$229.00🔴 REJECTGate failed: P/E 127 > 56TECHNOLOGY126.50.37 22.8234.4% 35.7% 35.4% 157.0% 0.00% 0.018.513.1891%
CAT$909.81🔴 REJECTGate failed: D/E 2.3 > 1.5 | Quick 0.73 < 0.8 | P/E 45 > 43GENERAL45.22.06 22.4651.3% 18.2% 13.3% 22.2% 0.9% 0.70% 2.310.731.6396%34.0
MCHP$96.96🔴 REJECTGate failed: P/E 441 > 56TECHNOLOGY440.70.34 8.163.4% 17.1% 4.9% 35.1% 2.2% 1.99% 0.881.001.7484%54.6
MPWR$1624.99🔴 REJECTGate failed: P/E 116 > 56TECHNOLOGY116.32.03 21.7119.6% 30.0% 23.0% 26.1% 0.6% 0.43% 0.013.431.7491%95.9
HPE$56.15🔴 REJECTGate failed: Quick 0.57 < 0.8TECHNOLOGY52.50.85 3.016.3% 7.9% 4.0% 40.0% 4.3% 0.00% 0.840.571.2983%11.7
PANW$297.18🔴 REJECTGate failed: P/E 256 > 56 | PEG 5.0 > 2.9TECHNOLOGY256.25.04 22.2416.3% 15.5% 13.0% 14.9% 1.2% 0.00% 0.050.910.7796%60.6
CSCO$128.00🔴 REJECTGate failed: Quick 0.70 < 0.8TECHNOLOGY42.71.67 10.3225.2% 25.0% 19.7% 12.0% 1.8% 1.36% 0.680.700.91100%38.7
SHOP$117.01🔴 REJECTGate failed: P/E 115 > 56TECHNOLOGY114.72.10 12.1811.3% 15.7% 10.8% 34.3% 0.9% 0.00% 0.014.532.6426%66.5
VLO$258.26🔴 REJECTGate failed: PEG 4.1 > 2.4ENERGY18.94.08 3.2515.8% 6.1% 3.6% 6.6% 6.2% 1.82% 0.431.080.5796%12.2
DOCU$55.10🔴 REJECTGate failed: Quick 0.68 < 0.8TECHNOLOGY37.20.63 5.6815.8% 10.5% 9.6% 7.8% 11.2% 0.00% 0.100.680.8827%9.2
BBCP$7.83🔴 REJECTGate failed: D/E 1.5 > 1.5 | P/E 87 > 43GENERAL87.01.512.3% 5.0% 1.7% 4.8% -7.4% 0.00% 1.531.680.9488%5.0
WMT$113.06🔴 REJECTGate failed: Quick 0.19 < 0.5 | PEG 4.6 > 2.4CONSUMER_STAPLES39.84.56 9.0424.1% 4.2% 3.1% 7.3% 0.8% 0.83% 0.750.190.6547%22.0
COST$954.27🔴 REJECTGate failed: P/E 48 > 43 | PEG 4.9 > 2.4CONSUMER_STAPLES48.14.86 25.6029.2% 3.7% 3.0% 21.5% 1.8% 0.57% 0.600.560.9144%28.2
TGT$123.18🔴 REJECTGate failed: Quick 0.18 < 0.5CONSUMER_STAPLES16.32.36 3.4522.0% 4.5% 3.2% 6.7% 5.6% 3.67% 1.180.181.0180%8.0
FIG$24.29🔴 REJECTGate failed: P/E 72 > 56 | PEG 4.9 > 2.9TECHNOLOGY72.04.93 8.77-101.7% -41.2% -123.8% 46.1% 7.5% 0.00% 0.042.366%43.2
INTC$107.93🔴 REJECTGate failed: P/E 70 > 56TECHNOLOGY69.91.36 4.87-2.9% 6.9% -5.9% 7.2% -1.5% 0.00% 0.361.662.1978%54.4
RBRK$82.33🔴 REJECTGate failed: P/E 141 > 56TECHNOLOGY141.3-21.8% -26.5% 46.3% 3.0% 0.00% 1.470.6366%46.8
+
+
+ + + +
TickerPriceVerdictScoreSectorP/EPEGP/BROE%OpMgn%NetMgn%Rev%FCF Yld%Div%D/EQuickBeta52W PosP/FFORisk Flags
NVDA$222.82🟢 BUY (High Conviction)Score: 13TECHNOLOGY34.10.69 34.43114.3% 65.6% 63.0% 85.2% 0.9% 0.02% 0.072.142.2486%43.0 ⚠ High volatility (β 2.24)
OPEN$5.41🟡 HOLDScore: 3REIT-456.95.15-173.6% -22.1% -35.2% -37.6% 22.6% 0.00% 1.403.1547%4.8
OPAD$0.82🟡 HOLDScore: 3REIT-3.40.85-103.6% -11.3% -8.5% -50.2% 287.2% 0.00% 2.060.642.464%0.5 ⚠ High volatility (β 2.46)⚠ Near 52-week low — potential opportunity
SNSN/A🟡 HOLDScore: 0GENERAL
AAPL$315.20🔴 REJECTGate failed: P/E 38 > 35 | PEG 2.7 > 1.5TECHNOLOGY38.22.72 43.42141.5% 32.3% 27.2% 16.6% 2.2% 0.34% 0.800.911.06100%33.0
GOOG$358.39🔴 REJECTGate failed: P/E 27 > 25COMMUNICATION27.31.48 9.0738.9% 36.1% 37.9% 21.8% 1.4% 0.23% 0.201.711.2781%11.2
AMZN$256.52🔴 REJECTGate failed: P/E 32 > 25 | PEG 1.8 > 1.5CONSUMER_DISCRETIONARY31.61.83 6.2424.3% 13.1% 12.2% 16.6% 0.4% 0.00% 0.530.971.4773%18.6
AMKR$74.74🔴 REJECTGate failed: P/E 43 > 35TECHNOLOGY43.00.76 4.0910.0% 6.0% 6.2% 27.5% -0.3% 0.46% 0.351.702.3193%15.2
MRVL$290.79🔴 REJECTGate failed: P/E 100 > 35TECHNOLOGY99.91.17 17.2216.0% 14.5% 29.0% 27.6% 0.9% 0.11% 0.292.512.25100%123.7
CRDO$229.00🔴 REJECTGate failed: P/E 127 > 35TECHNOLOGY126.50.37 22.8234.4% 35.7% 35.4% 157.0% 0.00% 0.018.513.1891%
CAT$909.81🔴 REJECTGate failed: D/E 2.3 > 1.5 | Quick 0.73 < 0.8 | P/E 45 > 15 | PEG 2.1 > 1GENERAL45.22.06 22.4651.3% 18.2% 13.3% 22.2% 0.9% 0.70% 2.310.731.6396%34.0
MCHP$96.96🔴 REJECTGate failed: P/E 441 > 35TECHNOLOGY440.70.34 8.163.4% 17.1% 4.9% 35.1% 2.2% 1.99% 0.881.001.7484%54.6
MPWR$1624.99🔴 REJECTGate failed: P/E 116 > 35 | PEG 2.0 > 1.5TECHNOLOGY116.32.03 21.7119.6% 30.0% 23.0% 26.1% 0.6% 0.43% 0.013.431.7491%95.9
HPE$56.15🔴 REJECTGate failed: Quick 0.57 < 0.8 | P/E 52 > 35TECHNOLOGY52.50.85 3.016.3% 7.9% 4.0% 40.0% 4.3% 0.00% 0.840.571.2983%11.7
PANW$297.18🔴 REJECTGate failed: P/E 256 > 35 | PEG 5.0 > 1.5TECHNOLOGY256.25.04 22.2416.3% 15.5% 13.0% 14.9% 1.2% 0.00% 0.050.910.7796%60.6
CSCO$128.00🔴 REJECTGate failed: Quick 0.70 < 0.8 | P/E 43 > 35 | PEG 1.7 > 1.5TECHNOLOGY42.71.67 10.3225.2% 25.0% 19.7% 12.0% 1.8% 1.36% 0.680.700.91100%38.7
SHOP$117.01🔴 REJECTGate failed: P/E 115 > 35 | PEG 2.1 > 1.5TECHNOLOGY114.72.10 12.1811.3% 15.7% 10.8% 34.3% 0.9% 0.00% 0.014.532.6426%66.5
VLO$258.26🔴 REJECTGate failed: P/E 19 > 15 | PEG 4.1 > 1.5ENERGY18.94.08 3.2515.8% 6.1% 3.6% 6.6% 6.2% 1.82% 0.431.080.5796%12.2
DOCU$55.10🔴 REJECTGate failed: Quick 0.68 < 0.8 | P/E 37 > 35TECHNOLOGY37.20.63 5.6815.8% 10.5% 9.6% 7.8% 11.2% 0.00% 0.100.680.8827%9.2
BBCP$7.83🔴 REJECTGate failed: D/E 1.5 > 1.5 | P/E 87 > 15GENERAL87.01.512.3% 5.0% 1.7% 4.8% -7.4% 0.00% 1.531.680.9488%5.0
WMT$113.06🔴 REJECTGate failed: Quick 0.19 < 0.5 | P/E 40 > 22 | PEG 4.6 > 2CONSUMER_STAPLES39.84.56 9.0424.1% 4.2% 3.1% 7.3% 0.8% 0.83% 0.750.190.6547%22.0
COST$954.27🔴 REJECTGate failed: P/E 48 > 22 | PEG 4.9 > 2CONSUMER_STAPLES48.14.86 25.6029.2% 3.7% 3.0% 21.5% 1.8% 0.57% 0.600.560.9144%28.2
TGT$123.18🔴 REJECTGate failed: Quick 0.18 < 0.5 | PEG 2.4 > 2CONSUMER_STAPLES16.32.36 3.4522.0% 4.5% 3.2% 6.7% 5.6% 3.67% 1.180.181.0180%8.0
FIG$24.29🔴 REJECTGate failed: P/E 72 > 35 | PEG 4.9 > 1.5TECHNOLOGY72.04.93 8.77-101.7% -41.2% -123.8% 46.1% 7.5% 0.00% 0.042.366%43.2
INTC$107.93🔴 REJECTGate failed: P/E 70 > 35TECHNOLOGY69.91.36 4.87-2.9% 6.9% -5.9% 7.2% -1.5% 0.00% 0.361.662.1978%54.4
RBRK$82.33🔴 REJECTGate failed: P/E 141 > 35TECHNOLOGY141.3-21.8% -26.5% 46.3% 3.0% 0.00% 1.470.6366%46.8
+
+
+ + + + + +
+ + + + \ No newline at end of file diff --git a/server/clients/YahooFinanceClient.ts b/server/clients/YahooFinanceClient.ts index a855fd5..0064b24 100644 --- a/server/clients/YahooFinanceClient.ts +++ b/server/clients/YahooFinanceClient.ts @@ -11,10 +11,16 @@ export class YahooFinanceClient { }); } + /** Normalise ticker before hitting Yahoo: BRK.B → BRK-B */ + private static normalise(ticker: string): string { + return ticker.toUpperCase().replace(/\./g, '-'); + } + async fetchSummary(ticker: string, retries = 3, backoff = 1000): Promise { + const normalised = YahooFinanceClient.normalise(ticker); for (let attempt = 0; attempt < retries; attempt++) { try { - return await this.lib.quoteSummary(ticker, { modules: YAHOO_MODULES }); + return await this.lib.quoteSummary(normalised, { modules: YAHOO_MODULES }); } catch (error) { if (attempt === retries - 1) throw error; await new Promise((resolve) => setTimeout(resolve, backoff * (attempt + 1))); @@ -24,7 +30,9 @@ export class YahooFinanceClient { async fetchCalendarEvents(ticker: string): Promise { try { - const result = await this.lib.quoteSummary(ticker, { modules: ['calendarEvents'] }); + const result = await this.lib.quoteSummary(YahooFinanceClient.normalise(ticker), { + modules: ['calendarEvents'], + }); return result.calendarEvents ?? null; } catch { return null; diff --git a/server/controllers/finance.controller.ts b/server/controllers/finance.controller.ts index 899bab2..2047ece 100644 --- a/server/controllers/finance.controller.ts +++ b/server/controllers/finance.controller.ts @@ -13,10 +13,6 @@ export class FinanceController { private readonly advisor: PortfolioAdvisor, ) {} - private static normalizeYahoo(ticker: string): string { - return ticker.toUpperCase().replace(/\./g, '-'); - } - register(app: FastifyInstance): void { app.get('/api/finance/portfolio', this.portfolio.bind(this)); app.post('/api/finance/holdings', { schema: holdingSchema }, this.addHolding.bind(this)); @@ -38,7 +34,7 @@ export class FinanceController { const screenable = holdings .filter((h) => (h.type ?? 'stock') !== 'crypto') - .map((h) => FinanceController.normalizeYahoo(h.ticker)); + .map((h) => h.ticker.toUpperCase()); const results = screenable.length > 0 diff --git a/server/services/BenchmarkProvider.ts b/server/services/BenchmarkProvider.ts index f9734e2..433ab20 100644 --- a/server/services/BenchmarkProvider.ts +++ b/server/services/BenchmarkProvider.ts @@ -1,9 +1,16 @@ +import { readFileSync, writeFileSync, existsSync } from 'fs'; import { YahooFinanceClient } from '../clients/YahooFinanceClient'; import { REGIME } from '../config/constants'; import type { MarketContext, Logger, BenchmarkProviderOptions } from '../types'; +interface CacheFile { + data: MarketContext; + expiresAt: number; +} + export class BenchmarkProvider { private static readonly TTL_MS = 60 * 60 * 1000; + private static readonly CACHE_PATH = '.benchmark-cache.json'; private static readonly DEFAULTS: MarketContext = { sp500Price: 5000, @@ -32,10 +39,33 @@ export class BenchmarkProvider { private readonly client: YahooFinanceClient, { logger }: BenchmarkProviderOptions = {}, ) { - this.cache = { data: null, expiresAt: 0 }; + this.cache = this.loadDiskCache(); this.logger = logger ?? (console as unknown as Logger); } + private loadDiskCache(): { data: MarketContext | null; expiresAt: number } { + try { + if (!existsSync(BenchmarkProvider.CACHE_PATH)) return { data: null, expiresAt: 0 }; + const file = JSON.parse(readFileSync(BenchmarkProvider.CACHE_PATH, 'utf8')) as CacheFile; + if (Date.now() < file.expiresAt) return { data: file.data, expiresAt: file.expiresAt }; + } catch { + // corrupt or missing — ignore + } + return { data: null, expiresAt: 0 }; + } + + private saveDiskCache(data: MarketContext, expiresAt: number): void { + try { + writeFileSync( + BenchmarkProvider.CACHE_PATH, + JSON.stringify({ data, expiresAt } satisfies CacheFile, null, 2), + 'utf8', + ); + } catch { + // non-fatal — in-memory cache still works + } + } + async getMarketContext(): Promise { if (this.cache.data && Date.now() < this.cache.expiresAt) return this.cache.data; @@ -75,7 +105,9 @@ export class BenchmarkProvider { }, }; - this.cache = { data: context, expiresAt: Date.now() + BenchmarkProvider.TTL_MS }; + const expiresAt = Date.now() + BenchmarkProvider.TTL_MS; + this.cache = { data: context, expiresAt }; + this.saveDiskCache(context, expiresAt); return context; } catch (err) { this.logger.warn('Market data fetch failed, using defaults:', (err as Error).message);