{"record":{"id":"244c090d1715bbe4","repo":"ZhuLinsen/daily_stock_analysis","slug":"error-244c09","errorCode":null,"errorMessage":"选股功能不可用。请检查策略配置、数据依赖和服务日志。","messagePattern":"选股功能不可用。请检查策略配置、数据依赖和服务日志。","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/dsa-web/src/api/screening.ts","lineNumber":460,"sourceCode":"      `/api/v1/screening/hotspots/${encodeURIComponent(payload.topic)}`,\n      {\n        params: {\n          provider: payload.provider || 'akshare',\n          refresh: payload.refresh ?? false,\n          include_search: payload.includeSearch ?? false,\n        },\n        timeout: SCREENING_REQUEST_TIMEOUT_MS,\n      },\n    );\n    return toCamelCase<ScreeningHotspotDetail>(response.data);\n  },\n\n  async enable(): Promise<void> {\n    await setScreeningEnabled('true');\n    try {\n      const status = await screeningApi.getStatus();\n      if (!status.available) {\n        throw new Error('选股功能不可用。请检查策略配置、数据依赖和服务日志。');\n      }\n    } catch (error) {\n      try {\n        await setScreeningEnabled('false');\n      } catch {\n        // Preserve the original availability/status failure for the caller.\n      }\n      throw error;\n    }\n  },\n};\n","sourceCodeStart":442,"sourceCodeEnd":472,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/apps/dsa-web/src/api/screening.ts#L442-L472","documentation":"Raised inside quantity replay (portfolio_service.py:765) when a historical sell event exceeds the running quantity_held at that point in the ordered event stream. This is the same oversell contract as the write-time check, but detected during replay of already-stored events, e.g. when computing a snapshot; PortfolioOversellError carries the event's symbol, date, and quantities.","triggerScenarios":"Deleting or re-dating a buy after sells were recorded; inserting a sell with an earlier trade_date than its covering buy; a missing split_adjustment so historical holdings are understated; importing an out-of-order history where an early sell is replayed before its buy. Because writes are validated at insert time, hitting this at replay usually means the event timeline changed after the fact.","commonSituations":"Manual edits to trade dates; deletion of a buy via direct DB access; import jobs inserting events in reverse-chronological batches with later backfills; timezone handling that shifts a buy's date to after a same-day sell.","solutions":["Reconstruct a consistent timeline: ensure for each symbol every sell date has cumulative buys+splits covering it","Restore or re-add the deleted/edited buy events, or delete the orphaned sells if they were wrong","Insert the missing split_adjustment events so historical quantities scale correctly","Import in chronological order and never mutate stored event dates directly in the DB"],"exampleFix":"# before\n# sell imported with date before its buy\nsvc.add_trade(account_id=1, symbol=\"AAPL\", side=\"sell\", quantity=10, trade_date=date(2025,1,1), ...)\nsvc.add_trade(account_id=1, symbol=\"AAPL\", side=\"buy\", quantity=10, trade_date=date(2024,12,31), ...)\n# after\nsvc.add_trade(account_id=1, symbol=\"AAPL\", side=\"buy\", quantity=10, trade_date=date(2024,12,31), ...)\nsvc.add_trade(account_id=1, symbol=\"AAPL\", side=\"sell\", quantity=10, trade_date=date(2025,1,1), ...)","handlingStrategy":"try-catch","validationCode":"def timeline_consistent(svc, account_id, symbol):\n    held = 0.0\n    for ev in svc.query_trades(account_id=account_id, symbol=symbol, page=1, page_size=10000)[\"items\"]:\n        held += ev[\"quantity\"] if ev[\"side\"] == \"buy\" else -ev[\"quantity\"]\n        if held < -1e-8:\n            return False\n    return True","typeGuard":"from src.services.portfolio_service import PortfolioOversellError\n\ndef is_oversell(exc: Exception) -> bool:\n    return isinstance(exc, PortfolioOversellError)","tryCatchPattern":"from src.services.portfolio_service import PortfolioOversellError\n\ntry:\n    svc.get_snapshot(account_id=a)\nexcept PortfolioOversellError as exc:\n    # exc.symbol / exc.trade_date pinpoint the first inconsistent sell\n    quarantine_event(exc.symbol, exc.trade_date); svc.get_snapshot(account_id=a)","preventionTips":["Import events in chronological order; never re-date stored events","Do not delete or edit buys via direct DB access","Insert split events before the sells that depend on them","Verify same-day buy-then-sell ordering after timezone conversion"],"tags":["portfolio","oversell","replay","trade","data-integrity"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}