diff --git a/app/services/command_service.py b/app/services/command_service.py index 723efb8..84f9a8d 100644 --- a/app/services/command_service.py +++ b/app/services/command_service.py @@ -76,6 +76,51 @@ def effective_stock_params() -> dict: return out +def _shown_price(v): + """显示用价格归一: 取不成数就是 None, 小于等于 0 也是 None (等于没设)。 + + 「小于等于 0 当作没设」不是这里新立的规矩, 是照抄两个求值处的现成口径: + 规则闸 rule_gate._check_stop_price 判的是 stop <= 0 就什么都不做, 动作引擎 + action_engine.eval_target 判的是 target <= 0 就当这条动作不存在 (设定某股目标价 + 这条命令的取消办法就是再设一次、价格填 0)。页面必须跟这两处看到同一件事, + 否则用户填 0 取消了目标价, 页面还挂着一个数, 又是一次「说的和做的不一样」。 + """ + try: + f = float(v) + except (TypeError, ValueError): + return None + return f if f > 0 else None + + +def attach_user_prices(positions: list, stock_params: dict = None) -> list: + """把用户命令设的止损价与目标价挂到持仓行上, 只供页面显示, 不参与任何判断。 + + 为什么持仓行上非要多这两个字段 + ------------------------------ + 页面参考位那一列读的是持仓行上的 stop_ref, 而 stop_ref 是系统自己算的参考位: + ledger_service.premarket() 每个交易日盘前都会用参考位取数, 把 support_ref、 + pressure_ref、stop_ref、ref_source 整批重写一遍。用户下达设定某股止损价这条命令时, + 命令会把价投影到 stop_ref 上, 到第二天早上这份投影就被系统参考位盖掉了。 + + 盖掉这件事本身是对的, 系统参考位本来就该每天重算, 所以这次不动 premarket 的行为。 + 判断那一侧也早就不吃这份投影了: 规则闸的止损价告警读命令表, 动作引擎的目标价到价 + 也读命令表, 两处的事实源都是命令表。真正没治的只有显示 —— 用户设完止损价, + 第二天在页面上只看得到系统算的那个数, 会以为自己设的价没生效或者被系统改掉了。 + + 所以这里从命令表把这两个价单独取出来, 挂成 user_stop_price 与 user_target_price + 两个字段, 页面把它们与系统参考位分列显示, 两个数各归各位。 + + 就地改传进来的那批行 (positions_view 返回的 positions 与 held 是同一批对象, + 改一次两处都带上), 并把同一个列表原样返回, 便于串写。 + """ + sp = stock_params or {} + for row in positions or []: + d = sp.get(row.get("ts_code")) or {} + row["user_stop_price"] = _shown_price(d.get("stop_price")) + row["user_target_price"] = _shown_price(d.get("target_price")) + return positions + + def blacklist() -> set: return {c for c, d in effective_stock_params().items() if d.get("black")} diff --git a/app/services/param_store.py b/app/services/param_store.py index 7d4b0bd..d50a5e6 100644 --- a/app/services/param_store.py +++ b/app/services/param_store.py @@ -120,11 +120,14 @@ FAIL_CLOSED = { # 设定某股目标价 SET_TARGET_PRICE 2026-09-03 已接通 —— 动作引擎 eval_target 到价产出 # 清仓提议, 必定交人拍板。接通前它只存进命令表没人读。 # 设定某股止损价 SET_STOP_PRICE 2026-09-03 已接进规则闸的告警层 (只披露不拦截, -# 不自动卖)。但它投影到持仓行的 stop_ref 每天盘前会被 -# ledger_service.premarket() 的参考位取数覆盖掉 —— -# 页面参考位列第二天显示的就不再是你设的那个价了。 -# 所以判断一律读命令表 (事实源), 别读投影。这处覆盖 -# 本身还没修, 见交接里的遗留问题。 +# 不自动卖)。它投影到持仓行的 stop_ref 每天盘前会被 +# ledger_service.premarket() 的参考位取数覆盖掉, +# 所以判断一律读命令表 (事实源), 别读投影。 +# 这处覆盖是系统自算参考位的正常行为, 保留不动; +# 由此带来的页面错位已在 2026-09-03 修掉显示那一侧: +# 用户设的止损价与目标价由 command_service +# .attach_user_prices 从命令表单独取出来, 与系统参考位 +# 分列显示, 两张持仓表都看得到自己设的价。 # 做T授权 T0_ENABLE 只把 t0_enabled / t0_ratio 投影到持仓行给页面显示, # 没有任何代码把它当成「可以做T」的授权来检查 (真正的 # 做T由单独挂的交易方案驱动)。要不要把它变成硬授权, diff --git a/app/web/main.py b/app/web/main.py index f785ae6..38ed4f0 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -366,8 +366,16 @@ def api_plans(command_id: str = Query(None), status: str = Query(None), limit: i # ================================================================ ③ 持仓与账本 @app.get("/api/positions") def api_positions(): - return ok(lambda: {"ok": True, **portfolio.positions_view(), - "stock_params": command_service.effective_stock_params()}) + def _view(): + v = portfolio.positions_view() + sp = command_service.effective_stock_params() + # 用户设的止损价与目标价, 事实源在命令表, 与持仓行上系统每天早上重算的参考位 + # (support_ref / pressure_ref / stop_ref) 是两回事。在这里合到一起, 是为了让页面 + # 把两个数分列显示 —— 只是显示, 判断那一侧照旧各读各的事实源, 见 + # command_service.attach_user_prices 的说明。 + command_service.attach_user_prices(v["positions"], sp) + return {"ok": True, **v, "stock_params": sp} + return ok(_view) @app.get("/api/positions/{ts_code}/lots") diff --git a/app/web/static/index.html b/app/web/static/index.html index d449852..af96e3d 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -817,7 +817,9 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}

我的持仓 · {{ heldPositions.length }} 只

安全垫? 看的是浮盈厚不厚:厚垫可以考虑加仓,薄垫先放着,浮亏无垫不加仓。 - 成本是各批买入价的加权平均,占比是占总操作规模的百分比。
+ 成本是各批买入价的加权平均,占比是占总操作规模的百分比。 + 「我设的价」这一列是你自己下命令定的止损价与目标价,一直在,你改了才变; + 系统每天早上自己算的参考位是另一回事,在管理视图的持仓总览里看。