diff --git a/app/services/proposal_service.py b/app/services/proposal_service.py index 774cac4..4238e36 100644 --- a/app/services/proposal_service.py +++ b/app/services/proposal_service.py @@ -209,6 +209,58 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list: return res["candidates"] +def disposition_snapshot(now=None) -> dict: + """候选池处置快照 (只读, 无副作用): 候选池里每只票今天为什么下单 / 没下单。 + + 页面「系统在盯的候选」原来只铺上游榜单, 看不出每只票的去向。这里补上去向与原因: + 名额满 / 可投金额不足 / 超上限 / 不足一手 / 在途 / 已持有 / 今日已拒 —— 这些原因每分钟的 + scan_and_route 都算了, 但算完即弃 (评审账本只留规则闸/研判闸的驳回, 这一层查不到)。 + + 实现上**直接复用** `_scan_open`: 把它写 out / mkt 的两个收集器换成丢弃用的临时容器, + 一个字都不改扫描逻辑, 口径与实盘扫描逐字一致 (不另写一份, 免得日后分叉)。_scan_open 内部 + 只读库与行情、不落任何表, 所以这样调用是安全的。 + 「已下单 / 已生成提议」交给前端用它已在手的 instructions / proposals 按股票对齐, 这里不重复查。 + """ + now = now or datetime.now() + res = {"ok": True, "ready": True, "at": now.isoformat(timespec="seconds"), + "by_code": {}, "notes": [], + "autonomy": param_store.get("PMS_AUTONOMY", AUTONOMY_PROPOSE), + "open_autonomy": param_store.get("PMS_OPEN_AUTONOMY", AUTONOMY_FULL)} + if res["autonomy"] == AUTONOMY_OFF: + res["notes"].append("自主档位 off —— 本轮不扫描候选池 (off 是总闸)") + return res + if param_store.get_bool("PMS_GLOBAL_EXEC_HALT", False): + res["notes"].append("全局暂停执行 (休假模式) —— 本轮不扫描候选池") + return res + if res["open_autonomy"] == AUTONOMY_OFF: + res["notes"].append("新建仓档位 off —— 不扫描候选池") + return res + try: + view = portfolio.positions_view() + params = _scan_params(view) + stock_params = command_service.effective_stock_params() + # 与实盘扫描同一份 skip: 在途 / 今日已被规则闸拒 / 今日已被研判闸驳回的新建仓 + skip = {**_judge_rejected_open_keys(), **_rejected_today_keys(), **_inflight_keys()} + sink = {"skipped": []} # 丢弃用: _scan_open 只往里 append/extend, 不读它 + would = _scan_open(view, params, stock_params, skip, {}, sink) + except Exception as e: + logger.exception("候选处置快照失败") + return {"ok": False, "ready": False, "error": f"{type(e).__name__}: {e}", + "by_code": {}, "notes": []} + for c in would: + res["by_code"][c["ts_code"]] = { + "disp": "would", "why": "名额与资金都够, 本轮将建底仓 (下一跳落单)"} + for s in sink["skipped"]: + code = s.get("ts_code") + if not code or code == "*": # 池级原因 (名额满/资金为零/候选池空…) 挂到 notes + if s.get("why"): + res["notes"].append(s["why"]) + else: # 单票原因; 不覆盖已判 would 的 + res["by_code"].setdefault( + code, {"disp": "deny", "why": s.get("why") or "未产出候选"}) + return res + + def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out, *, deadline=None): code, action, side = c["ts_code"], c["action"], c["side"] diff --git a/app/web/main.py b/app/web/main.py index 23fd0cb..aad8f36 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -486,6 +486,18 @@ def api_clear_resync(): # ================================================================ 上游选股计划 +@app.get("/api/open-scan") +def api_open_scan(): + """候选池处置快照 (只读): 每只候选今天为什么下单 / 没下单。 + + 复用每分钟那条自主扫描的 scan_open 口径, 但止步于规则闸之前、不送研判、不落任何表 —— + 补上「名额/资金/上限/不足一手/在途/已持有」这些算完即弃、评审账本里查不到的原因。 + 「已下单/已生成提议」由前端拿它已在手的 instructions/proposals 按股票对齐, 这里不重复查。 + """ + from app.services import proposal_service + return ok(proposal_service.disposition_snapshot) + + @app.get("/api/upstream/plan") def api_upstream_plan(limit: int = Query(30), date: str = Query(None), bucket: str = Query("main")): diff --git a/app/web/static/index.html b/app/web/static/index.html index 442bbf4..40f7cdd 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -1,79 +1,128 @@ - +