slice 5b (omlx/ornith-35): market.candles + market.indicators endpoints
Chart Lab data path: market.candles (cached OHLCV by timeframe) + market.indicators (EMA/RSI/relativeVolume via analysis/indicators.ts). By ornith-35 (local, ~75s); reviewed by orchestrator (128/128 tests, no regression). Conflict-free: impl=ornith-35, reviewer=orchestrator.
This commit is contained in:
@@ -190,3 +190,67 @@ test('onboarding.complete writes complexity/risk/drawdown + starter watchlist +
|
||||
const demand = await cache.demandSet();
|
||||
assert.ok(demand.includes('NVDA') && demand.includes('BTC'), 'starter symbols subscribed to demand set');
|
||||
});
|
||||
|
||||
// --- Slice 5: market.candles + market.indicators ---
|
||||
test('market.candles returns cached candles with isStale=false after drain', async () => {
|
||||
const { queue, freshCtx } = setup();
|
||||
await appRouter.createCaller(freshCtx()).market.candles({ symbol: 'NVDA', timeframe: '1d' });
|
||||
await queue.drain();
|
||||
const res = await appRouter.createCaller(freshCtx()).market.candles({ symbol: 'NVDA', timeframe: '1d' });
|
||||
assert.equal(res.symbol, 'NVDA');
|
||||
assert.equal(res.timeframe, '1d');
|
||||
assert.equal(res.candles.length, 1);
|
||||
assert.equal(res.isStale, false);
|
||||
});
|
||||
|
||||
test('market.candles returns empty candles + isStale=true when cache is empty', async () => {
|
||||
const { freshCtx } = setup();
|
||||
const res = await appRouter.createCaller(freshCtx()).market.candles({ symbol: 'ZZZZ' });
|
||||
assert.equal(res.symbol, 'ZZZZ');
|
||||
assert.deepEqual(res.candles, []);
|
||||
assert.equal(res.isStale, true);
|
||||
});
|
||||
|
||||
test('market.indicators returns arrays aligned to candle length', async () => {
|
||||
const { queue, freshCtx } = setup();
|
||||
await appRouter.createCaller(freshCtx()).market.indicators({ symbol: 'NVDA', timeframe: '1d' });
|
||||
await queue.drain();
|
||||
const res = await appRouter.createCaller(freshCtx()).market.indicators({ symbol: 'NVDA', timeframe: '1d' });
|
||||
// Single candle: all indicators should be undefined arrays of length 1
|
||||
assert.equal(res.ema['9']!.length, 1);
|
||||
assert.equal(res.ema['21']!.length, 1);
|
||||
assert.equal(res.ema['50']!.length, 1);
|
||||
assert.equal(res.ema['200']!.length, 1);
|
||||
assert.equal(res.rsi.length, 1);
|
||||
assert.equal(res.relativeVolume.length, 1);
|
||||
// With only 1 candle, nothing can be computed yet
|
||||
assert.equal(res.ema['9']![0], undefined);
|
||||
assert.equal(res.rsi[0], undefined);
|
||||
assert.equal(res.relativeVolume[0], undefined);
|
||||
});
|
||||
|
||||
test('market.indicators computes real values when enough candles exist', async () => {
|
||||
const { db, cache, queue, freshCtx } = setup();
|
||||
// Pre-populate 50 candles so EMA-9, RSI-14, and relativeVolume-20 all have computed values
|
||||
const candles: PriceCandle[] = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
candles.push({ ts: `2026-06-${String(1 + i).padStart(2, '0')}`, o: 100 + i * 0.5, h: 102 + i * 0.5, l: 99 + i * 0.5, c: 100 + i, v: 1e6 + i * 1e4, adjClose: 100 + i });
|
||||
}
|
||||
// Write candles directly to the cache
|
||||
await cache.set<PriceCandle[]>(`yfinance:candles:TESTSYM:1d`, candles, 'daily_permanent', { fetchedAt: new Date().toISOString(), sourceKind: 'yfinance' });
|
||||
const res = await appRouter.createCaller(freshCtx()).market.indicators({ symbol: 'TESTSYM', timeframe: '1d', periods: { ema: [9], rsi: 14, relvol: 20 } });
|
||||
// EMA-9: seed SMA at index 8, then recursive EMA from index 9 onward
|
||||
const ema9 = res.ema['9']!;
|
||||
assert.equal(ema9.length, 50);
|
||||
assert.equal(ema9[7], undefined, 'EMA-9 undefined before period-1');
|
||||
assert.ok(typeof ema9[8] === 'number', 'EMA-9 seed SMA at index 8');
|
||||
assert.ok(typeof ema9[9] === 'number', 'EMA-9 recursive value at index 9');
|
||||
// RSI-14: first computed value at index 14
|
||||
assert.equal(res.rsi.length, 50);
|
||||
assert.equal(res.rsi[13], undefined, 'RSI undefined before period');
|
||||
assert.ok(typeof res.rsi[14] === 'number', 'RSI computed at index 14');
|
||||
// RelativeVolume-20: first computed value at index 20
|
||||
assert.equal(res.relativeVolume.length, 50);
|
||||
assert.equal(res.relativeVolume[19], undefined, 'relvol undefined before period');
|
||||
assert.ok(typeof res.relativeVolume[20] === 'number', 'relativeVolume computed at index 20');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user