phase-8f: persistant cache locally
This commit is contained in:
committed by
saikiranvella
parent
ff1b99910b
commit
5af9ded35e
+2
-3
@@ -11,6 +11,5 @@ market-calls.json
|
|||||||
ui/.svelte-kit
|
ui/.svelte-kit
|
||||||
ui/build
|
ui/build
|
||||||
|
|
||||||
# Reports
|
# Runtime cache
|
||||||
screener-report.html
|
.benchmark-cache.json
|
||||||
finance-report.html
|
|
||||||
@@ -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`).
|
- 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.
|
- 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.
|
- 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
|
### 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.
|
`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<any> {
|
|
||||||
const normalized = ticker.replace(/\./g, '-');
|
|
||||||
return await this.lib.quoteSummary(normalized, { modules: YAHOO_MODULES });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 8f — Persistent benchmark cache
|
#### 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.
|
`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.
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Personal Finance — 2026-06-03</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e2e8f0; font-size: 13px; }
|
||||||
|
h1 { font-size: 20px; font-weight: 600; }
|
||||||
|
h2 { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #94a3b8; margin-bottom: 12px; }
|
||||||
|
.header { padding: 24px 32px 16px; border-bottom: 1px solid #1e293b; display: flex; align-items: center; gap: 16px; }
|
||||||
|
.pill { background: #1e293b; border-radius: 6px; padding: 4px 12px; font-size: 12px; color: #94a3b8; margin-left: auto; }
|
||||||
|
.pill span { color: #e2e8f0; font-weight: 600; margin-left: 4px; }
|
||||||
|
.content { padding: 24px 32px; }
|
||||||
|
.section { margin-bottom: 40px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 14px 16px; }
|
||||||
|
.card-label { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
|
.card-value { font-size: 20px; font-weight: 700; color: #f1f5f9; margin-top: 4px; }
|
||||||
|
.card-sub { font-size: 11px; color: #64748b; margin-top: 2px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
thead th { text-align: left; padding: 8px 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: #64748b; border-bottom: 1px solid #1e293b; white-space: nowrap; }
|
||||||
|
tbody tr { border-bottom: 1px solid #1a2233; }
|
||||||
|
tbody tr:hover { background: #1e293b; }
|
||||||
|
tbody td { padding: 10px 12px; vertical-align: middle; }
|
||||||
|
.ticker { font-weight: 700; font-size: 14px; color: #f1f5f9; }
|
||||||
|
.green { color: #4ade80; }
|
||||||
|
.yellow { color: #facc15; }
|
||||||
|
.orange { color: #fb923c; }
|
||||||
|
.red { color: #f87171; }
|
||||||
|
.gray { color: #64748b; }
|
||||||
|
.advice-green { color: #4ade80; font-weight: 600; }
|
||||||
|
.advice-yellow { color: #facc15; font-weight: 600; }
|
||||||
|
.advice-orange { color: #fb923c; font-weight: 600; }
|
||||||
|
.advice-red { color: #f87171; font-weight: 600; }
|
||||||
|
.reason { color: #94a3b8; font-size: 11px; }
|
||||||
|
.bar-bg { background: #1e293b; border-radius: 4px; height: 8px; }
|
||||||
|
.bar-fill { background: #3b82f6; border-radius: 4px; height: 8px; }
|
||||||
|
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>💰 Personal Finance</h1>
|
||||||
|
<div class="pill">Date <span>2026-06-03</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Portfolio — Hold / Sell / Add Advice</h2>
|
||||||
|
<div class="grid" style="margin-bottom:16px">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Total Value</div>
|
||||||
|
<div class="card-value ">$41,451</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Total Cost</div>
|
||||||
|
<div class="card-value ">$25,180</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Total G/L</div>
|
||||||
|
<div class="card-value green">$16,271</div>
|
||||||
|
<div class="card-sub">64.6%</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">S&P 500 P/E</div>
|
||||||
|
<div class="card-value ">28.5x</div>
|
||||||
|
<div class="card-sub">Live benchmark</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h2 style="margin-bottom:10px">Stocks & ETFs</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr>
|
||||||
|
<th>Ticker</th><th>Source</th><th>Type</th><th>Shares</th>
|
||||||
|
<th>Cost Basis</th><th>Current</th><th>Value</th>
|
||||||
|
<th>G/L</th><th>Signal</th><th>Advice</th><th>Reason</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="ticker">AAPL</td>
|
||||||
|
<td><span style="background:#22c55e22;color:#22c55e;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Robinhood</span></td>
|
||||||
|
<td><span style="background:#1e293b;padding:2px 8px;border-radius:4px;font-size:11px">stock</span></td>
|
||||||
|
<td>10</td>
|
||||||
|
<td>$150.00</td>
|
||||||
|
<td>$315.20</td>
|
||||||
|
<td>$3,152.00</td>
|
||||||
|
<td class="green">110.1%</td>
|
||||||
|
<td class="gray" style="font-size:11px">⚠️ Speculation</td>
|
||||||
|
<td class="advice-orange">🟠 Reduce Position</td>
|
||||||
|
<td class="reason">In profit on a speculative position — take partial profits.</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td class="ticker">PLTR</td>
|
||||||
|
<td><span style="background:#22c55e22;color:#22c55e;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Robinhood</span></td>
|
||||||
|
<td><span style="background:#1e293b;padding:2px 8px;border-radius:4px;font-size:11px">stock</span></td>
|
||||||
|
<td>50</td>
|
||||||
|
<td>$18.50</td>
|
||||||
|
<td>$152.17</td>
|
||||||
|
<td>$7,608.50</td>
|
||||||
|
<td class="green">722.5%</td>
|
||||||
|
<td class="gray" style="font-size:11px">❌ Avoid</td>
|
||||||
|
<td class="advice-red">🔴 Sell (Take Profits)</td>
|
||||||
|
<td class="reason">Fails both analyses — you're in profit, take it.</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td class="ticker">TSLA</td>
|
||||||
|
<td><span style="background:#22c55e22;color:#22c55e;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Robinhood</span></td>
|
||||||
|
<td><span style="background:#1e293b;padding:2px 8px;border-radius:4px;font-size:11px">stock</span></td>
|
||||||
|
<td>3</td>
|
||||||
|
<td>$200.00</td>
|
||||||
|
<td>$423.74</td>
|
||||||
|
<td>$1,271.22</td>
|
||||||
|
<td class="green">111.9%</td>
|
||||||
|
<td class="gray" style="font-size:11px">❌ Avoid</td>
|
||||||
|
<td class="advice-red">🔴 Sell (Take Profits)</td>
|
||||||
|
<td class="reason">Fails both analyses — you're in profit, take it.</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td class="ticker">MSFT</td>
|
||||||
|
<td><span style="background:#22c55e22;color:#22c55e;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Robinhood</span></td>
|
||||||
|
<td><span style="background:#1e293b;padding:2px 8px;border-radius:4px;font-size:11px">stock</span></td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>$300.00</td>
|
||||||
|
<td>$441.31</td>
|
||||||
|
<td>$2,206.55</td>
|
||||||
|
<td class="green">47.1%</td>
|
||||||
|
<td class="gray" style="font-size:11px">✅ Strong Buy</td>
|
||||||
|
<td class="advice-green">🟢 Hold & Add</td>
|
||||||
|
<td class="reason">Passes both analyses. Strong conviction.</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td class="ticker">VOO</td>
|
||||||
|
<td><span style="background:#3b82f622;color:#3b82f6;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Vanguard</span></td>
|
||||||
|
<td><span style="background:#1e293b;padding:2px 8px;border-radius:4px;font-size:11px">etf</span></td>
|
||||||
|
<td>8</td>
|
||||||
|
<td>$380.00</td>
|
||||||
|
<td>$698.26</td>
|
||||||
|
<td>$5,586.08</td>
|
||||||
|
<td class="green">83.8%</td>
|
||||||
|
<td class="gray" style="font-size:11px">⚡ Momentum</td>
|
||||||
|
<td class="advice-yellow">🟡 Hold</td>
|
||||||
|
<td class="reason">Up significantly on momentum — consider partial profit-taking.</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td class="ticker">BND</td>
|
||||||
|
<td><span style="background:#3b82f622;color:#3b82f6;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Vanguard</span></td>
|
||||||
|
<td><span style="background:#1e293b;padding:2px 8px;border-radius:4px;font-size:11px">etf</span></td>
|
||||||
|
<td>15</td>
|
||||||
|
<td>$75.00</td>
|
||||||
|
<td>$73.20</td>
|
||||||
|
<td>$1,098.00</td>
|
||||||
|
<td class="red">-2.4%</td>
|
||||||
|
<td class="gray" style="font-size:11px">🔄 Neutral</td>
|
||||||
|
<td class="advice-yellow">🟡 Hold</td>
|
||||||
|
<td class="reason">No clear edge. Review on any catalyst.</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td class="ticker">O</td>
|
||||||
|
<td><span style="background:#22c55e22;color:#22c55e;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Robinhood</span></td>
|
||||||
|
<td><span style="background:#1e293b;padding:2px 8px;border-radius:4px;font-size:11px">stock</span></td>
|
||||||
|
<td>20</td>
|
||||||
|
<td>$52.00</td>
|
||||||
|
<td>$59.91</td>
|
||||||
|
<td>$1,198.20</td>
|
||||||
|
<td class="green">15.2%</td>
|
||||||
|
<td class="gray" style="font-size:11px">✅ Strong Buy</td>
|
||||||
|
<td class="advice-green">🟢 Hold & Add</td>
|
||||||
|
<td class="reason">Passes both analyses. Strong conviction.</td>
|
||||||
|
</tr></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
<h2 style="margin-top:24px;margin-bottom:10px">Crypto</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr>
|
||||||
|
<th>Ticker</th><th>Source</th><th>Shares</th>
|
||||||
|
<th>Cost Basis</th><th>Current</th><th>Value</th>
|
||||||
|
<th>G/L</th><th>Advice</th><th>Note</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="ticker">BTC-USD</td>
|
||||||
|
<td><span style="background:#8b5cf622;color:#8b5cf6;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Coinbase</span></td>
|
||||||
|
<td>0.25</td>
|
||||||
|
<td>$45,000.00</td>
|
||||||
|
<td>$66,289.92</td>
|
||||||
|
<td>$16,572.48</td>
|
||||||
|
<td class="green">47.3%</td>
|
||||||
|
<td class="advice-yellow">🟡 Hold</td>
|
||||||
|
<td class="reason">Crypto — no fundamental analysis. Track price and manage risk manually.</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td class="ticker">ETH-USD</td>
|
||||||
|
<td><span style="background:#8b5cf622;color:#8b5cf6;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600">Coinbase</span></td>
|
||||||
|
<td>1.5</td>
|
||||||
|
<td>$2,800.00</td>
|
||||||
|
<td>$1,838.88</td>
|
||||||
|
<td>$2,758.32</td>
|
||||||
|
<td class="red">-34.3%</td>
|
||||||
|
<td class="advice-red">🔴 Review position</td>
|
||||||
|
<td class="reason">Crypto — no fundamental analysis. Track price and manage risk manually.</td>
|
||||||
|
</tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
@@ -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<any> {
|
async fetchSummary(ticker: string, retries = 3, backoff = 1000): Promise<any> {
|
||||||
|
const normalised = YahooFinanceClient.normalise(ticker);
|
||||||
for (let attempt = 0; attempt < retries; attempt++) {
|
for (let attempt = 0; attempt < retries; attempt++) {
|
||||||
try {
|
try {
|
||||||
return await this.lib.quoteSummary(ticker, { modules: YAHOO_MODULES });
|
return await this.lib.quoteSummary(normalised, { modules: YAHOO_MODULES });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (attempt === retries - 1) throw error;
|
if (attempt === retries - 1) throw error;
|
||||||
await new Promise<void>((resolve) => setTimeout(resolve, backoff * (attempt + 1)));
|
await new Promise<void>((resolve) => setTimeout(resolve, backoff * (attempt + 1)));
|
||||||
@@ -24,7 +30,9 @@ export class YahooFinanceClient {
|
|||||||
|
|
||||||
async fetchCalendarEvents(ticker: string): Promise<any | null> {
|
async fetchCalendarEvents(ticker: string): Promise<any | null> {
|
||||||
try {
|
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;
|
return result.calendarEvents ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ export class FinanceController {
|
|||||||
private readonly advisor: PortfolioAdvisor,
|
private readonly advisor: PortfolioAdvisor,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private static normalizeYahoo(ticker: string): string {
|
|
||||||
return ticker.toUpperCase().replace(/\./g, '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
register(app: FastifyInstance): void {
|
register(app: FastifyInstance): void {
|
||||||
app.get('/api/finance/portfolio', this.portfolio.bind(this));
|
app.get('/api/finance/portfolio', this.portfolio.bind(this));
|
||||||
app.post('/api/finance/holdings', { schema: holdingSchema }, this.addHolding.bind(this));
|
app.post('/api/finance/holdings', { schema: holdingSchema }, this.addHolding.bind(this));
|
||||||
@@ -38,7 +34,7 @@ export class FinanceController {
|
|||||||
|
|
||||||
const screenable = holdings
|
const screenable = holdings
|
||||||
.filter((h) => (h.type ?? 'stock') !== 'crypto')
|
.filter((h) => (h.type ?? 'stock') !== 'crypto')
|
||||||
.map((h) => FinanceController.normalizeYahoo(h.ticker));
|
.map((h) => h.ticker.toUpperCase());
|
||||||
|
|
||||||
const results =
|
const results =
|
||||||
screenable.length > 0
|
screenable.length > 0
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||||
import { YahooFinanceClient } from '../clients/YahooFinanceClient';
|
import { YahooFinanceClient } from '../clients/YahooFinanceClient';
|
||||||
import { REGIME } from '../config/constants';
|
import { REGIME } from '../config/constants';
|
||||||
import type { MarketContext, Logger, BenchmarkProviderOptions } from '../types';
|
import type { MarketContext, Logger, BenchmarkProviderOptions } from '../types';
|
||||||
|
|
||||||
|
interface CacheFile {
|
||||||
|
data: MarketContext;
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class BenchmarkProvider {
|
export class BenchmarkProvider {
|
||||||
private static readonly TTL_MS = 60 * 60 * 1000;
|
private static readonly TTL_MS = 60 * 60 * 1000;
|
||||||
|
private static readonly CACHE_PATH = '.benchmark-cache.json';
|
||||||
|
|
||||||
private static readonly DEFAULTS: MarketContext = {
|
private static readonly DEFAULTS: MarketContext = {
|
||||||
sp500Price: 5000,
|
sp500Price: 5000,
|
||||||
@@ -32,10 +39,33 @@ export class BenchmarkProvider {
|
|||||||
private readonly client: YahooFinanceClient,
|
private readonly client: YahooFinanceClient,
|
||||||
{ logger }: BenchmarkProviderOptions = {},
|
{ logger }: BenchmarkProviderOptions = {},
|
||||||
) {
|
) {
|
||||||
this.cache = { data: null, expiresAt: 0 };
|
this.cache = this.loadDiskCache();
|
||||||
this.logger = logger ?? (console as unknown as Logger);
|
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<MarketContext> {
|
async getMarketContext(): Promise<MarketContext> {
|
||||||
if (this.cache.data && Date.now() < this.cache.expiresAt) return this.cache.data;
|
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;
|
return context;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.warn('Market data fetch failed, using defaults:', (err as Error).message);
|
this.logger.warn('Market data fetch failed, using defaults:', (err as Error).message);
|
||||||
|
|||||||
Reference in New Issue
Block a user