{"record":{"id":"16500965454b1edc","repo":"virattt/ai-hedge-fund","slug":"held-position-ticker-has-no-price-within-mark","errorCode":null,"errorMessage":"held position {ticker} has no price within {_MARK_LOOKBACK_DAYS} days of {as_of} — cannot value the book","messagePattern":"held position (.+?) has no price within (.+?) days of (.+?) — cannot value the book","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"hedge_fund/pipeline/run_cycle.py","lineNumber":163,"sourceCode":"    held: dict,\n    data_client: DataClient,\n) -> tuple[dict[str, float], list[TickerSkip]]:\n    \"\"\"Last close on or before *as_of* for each ticker, within the lookback.\n\n    No bar and not held -> TickerSkip (the caller then never runs analysts\n    on it). No bar but HELD -> raise: the book cannot be honestly valued.\n    \"\"\"\n    start = (_date.fromisoformat(as_of) - timedelta(days=_MARK_LOOKBACK_DAYS)).isoformat()\n    marks: dict[str, float] = {}\n    skipped: list[TickerSkip] = []\n\n    for ticker in tickers:\n        prices = data_client.get_prices(ticker, start, as_of)\n        bars = [p for p in prices if p.time[:10] <= as_of]\n        if bars:\n            marks[ticker] = max(bars, key=lambda p: p.time).close\n        elif ticker in held:\n            raise ValueError(\n                f\"held position {ticker} has no price within \"\n                f\"{_MARK_LOOKBACK_DAYS} days of {as_of} — cannot value the book\"\n            )\n        else:\n            skipped.append(TickerSkip(\n                ticker=ticker,\n                reason=f\"no close within {_MARK_LOOKBACK_DAYS} days of {as_of}\",\n            ))\n\n    return marks, skipped\n","sourceCodeStart":145,"sourceCodeEnd":174,"githubUrl":"https://github.com/virattt/ai-hedge-fund/blob/eff8a7320fcf0b473b135690fa1a5b0d9b022a83/hedge_fund/pipeline/run_cycle.py#L145-L174","documentation":"Raised by _mark_prices (hedge_fund/pipeline/run_cycle.py:163) when a ticker the broker currently HOLDS has no price bar within the last _MARK_LOOKBACK_DAYS (=7) calendar days of as_of. The policy is asymmetric by design: a universe ticker with no recent bar is merely skipped (TickerSkip, 'missing data reads as no signal'), but a held ticker cannot be valued honestly, so the run raises rather than mark the book at zero or stale prices.","triggerScenarios":"A held ticker is delisted, halted, or thinly traded (no close in the 7-day window ending at as_of): e.g. an earnings halt, a suspension, or a backtest grid date that ran past the ticker's last trade. Also possible: the cached price data simply ends before end of the backtest window. Only fires when ticker in held — universe-only names never trigger it.","commonSituations":"Backtests that hold small-caps through halts; a delisting mid-window (data stops); using as_of dates beyond the last cached bar; data provider gaps of >7 days for OTC names.","solutions":["Verify data coverage: check get_prices(ticker, as_of-7d, as_of) actually returns bars; if the cache is truncated for that ticker, refresh/re-fetch it.","If the position is legitimately stale (halt/delisting), have the cycle logic liquidate or write off the position before the mark step instead of holding it into a valuation.","Use more liquid tickers in the universe, or accept that halted names abort the run and wrap run_cycle per cycle with recovery logic.","As a last resort, widen _MARK_LOOKBACK_DAYS — but understand this marks the book at up-to-N-day-old prices, changing valuation honesty."],"exampleFix":"# before\nrecord = run_cycle(fund, as_of, broker, client, universe)  # held MEgas halt -> ValueError mid-run\n\n# after\nfrom datetime import date, timedelta\nlook = (date.fromisoformat(as_of) - timedelta(days=7)).isoformat()\nstale = [t for t in broker.positions() if not client.get_prices(t, look, as_of)]\nif stale:\n    # liquidate/flag stale names before the cycle\n    for t in stale:\n        broker.close(t)  # or record a write-off\nrecord = run_cycle(fund, as_of, broker, client, universe)","handlingStrategy":"validation","validationCode":"from datetime import date, timedelta\n\ndef stale_held_positions(client, broker, as_of: str, lookback_days: int = 7) -> list[str]:\n    \"\"\"Held tickers with no bar in the mark window — the exact raise condition.\"\"\"\n    start = (date.fromisoformat(as_of) - timedelta(days=lookback_days)).isoformat()\n    return [\n        t for t, _ in broker.positions().items()\n        if not any(p.time[:10] <= as_of for p in client.get_prices(t, start, as_of))\n    ]","typeGuard":"def position_is_markable(bars: list, as_of: str) -> bool:\n    return any(b.time[:10] <= as_of for b in bars)","tryCatchPattern":"try:\n    record = run_cycle(fund, as_of, broker, client, universe)\nexcept ValueError as e:\n    if \"cannot value the book\" in str(e):\n        # deliberate policy: either liquidate the stale name and restart the\n        # cycle, or abort the backtest. Do NOT mark it at zero silently.\n        raise SystemExit(f\"unmarkable position: {e}\") from e\n    raise","preventionTips":["Before each cycle, scan held positions for a bar within _MARK_LOOKBACK_DAYS (7) and act (liquidate/flag) instead of letting the mark step raise.","Ensure cached price data covers every held ticker through end of the backtest window.","Prefer liquid tickers; know that any halt/delisting mid-run will abort the run by design.","Treat widening _MARK_LOOKBACK_DAYS as a valuation-policy change, not a quick fix."],"tags":["pricing","portfolio","data-gap","halt"],"backgroundTag":null,"analyzedSha":"eff8a7320fcf0b473b135690fa1a5b0d9b022a83","analyzedAt":"2026-08-15T00:22:46.567Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}