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:
@@ -8,6 +8,7 @@ import { generateBackupCodes, hashBackupCode } from '../auth/backup-codes.ts';
|
||||
import { buildAuthorizeUrl, generateState, exchangeCode, type OAuthProvider } from '../auth/oauth.ts';
|
||||
import { STARTER_WATCHLIST, defaultDrawdownTolerancePct, defaultRiskTolerance, ONBOARDING_DISCLAIMER, type Complexity } from '../onboarding/starter.ts';
|
||||
import type { Quote, PriceCandle, SymbolMeta } from '../cache/CacheRepository.ts';
|
||||
import { emaFromCandles, rsi as rsiFn, relativeVolume } from '../analysis/indicators.ts';
|
||||
|
||||
const t = initTRPC.context<Context>().create();
|
||||
const router = t.router;
|
||||
@@ -171,6 +172,42 @@ const marketRouter = router({
|
||||
stale: { quote: stale(k.quote), candles: stale(k.candles), sector: stale(k.sector) },
|
||||
};
|
||||
}),
|
||||
candles: publicProcedure
|
||||
.input(z.object({ symbol: z.string().min(1), timeframe: z.enum(['1d', '1wk']).default('1d') }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const symbol = input.symbol.toUpperCase();
|
||||
const key = `yfinance:candles:${symbol}:${input.timeframe}`;
|
||||
const entry = await ctx.cache.get<PriceCandle[]>(key);
|
||||
return { symbol, timeframe: input.timeframe, candles: (entry.value ?? []), isStale: entry.isStale };
|
||||
}),
|
||||
indicators: publicProcedure
|
||||
.input(z.object({
|
||||
symbol: z.string().min(1),
|
||||
timeframe: z.enum(['1d', '1wk']).default('1d'),
|
||||
periods: z.object({
|
||||
ema: z.array(z.number().int()).default([9, 21, 50, 200]),
|
||||
rsi: z.number().int().default(14),
|
||||
relvol: z.number().int().default(20),
|
||||
}).default(() => ({ ema: [9, 21, 50, 200], rsi: 14, relvol: 20 } as const)),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const symbol = input.symbol.toUpperCase();
|
||||
const key = `yfinance:candles:${symbol}:${input.timeframe}`;
|
||||
const entry = await ctx.cache.get<PriceCandle[]>(key);
|
||||
const candles = entry.value ?? [];
|
||||
const periods = input.periods ?? { ema: [9, 21, 50, 200], rsi: 14, relvol: 20 };
|
||||
const closes = candles.map((c) => c.c);
|
||||
const volumes = candles.map((c) => c.v);
|
||||
const emaObj: Record<string, (number | undefined)[]> = {};
|
||||
for (const p of periods.ema) {
|
||||
emaObj[String(p)] = emaFromCandles(candles, 'adjClose', p);
|
||||
}
|
||||
return {
|
||||
ema: emaObj,
|
||||
rsi: rsiFn(closes, periods.rsi),
|
||||
relativeVolume: relativeVolume(volumes, periods.relvol),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
export const appRouter = router({ auth: authRouter, onboarding: onboardingRouter, market: marketRouter });
|
||||
|
||||
Reference in New Issue
Block a user