feat(settings): add settings route and API updates\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This commit is contained in:
2026-05-16 20:19:35 +02:00
parent 9b63d981b0
commit 0ee89cf052
38 changed files with 1426 additions and 562 deletions
+47 -15
View File
@@ -1,11 +1,18 @@
import { fetchLatestBar } from "../../lib/alpacaClient";
import alpacaService from "../../lib/alpacaClient";
export async function loader({ request }: { request: Request }) {
const url = new URL(request.url);
const ticker = (url.searchParams.get("ticker") || "").toUpperCase();
if (!ticker) return new Response("ticker required", { status: 400 });
const timeframe = url.searchParams.get("timeframe") || "1Min"; // default to 1Min bars for live price
const mode = url.searchParams.get("mode") === "live" ? 'live' : 'paper';
function mapToAlpacaTimeframe(tf: string) {
switch (tf) {
case "1H": return "1Hour";
case "1D": return "1Day";
default: return tf;
}
}
const alpacaTimeframe = mapToAlpacaTimeframe(timeframe);
const headers = new Headers({
"Content-Type": "text/event-stream",
@@ -13,9 +20,8 @@ export async function loader({ request }: { request: Request }) {
Connection: "keep-alive",
});
// Create a ReadableStream that polls latest bar every second and pushes SSE
// Create a ReadableStream that polls latest bar with adaptive backoff and SSE
let closed = false;
let interval: any;
const stream = new ReadableStream({
start(controller) {
// helper to push SSE event
@@ -31,28 +37,54 @@ export async function loader({ request }: { request: Request }) {
// initial ping
pushEvent({ event: "connected", ticker, timeframe });
interval = setInterval(async () => {
const baseDelay = 5000; // start with 5s between Alpaca calls
const maxDelay = 60000; // cap backoff at 60s
let delay = baseDelay;
let lastBarId: string | number | null = null;
async function poll() {
if (closed) return;
try {
const last = await fetchLatestBar(ticker, timeframe, mode);
const last = await alpacaService.fetchLatestBar(ticker, alpacaTimeframe);
const price = last ? (last.ClosePrice ?? last.c ?? null) : null;
// create a dedupe id from available fields
const barId = last ? (last.T ?? last.t ?? last.Timestamp ?? last.ClosePrice ?? last.c ?? null) : null;
if (price != null) {
pushEvent({ price, ts: Date.now(), timeframe });
if (barId == null || barId !== lastBarId) {
lastBarId = barId;
pushEvent({ price, ts: Date.now(), timeframe });
}
} else {
pushEvent({ error: "no_bar", ts: Date.now() });
}
} catch (err) {
console.error("price-stream: error fetching latest bar", err);
pushEvent({ error: String(err), ts: Date.now() });
// keep trying
}
}, 1000);
// no-op here; cleanup handled in cancel
// on success, reset backoff
delay = baseDelay;
} catch (err: any) {
const msg = String(err?.message ?? err ?? "error");
console.error("price-stream: error fetching latest bar", msg);
pushEvent({ error: msg, ts: Date.now() });
// apply exponential backoff on rate limit errors
if (/429|too many requests/i.test(msg)) {
delay = Math.min(delay * 2, maxDelay);
console.warn(`price-stream: rate limited, backing off to ${delay}ms`);
} else {
// mild backoff for other errors
delay = Math.min(Math.floor(delay * 1.5), maxDelay);
}
}
if (!closed) setTimeout(poll, delay);
}
// start polling immediately
setTimeout(poll, 0);
},
cancel() {
closed = true;
clearInterval(interval);
},
});