76 lines
2.6 KiB
JavaScript
76 lines
2.6 KiB
JavaScript
// Investments: nine societies in three risk tiers. Every `intervalSeconds` each
|
|
// price moves by a random percentage within the tier's swing (low ±5 %, mid
|
|
// ±15 %, high ±50 %), then drifts back towards its fair value (more weakly for the wilder tiers), and
|
|
// holdings pay a small dividend. Buying and selling carries a small fee.
|
|
|
|
import { clamp } from './formulas.js';
|
|
|
|
export function holdingValue(state, id) {
|
|
return state.invest.holdings[id] * state.invest.prices[id];
|
|
}
|
|
|
|
export function portfolio(state, ctx) {
|
|
let value = 0;
|
|
let basis = 0;
|
|
for (const s of ctx.investments.societies) {
|
|
value += holdingValue(state, s.id);
|
|
basis += state.invest.basis[s.id];
|
|
}
|
|
return { value, basis, profit: value - basis };
|
|
}
|
|
|
|
function repriceAll(state, ctx, rng, dividendMultiplier) {
|
|
const cfg = ctx.investments;
|
|
const inv = state.invest;
|
|
for (const s of cfg.societies) {
|
|
const tier = cfg.tiers[s.tier];
|
|
const before = inv.prices[s.id];
|
|
let price = before * (1 + (rng() * 2 - 1) * tier.swing);
|
|
price += (s.basePrice - price) * tier.reversion;
|
|
price = clamp(price, s.basePrice * cfg.minRatio, s.basePrice * cfg.maxRatio);
|
|
inv.prices[s.id] = price;
|
|
inv.change[s.id] = price / before - 1;
|
|
state.funds += inv.holdings[s.id] * price * tier.dividend * dividendMultiplier;
|
|
}
|
|
}
|
|
|
|
export function stepInvestments(state, ctx, dt, rng, mods) {
|
|
state.investTimer += dt;
|
|
while (state.investTimer >= ctx.investments.intervalSeconds) {
|
|
state.investTimer -= ctx.investments.intervalSeconds;
|
|
repriceAll(state, ctx, rng, mods.dividend);
|
|
}
|
|
}
|
|
|
|
/** Spend `fraction` of the current funds on society `id`. */
|
|
export function buyShares(state, ctx, id, fraction) {
|
|
if (!ctx.societiesById.has(id)) return false;
|
|
const spend = state.funds * clamp(fraction, 0, 1);
|
|
if (!(spend >= 0.01)) return false;
|
|
state.funds -= spend;
|
|
state.invest.holdings[id] += (spend * (1 - ctx.investments.fee)) / state.invest.prices[id];
|
|
state.invest.basis[id] += spend;
|
|
return true;
|
|
}
|
|
|
|
/** Sell `fraction` of what is held in society `id`. */
|
|
export function sellShares(state, ctx, id, fraction) {
|
|
if (!ctx.societiesById.has(id)) return false;
|
|
const inv = state.invest;
|
|
const share = clamp(fraction, 0, 1);
|
|
if (!(inv.holdings[id] > 0) || share <= 0) return false;
|
|
|
|
const proceeds = inv.holdings[id] * share * inv.prices[id] * (1 - ctx.investments.fee);
|
|
const basisPart = inv.basis[id] * share;
|
|
state.funds += proceeds;
|
|
state.stats.investProfit += proceeds - basisPart;
|
|
if (share >= 1) {
|
|
inv.holdings[id] = 0;
|
|
inv.basis[id] = 0;
|
|
} else {
|
|
inv.holdings[id] -= inv.holdings[id] * share;
|
|
inv.basis[id] -= basisPart;
|
|
}
|
|
return true;
|
|
}
|