retro / static /engine.js
sankalphs's picture
feat: browser-local engine, Zerodha dashboard, historical events, chatbot, per-user isolation
f316f5a
Raw
History Blame Contribute Delete
9.35 kB
// Retro Alpha — Browser-local game engine.
// Mirrors engine.py: random walk, price shock, agent pressure, trades,
// cost basis, history. Runs 100% in the browser; server is only for LLM.
(function () {
const ASSETS = ["cash", "fd", "gov_bonds", "nifty_50", "nifty_it", "real_estate", "crypto", "gold"];
const REGIMES = [
"bull_market", "bear_market", "market_crash", "recovery", "high_inflation",
"rate_hike", "rate_cut", "election_year", "monsoon_shock", "fii_exit",
"tech_boom", "real_estate_boom", "crypto_frenzy", "gold_rush", "stagnation"
];
const ASSET_PARAMS = {
cash: { mean: 0.00, vol: 0.01 },
fd: { mean: 0.065, vol: 0.005 },
gov_bonds: { mean: 0.07, vol: 0.06 },
nifty_50: { mean: 0.12, vol: 0.16 },
nifty_it: { mean: 0.15, vol: 0.28 },
real_estate:{ mean: 0.10, vol: 0.18 },
crypto: { mean: 0.20, vol: 0.65 },
gold: { mean: 0.08, vol: 0.14 },
};
const CORRELATION = 0.3;
const STARTING_YEAR = 1994;
const STARTING_MONTH = 4;
const GAME_LENGTH_MONTHS = 120;
const WIN_THRESHOLD = 2_000_000;
const STARTING_CASH = 1_000_000;
// --- helpers --------------------------------------------------------
function gaussian() {
// Box-Muller standard normal
const u1 = Math.random() || 1e-12;
const u2 = Math.random();
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
function makePrices() {
const p = {};
for (const a of ASSETS) p[a] = 1.0;
return p;
}
// --- game state -----------------------------------------------------
function newGame() {
return {
year: STARTING_YEAR,
month: STARTING_MONTH,
months_elapsed: 0,
prices: makePrices(),
portfolio: Object.fromEntries(ASSETS.map((a) => [a, 0.0])),
cost_basis: Object.fromEntries(ASSETS.map((a) => [a, 0.0])),
cash_balance: STARTING_CASH,
news: {},
agent_actions: [],
ledger: [],
game_over: false,
won: false,
last_event: {},
value_history: [STARTING_CASH],
price_history: [],
};
}
function totalValue(s) {
let v = s.cash_balance;
for (const a of ASSETS) {
if (a === "cash") continue;
v += s.portfolio[a] * s.prices[a];
}
return v;
}
function investedValue(s) {
let v = 0;
for (const a of ASSETS) {
if (a === "cash") continue;
v += s.portfolio[a] * s.prices[a];
}
return v;
}
function totalPnl(s) {
let pnl = 0;
for (const a of ASSETS) {
const current = s.portfolio[a] * s.prices[a];
pnl += current - s.cost_basis[a];
}
return pnl;
}
// --- price dynamics -------------------------------------------------
function priceShock(s, impact) {
for (const a of ASSETS) {
if (a === "cash") continue;
if (impact[a] !== undefined) {
s.prices[a] = s.prices[a] * (1 + Number(impact[a]));
}
}
}
function randomWalk(s) {
const tradable = ASSETS.filter((a) => a !== "cash");
const n = tradable.length;
const shocks = tradable.map(() => gaussian());
const meanShock = shocks.reduce((a, b) => a + b, 0) / n;
const correlated = shocks.map((z) => meanShock * CORRELATION + z * Math.sqrt(1 - CORRELATION * CORRELATION));
for (let i = 0; i < n; i++) {
const asset = tradable[i];
const p = ASSET_PARAMS[asset];
const monthlyMean = p.mean / 12;
const monthlyVol = p.vol / Math.sqrt(12);
const ret = monthlyMean + monthlyVol * correlated[i];
s.prices[asset] = s.prices[asset] * (1 + ret);
}
}
function applyAgentTrades(s, agentActions) {
const pressure = Object.fromEntries(ASSETS.map((a) => [a, 0.0]));
for (const action of agentActions || []) {
for (const item of action.actions || []) {
const asset = item.asset;
if (!(asset in pressure)) continue;
const amt = Number(item.amount_pct || 0) * (item.action === "buy" ? 1 : -1);
pressure[asset] += amt;
}
}
for (const a of ASSETS) {
if (a === "cash") continue;
s.prices[a] = s.prices[a] * (1 + pressure[a] * 0.03);
}
}
// --- player trades --------------------------------------------------
function executePlayerTrade(s, asset, action, amountPct) {
if (!(asset in s.prices)) {
throw new Error("Unknown asset: " + asset);
}
const total = totalValue(s);
let tradeValue = total * amountPct;
if (action === "buy") {
tradeValue = Math.min(tradeValue, s.cash_balance);
if (tradeValue <= 0) return;
const price = s.prices[asset];
const shares = tradeValue / price;
s.cash_balance -= tradeValue;
s.portfolio[asset] += shares;
s.cost_basis[asset] += tradeValue;
} else if (action === "sell") {
const price = s.prices[asset];
const currentValue = s.portfolio[asset] * price;
const sellValue = Math.min(tradeValue, currentValue);
if (sellValue <= 0) return;
const shares = sellValue / price;
// Reduce cost basis proportionally (average-cost method)
if (s.portfolio[asset] > 0) {
const fractionSold = shares / s.portfolio[asset];
s.cost_basis[asset] = Math.max(0, s.cost_basis[asset] * (1 - fractionSold));
}
s.portfolio[asset] -= shares;
s.cash_balance += sellValue;
}
s.ledger.push({
month: s.month, year: s.year, asset, action,
amount_pct: amountPct, value: tradeValue,
});
}
// --- advance month --------------------------------------------------
function advanceMonth(s, news, agentActions, event) {
if (s.game_over) return;
s.months_elapsed += 1;
s.month += 1;
if (s.month > 12) { s.month = 1; s.year += 1; }
s.news = news || {};
s.agent_actions = agentActions || [];
s.last_event = event || {};
if (event && event.impact) priceShock(s, event.impact);
applyAgentTrades(s, agentActions);
randomWalk(s);
s.value_history.push(totalValue(s));
if (s.value_history.length > 240) {
s.value_history = s.value_history.slice(-240);
}
const snap = {};
for (const a of ASSETS) snap[a] = s.prices[a];
s.price_history.push(snap);
if (s.price_history.length > 240) {
s.price_history = s.price_history.slice(-240);
}
if (s.months_elapsed >= GAME_LENGTH_MONTHS) {
s.game_over = true;
s.won = totalValue(s) >= WIN_THRESHOLD;
}
}
// --- local NPC agents (deterministic, no LLM needed) ---------------
function localAgentDecide(persona, state, event) {
const regime = (event && event.regime) || "stagnation";
const crashy = ["market_crash", "bear_market", "fii_exit", "high_inflation"].includes(regime);
const boomy = ["bull_market", "tech_boom", "recovery", "real_estate_boom"].includes(regime);
let asset, action, amountPct, reason, sentiment;
if (persona === "whale") {
if (crashy) {
asset = "gov_bonds"; action = "buy"; amountPct = 0.15;
reason = "Flight to safety during the " + regime.replace(/_/g, " ");
sentiment = "cautious";
} else if (boomy) {
asset = "nifty_50"; action = "buy"; amountPct = 0.10;
reason = "Risk-on into a " + regime.replace(/_/g, " ");
sentiment = "bullish";
} else {
asset = "fd"; action = "buy"; amountPct = 0.05;
reason = "Park cash, wait for a clearer setup";
sentiment = "neutral";
}
} else if (persona === "retail") {
if (crashy) {
asset = "nifty_it"; action = "sell"; amountPct = 0.20;
reason = "Panic selling into the " + regime.replace(/_/g, " ");
sentiment = "panic";
} else if (boomy) {
asset = "nifty_50"; action = "buy"; amountPct = 0.10;
reason = "FOMO is real";
sentiment = "bullish";
} else {
asset = "gold"; action = "buy"; amountPct = 0.05;
reason = "Safe haven while I figure this out";
sentiment = "neutral";
}
} else { // permabull
if (regime === "crypto_frenzy" || crashy) {
asset = "crypto"; action = "buy"; amountPct = 0.20;
reason = "Buy the dip. Crypto only goes up.";
sentiment = "bullish";
} else {
asset = "crypto"; action = "buy"; amountPct = 0.05;
reason = "DCA forever";
sentiment = "bullish";
}
}
return {
agent: persona,
actions: [{ asset, action, amount_pct: amountPct, reason }],
sentiment,
};
}
function allLocalAgentsDecide(state, event) {
return ["whale", "retail", "permabull"].map((p) => localAgentDecide(p, state, event));
}
// --- public API -----------------------------------------------------
window.RetroEngine = {
ASSETS, REGIMES, ASSET_PARAMS, ASSET_DISPLAY_NAMES: {
cash: "Cash", fd: "FD", gov_bonds: "Gov Bonds", nifty_50: "Nifty 50",
nifty_it: "Nifty IT", real_estate: "Real Estate", crypto: "Crypto", gold: "Gold",
},
TRADABLE_KEYS: ["fd", "gov_bonds", "nifty_50", "nifty_it", "real_estate", "crypto", "gold"],
STARTING_YEAR, STARTING_MONTH, GAME_LENGTH_MONTHS, WIN_THRESHOLD, STARTING_CASH,
newGame, totalValue, investedValue, totalPnl,
priceShock, randomWalk, applyAgentTrades,
executePlayerTrade, advanceMonth,
localAgentDecide, allLocalAgentsDecide,
};
})();