feat: add autonomous paper-trading and calibration pipeline
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
const https = require("https");
|
||||
|
||||
// fetches daily OHLC from yahoo finance v8 chart api
|
||||
// no api key needed but rate limited so we cache aggressively
|
||||
async function fetchYahooHistory(ticker, range = "6mo") {
|
||||
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?range=${range}&interval=1d`;
|
||||
|
||||
const body = await httpGet(url, {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; duriin-intelligence/1.0)",
|
||||
});
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(body);
|
||||
} catch (_) {
|
||||
throw new Error(`yahoo response not JSON: ${body.slice(0, 200)}`);
|
||||
}
|
||||
|
||||
const result = parsed?.chart?.result?.[0];
|
||||
if (!result) return null;
|
||||
|
||||
const ts = result.timestamp || [];
|
||||
const closes = result.indicators?.quote?.[0]?.close || [];
|
||||
|
||||
const out = [];
|
||||
for (let i = 0; i < ts.length; i++) {
|
||||
if (closes[i] == null) continue;
|
||||
out.push({
|
||||
date: new Date(ts[i] * 1000).toISOString().slice(0, 10),
|
||||
close: closes[i],
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
function nearestPriceOnOrBefore(history, dateStr) {
|
||||
// history is sorted ascending by date
|
||||
let last = null;
|
||||
for (const row of history) {
|
||||
if (row.date <= dateStr) last = row;
|
||||
else break;
|
||||
}
|
||||
return last ? last.close : null;
|
||||
}
|
||||
|
||||
|
||||
function computeStdev(values) {
|
||||
if (values.length < 2) return 0;
|
||||
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
||||
const sq = values.reduce((acc, v) => acc + (v - mean) ** 2, 0);
|
||||
return Math.sqrt(sq / (values.length - 1));
|
||||
}
|
||||
|
||||
|
||||
function computeReturns(history) {
|
||||
const rets = [];
|
||||
for (let i = 1; i < history.length; i++) {
|
||||
const prev = history[i - 1].close;
|
||||
const cur = history[i].close;
|
||||
if (prev > 0) rets.push((cur - prev) / prev);
|
||||
}
|
||||
return rets;
|
||||
}
|
||||
|
||||
|
||||
// returns { price, price_30d_ago, price_90d_ago, vol_30d } as_of a given date
|
||||
async function getPriceContext(intelligenceDb, ticker, asOfDate) {
|
||||
if (!ticker || !asOfDate) return null;
|
||||
|
||||
// skip private/synthetic tickers — yahoo wont know them
|
||||
if (/^(OPENAI|ANTHROPIC|XAI|HUAWEI|BYTEDANCE|DEEPSEEK|MISTRAL|COHERE|GROQ|SCALEAI|MCKINSEY|DELOITTE|STABILITY|INFLECTION|SPACEX|BLUEORIGIN)$/i.test(ticker)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheRow = intelligenceDb.prepare(
|
||||
"SELECT price, price_30d_ago, price_90d_ago, vol_30d FROM price_snapshots WHERE ticker = ? AND as_of = ?"
|
||||
).get(ticker, asOfDate);
|
||||
|
||||
if (cacheRow) return cacheRow;
|
||||
|
||||
let history;
|
||||
try {
|
||||
history = await fetchYahooHistory(ticker, "6mo");
|
||||
} catch (err) {
|
||||
// dont blow up the worker on a single bad ticker
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!history || history.length === 0) return null;
|
||||
|
||||
const price = nearestPriceOnOrBefore(history, asOfDate);
|
||||
if (price == null) return null;
|
||||
|
||||
const date30 = new Date(asOfDate);
|
||||
date30.setDate(date30.getDate() - 30);
|
||||
const price30 = nearestPriceOnOrBefore(history, date30.toISOString().slice(0, 10));
|
||||
|
||||
const date90 = new Date(asOfDate);
|
||||
date90.setDate(date90.getDate() - 90);
|
||||
const price90 = nearestPriceOnOrBefore(history, date90.toISOString().slice(0, 10));
|
||||
|
||||
// 30-day annualized vol from daily returns
|
||||
const recent = history.filter(h => h.date <= asOfDate).slice(-30);
|
||||
const vol30 = computeStdev(computeReturns(recent)) * Math.sqrt(252);
|
||||
|
||||
const snapshot = {
|
||||
price,
|
||||
price_30d_ago: price30,
|
||||
price_90d_ago: price90,
|
||||
vol_30d: vol30,
|
||||
};
|
||||
|
||||
try {
|
||||
intelligenceDb.prepare(
|
||||
"INSERT OR REPLACE INTO price_snapshots (ticker, as_of, price, price_30d_ago, price_90d_ago, vol_30d) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
).run(ticker, asOfDate, snapshot.price, snapshot.price_30d_ago, snapshot.price_90d_ago, snapshot.vol_30d);
|
||||
} catch (_) {}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
|
||||
// formats the snapshot for inclusion in the LLM prompt
|
||||
function formatPriceContext(snapshot, ticker) {
|
||||
if (!snapshot || snapshot.price == null) return null;
|
||||
|
||||
const lines = [`${ticker} price as of event: $${snapshot.price.toFixed(2)}`];
|
||||
|
||||
if (snapshot.price_30d_ago) {
|
||||
const ret30 = (snapshot.price - snapshot.price_30d_ago) / snapshot.price_30d_ago * 100;
|
||||
lines.push(`30-day return: ${ret30 >= 0 ? "+" : ""}${ret30.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
if (snapshot.price_90d_ago) {
|
||||
const ret90 = (snapshot.price - snapshot.price_90d_ago) / snapshot.price_90d_ago * 100;
|
||||
lines.push(`90-day return: ${ret90 >= 0 ? "+" : ""}${ret90.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
if (snapshot.vol_30d) {
|
||||
lines.push(`30-day annualized volatility: ${(snapshot.vol_30d * 100).toFixed(1)}%`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
|
||||
function httpGet(url, headers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const req = https.request({
|
||||
hostname: u.hostname,
|
||||
path: u.pathname + u.search,
|
||||
method: "GET",
|
||||
headers,
|
||||
}, (res) => {
|
||||
let data = "";
|
||||
res.on("data", chunk => data += chunk);
|
||||
res.on("end", () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) resolve(data);
|
||||
else reject(new Error(`yahoo ${res.statusCode}: ${data.slice(0, 200)}`));
|
||||
});
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = { getPriceContext, formatPriceContext };
|
||||
Reference in New Issue
Block a user