{"record":{"id":"744f925b6a56f5ce","repo":"virattt/ai-hedge-fund","slug":"spec-name-equity-is-equity-before-2f-as-of","errorCode":null,"errorMessage":"{spec.name}: equity is {equity_before:.2f} as of {as_of} — cannot size positions against a non-positive book","messagePattern":"(.+?): equity is (.+?) as of (.+?) — cannot size positions against a non-positive book","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"hedge_fund/pipeline/run_cycle.py","lineNumber":75,"sourceCode":"\n    The universe is an argument, not a mandate field: a fund is its desk —\n    strategies, staff, risk, capital — and can be pointed at any names. What\n    it was asked to trade this tick is recorded on the returned CycleRecord.\n    \"\"\"\n    spec = fund.spec\n    universe = normalize_universe(universe)\n    held = broker.positions()\n\n    marks, skipped = _mark_prices(\n        sorted(set(universe) | set(held)), as_of, held, data_client,\n    )\n\n    cash_before = broker.cash()\n    equity_before = cash_before + sum(\n        p.shares * marks[t] for t, p in held.items()\n    )\n    if equity_before <= 0:\n        raise ValueError(\n            f\"{spec.name}: equity is {equity_before:.2f} as of {as_of} — \"\n            \"cannot size positions against a non-positive book\"\n        )\n\n    tradeable = [t for t in universe if t in marks]\n\n    # Each strategy runs its own analysts and blends its own sleeve; the fund\n    # nets the sleeves by capital slice. A persona staffed into two strategies\n    # is asked twice, but the second ask is a prompt-cache hit, not spend.\n    total_slice = sum(s.weight for s, _ in fund.strategies)\n    strategy_records: list[StrategyRecord] = []\n    netted: dict[str, float] = {t: 0.0 for t in tradeable}\n    for strategy, staff in fund.strategies:\n        signals: list[Signal] = []\n        for ticker in tradeable:\n            for model in staff:\n                signals.append(model.predict(ticker, as_of, data_client))\n        blend = blend_signals(","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/virattt/ai-hedge-fund/blob/eff8a7320fcf0b473b135690fa1a5b0d9b022a83/hedge_fund/pipeline/run_cycle.py#L57-L93","documentation":"Raised in run_cycle (hedge_fund/pipeline/run_cycle.py:75) when the fund's equity (cash + marked value of held positions) is zero or negative at the start of a cycle. Position sizing scales targets against the current book, so a non-positive book makes sizing mathematically undefined — the run stops loudly instead of producing nonsense leverage.","triggerScenarios":"A SimBroker that has already lost everything (equity hit <= 0 through cumulative losses or fees in a long backtest); a negative cash state from a bug in fill simulation; a spec.capital of 0 combined with no positions. The check runs every cycle before strategy evaluation, using marks from _mark_prices for held tickers.","commonSituations":"High-leverage mandates bleeding to ruin mid-backtest (risk limits set too loose); a fill/fee model bug driving cash negative; spec.capital misconfigured as 0; short positions marked against the book across a crash.","solutions":["Inspect the CycleRecords leading up to the failure: the equity trajectory in prior cycles shows whether this is legitimate ruin or a marking/fill bug.","If it's genuine ruin, tighten the mandate's risk limits (per-position caps, gross exposure) or increase spec.capital and re-run.","If cash goes negative implausibly fast, audit broker fills/fees (SimBroker) and the marks in _mark_prices for stale or wrong prices.","Start the backtest from a date where the book is positive, or reset the broker before the run."],"exampleFix":"# before\n# mandate.yaml\ncapital: 10000\nrisk: {max_gross: 5.0}   # 5x leverage -> equity hits 0 mid-backtest -> ValueError\n\n# after\ncapital: 100000\nrisk: {max_gross: 1.5}","handlingStrategy":"validation","validationCode":"def book_is_positive(broker, marks: dict[str, float]) -> bool:\n    \"\"\"Equity check identical to run_cycle's, runnable before the call.\"\"\"\n    held = broker.positions()\n    equity = broker.cash() + sum(p.shares * marks[t] for t, p in held.items())\n    return equity > 0","typeGuard":"def is_ruined(equity: float) -> bool:\n    return not (equity > 0)  # True for 0, negative, and NaN","tryCatchPattern":"from datetime import date, timedelta\n\nlook = (date.fromisoformat(as_of) - timedelta(days=7)).isoformat()\nmarks, _ = _mark_like_prices(client, sorted(broker.positions()), as_of)\nif broker.cash() + sum(p.shares * marks[t] for t, p in broker.positions().items()) <= 0:\n    raise SystemExit(f\"book is non-positive before {as_of}; stopping run\")","preventionTips":["Track equity per cycle from CycleRecords and stop the run at a drawdown threshold you choose, before it hits zero.","Set mandate risk limits (per-position caps, gross exposure) so simulated ruin requires a real bug to reach.","Sanity-check spec.capital > 0 at mandate load time.","If equity crosses zero implausibly fast, audit SimBroker fills and fee modeling before re-running."],"tags":["portfolio","risk","backtesting","position-sizing"],"backgroundTag":null,"analyzedSha":"eff8a7320fcf0b473b135690fa1a5b0d9b022a83","analyzedAt":"2026-08-15T00:22:46.567Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}