fix: IREN/ASTS/IRE stuck-pending + corridor feature

Fix stuck adapter_queue jobs (ASTS/IRE/IREN stuck pending forever):

1. fetchSpec early-return paths (cooldown checks) now update job status
   to 'backoff' with last_attempt set and 30s backoff_until, instead of
   returning without any status change. Prevents jobs from being
   re-processed every drain cycle indefinitely.

2. Wrap adapter.fetchOne() in 30s Promise.race timeout. A hung HTTP
   request no longer blocks the entire per-source promise chain
   forever, preventing all subsequent jobs for that source.

Also includes corridor/confluence feature, tiered quote schedules,
cache improvements, and HoldingsBookView refinements.
This commit is contained in:
Investor Flow Build
2026-08-11 22:25:17 -04:00
parent 24349a8b6d
commit 9b41890d33
28 changed files with 2576 additions and 184 deletions
+121 -14
View File
@@ -38,6 +38,9 @@ import { CONFLUENCE_SLOT_IDS } from '../confluence/confluenceSlots.ts';
import { ConfluenceRepository, rackFromSlots } from '../db/confluenceRepository.ts';
import { runSlotBacktest, signalHistoryToStats, type ConfluenceFireEvent } from '../confluence/confluenceBacktest.ts';
import { detectPictureChange } from '../confluence/confluenceRack.ts';
import { runCorridorBacktest, aggregateBacktest } from '../analysis/corridorBacktest.ts';
import { runConfluenceEvaluationCycle } from '../confluence/confluenceEngine.ts';
import { CorridorRepository } from '../db/corridorRepository.ts';
// ---------------------------------------------------------------------------
// X cookie credential helpers. Loads AES-256-GCM encrypted ct0/auth_token from
@@ -452,8 +455,8 @@ const marketRouter = router({
const { classifyRegime } = await import('../macro/MacroRegime.ts');
const symbol = input.symbol.toUpperCase();
try { await ctx.cache.ensureInDemand(symbol, 'equity'); } catch { /* ignore */ }
try { await ctx.cache.ensureInDemand(BENCHMARK_SYMBOL, 'etf'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched(symbol, 'equity'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched(BENCHMARK_SYMBOL, 'etf'); } catch { /* ignore */ }
const metaEntry = await ctx.cache.get<SymbolMeta>(`yfinance:symbol:${symbol}`);
const meta = metaEntry.value;
@@ -516,7 +519,7 @@ const marketRouter = router({
for (const s of symbolsToLoad) {
const kind = s === BENCHMARK_SYMBOL || MARKET_ROTATION_UNIVERSE.some((u) => u.symbol === s) ? 'etf' : 'equity';
try { await ctx.cache.ensureInDemand(s, kind); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched(s, kind); } catch { /* ignore */ }
}
const candleKeys = symbolsToLoad.map((s) => `yfinance:candles:${s}:1d`);
@@ -776,8 +779,8 @@ const marketRouter = router({
// SPY trend + VIX from cache/queue only (ADR-0009: no live Yahoo on request path).
let spyCandles: PriceCandle[] = [];
try {
try { await ctx.cache.ensureInDemand('SPY', 'etf'); } catch { /* ignore */ }
try { await ctx.cache.ensureInDemand('^VIX', 'index'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched('SPY', 'etf'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched('^VIX', 'index'); } catch { /* ignore */ }
const spyEntry = await ctx.cache.get<PriceCandle[]>('yfinance:candles:SPY:1d');
spyCandles = (spyEntry?.value ?? []) as PriceCandle[];
factors.spy1M = totalReturnPct(spyCandles, 30 * 86_400_000);
@@ -981,7 +984,7 @@ const marketRouter = router({
const symbols = [BENCHMARK_SYMBOL, ...MARKET_ROTATION_UNIVERSE.map((s) => s.symbol)];
for (const sym of symbols) {
try { await ctx.cache.ensureInDemand(sym, 'etf'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched(sym, 'etf'); } catch { /* ignore */ }
}
const keys = symbols.map((s) => `yfinance:candles:${s}:1d`);
@@ -1137,7 +1140,7 @@ const marketRouter = router({
const symbols = [BENCHMARK_SYMBOL, ...dedupedDefs.map((s) => s.symbol)];
for (const sym of symbols) {
try { await ctx.cache.ensureInDemand(sym, 'etf'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched(sym, 'etf'); } catch { /* ignore */ }
}
const keys = symbols.map((s) => `yfinance:candles:${s}:1d`);
@@ -1281,7 +1284,7 @@ const marketRouter = router({
.query(async ({ ctx, input }) => {
const { buildSeasonalitySnapshot, upcomingSimpleEvents } = await import('../analysis/seasonality.ts');
const symbol = (input?.symbol ?? 'SPY').toUpperCase();
try { await ctx.cache.ensureInDemand(symbol, symbol === 'SPY' ? 'etf' : 'equity'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched(symbol, symbol === 'SPY' ? 'etf' : 'equity'); } catch { /* ignore */ }
const entry = await ctx.cache.get<PriceCandle[]>(`yfinance:candles:${symbol}:1d`);
const candles = (entry?.value ?? []) as PriceCandle[];
const snapshot = buildSeasonalitySnapshot(
@@ -1429,7 +1432,7 @@ const marketRouter = router({
for (const sym of symbols) {
if (quoteMap.has(sym) && quoteMap.get(sym)!.price != null) continue;
if (sym.includes('.') || sym.length > 5) continue;
try { await ctx.cache.ensureInDemand(sym, 'equity'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched(sym, 'equity'); } catch { /* ignore */ }
}
}
@@ -1467,7 +1470,7 @@ const marketRouter = router({
ctx.db.prepare(
'INSERT INTO rotation_custom_symbols (id, owner_id, symbol, name, grp, created_at) VALUES (?,?,?,?,?,?)'
).run(crypto.randomUUID(), userId, input.symbol, input.name ?? input.symbol, input.group ?? 'Custom', new Date().toISOString());
try { await ctx.cache.ensureInDemand(input.symbol, 'etf'); } catch { /* ignore */ }
try { await ctx.cache.bumpToWatched(input.symbol, 'etf'); } catch { /* ignore */ }
return { added: true };
}),
@@ -3399,7 +3402,7 @@ const dealerStudyRouter = router({
);
// Ensure daily candles are in demand for later grading (ADR-0009).
try {
await ctx.cache.ensureInDemand(input.symbol.toUpperCase(), 'equity');
await ctx.cache.bumpToWatched(input.symbol.toUpperCase(), 'equity');
} catch { /* optional */ }
return { ok: true as const, id, loggedAt };
}),
@@ -3835,7 +3838,7 @@ const mentorLedgerRouter = router({
: row.logged_at;
try {
await ctx.cache.ensureInDemand(row.symbol, 'equity');
await ctx.cache.bumpToWatched(row.symbol, 'equity');
} catch { /* optional */ }
const candles = ctx.db.prepare(
@@ -5382,7 +5385,7 @@ const mirrorRouter = router({
// ─── Confluence Signal Engine (M22) ─────────────────────────────────────────
const confluenceRouter = router({
/** The read-only 34-slot catalog, grouped by family, with slot metadata. */
/** The read-only slot catalog, grouped by family, with slot metadata. */
slots: publicProcedure.query(async () => {
return {
slots: CONFLUENCE_SLOTS.map((s) => ({
@@ -5476,7 +5479,7 @@ const confluenceRouter = router({
id: z.string().min(1).max(80).optional(),
name: z.string().min(1).max(80),
description: z.string().max(300).optional(),
slotIds: z.array(z.string()).min(1).max(34),
slotIds: z.array(z.string()).min(1).max(CONFLUENCE_SLOT_IDS.length),
}))
.mutation(({ ctx, input }) => {
const repo = new ConfluenceRepository(ctx.db);
@@ -5498,6 +5501,110 @@ const confluenceRouter = router({
}
return rack;
}),
// -------------------------------------------------------------------------
// Price Corridor (M24): valuation-corridor snapshots + backtest ledger + link
// -------------------------------------------------------------------------
/** Latest valuation-corridor snapshot for a symbol, with SPY market context. */
corridorSnapshot: publicProcedure
.input(z.object({ symbol: z.string().min(1).max(12) }))
.query(({ ctx, input }) => {
const repo = new CorridorRepository(ctx.db);
const symbol = input.symbol.toUpperCase();
const snapshot = repo.latestSnapshot(symbol);
const spy = repo.latestSnapshot('SPY');
const series = repo.snapshotsForSymbol(symbol, 90);
const corridor = (snapshot?.corridor1yLow ?? null) !== null
? {
low: snapshot!.corridor1yLow,
high: snapshot!.corridor1yHigh,
median: snapshot!.corridor1yMedian,
currentPE: snapshot!.trailingPE ?? snapshot!.forwardPE,
fairValue1y: snapshot!.fairValue1y,
impliedUpside1y: snapshot!.impliedUpside1y,
pePercentile1y: snapshot!.pePercentile1y,
}
: null;
return {
symbol,
snapshot,
spy,
series,
corridor,
};
}),
/** Estuary user's corridor watchlist (tickers surfaced in the Corridor panel). */
corridorWatchlist: protectedProcedure
.query(({ ctx }) => {
const repo = new CorridorRepository(ctx.db);
return { symbols: repo.listWatchlist(ctx.userId!) };
}),
/** Add a ticker to the corridor watchlist. */
corridorWatchlistAdd: protectedProcedure
.input(z.object({ symbol: z.string().min(1).max(12).transform((s) => s.toUpperCase()) }))
.mutation(({ ctx, input }) => {
const repo = new CorridorRepository(ctx.db);
repo.addToWatchlist(ctx.userId!, input.symbol);
return { symbols: repo.listWatchlist(ctx.userId!) };
}),
/** Remove a ticker from the corridor watchlist. */
corridorWatchlistRemove: protectedProcedure
.input(z.object({ symbol: z.string().min(1).max(12).transform((s) => s.toUpperCase()) }))
.mutation(({ ctx, input }) => {
const repo = new CorridorRepository(ctx.db);
repo.removeFromWatchlist(ctx.userId!, input.symbol);
return { symbols: repo.listWatchlist(ctx.userId!) };
}),
/** Corridor-method backtest scorecard: @alojoh's rated names vs actual forward returns. */
corridorBacktest: publicProcedure
.query(({ ctx }) => {
const repo = new CorridorRepository(ctx.db);
const rows = repo.listBacktests();
return {
rows,
aggregate: aggregateBacktest(
rows.map((r) => ({
articleDate: r.articleDate,
articleId: r.articleId,
rankingType: r.rankingType as 'entry_1y' | 'entry_90d',
topSymbols: r.topSymbols,
bottomSymbols: r.bottomSymbols,
topAvgReturn: r.topAvgReturn,
bottomAvgReturn: r.bottomAvgReturn,
spread: r.spread,
isWin: r.isWin,
horizonDays: r.horizonDays,
resolvableCount: 0,
})),
),
};
}),
/** Replay the corridor-method backtest corpus against cached candles (offline; idempotent). */
corridorBacktestRun: publicProcedure
.mutation(async ({ ctx }) => {
const { runCorridorBacktest } = await import('../analysis/corridorBacktest.ts');
const result = await runCorridorBacktest(ctx.db, async (symbol) => {
const entry = await ctx.cache.get<PriceCandle[]>(`yfinance:candles:${symbol.toUpperCase()}:1d`);
return (entry?.value ?? []) as PriceCandle[];
});
return { grades: result.grades.length, resolved: result.aggregate.resolved, aggregate: result.aggregate };
}),
/** Force a confluence evaluation cycle for the current symbol now (debug/admin). */
runEvaluationNow: protectedProcedure
.input(z.object({ symbol: z.string().min(1).max(12).optional() }))
.mutation(async ({ ctx, input }) => {
const summary = await runConfluenceEvaluationCycle(ctx.db, ctx.cache, {
symbols: input?.symbol ? [input.symbol] : undefined,
});
return summary;
}),
});
export const appRouter = router({