diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index e870f87..8ea9221 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 8a9a610..1b6eaa7 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index 8a9a610..1b6eaa7 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index be56ba5..7ab2f9b 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 83a49fd..2e19be5 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -2944,12 +2944,35 @@ function renderRollSection(rolls, tickMap) { if (!rolls || !rolls.length) return ""; return rolls - .map( - (g) => `
-
组 #${esc(g.id)} · 监控单 #${esc(g.order_monitor_id || "—")}
-
腿数 ${esc(g.leg_count != null ? g.leg_count : "—")} · 止损 ${fmtSymbolPrice(g.current_stop_loss, g.symbol, tickMap)} · ${esc(g.status || "active")}
-
` - ) + .map((g) => { + const sym = g.symbol || g.exchange_symbol || ""; + const avg = + g.avg_entry_display || fmtSymbolPrice(g.avg_entry, sym, tickMap) || "—"; + const tpProfit = + g.reward_at_tp_usdt != null && g.reward_at_tp_usdt !== "" + ? `${fmt(g.reward_at_tp_usdt, 2)}U` + : "—"; + const legs = Array.isArray(g.recent_legs) ? g.recent_legs : []; + const legRows = legs + .map((leg) => { + const legAvg = + leg.avg_entry_display || + fmtSymbolPrice(leg.avg_entry_after, sym, tickMap) || + "—"; + const legProfit = + leg.reward_at_tp_usdt != null && leg.reward_at_tp_usdt !== "" + ? `${fmt(leg.reward_at_tp_usdt, 2)}U` + : "—"; + return `
腿 #${esc(leg.leg_index)} ${esc(leg.add_mode || "")} · 张 ${esc(leg.amount != null ? leg.amount : "—")} · 均价 ${legAvg} · 止盈 ${legProfit}
`; + }) + .join(""); + return `
+
组 #${esc(g.id)} · ${esc(g.symbol || "")} ${renderDirectionHtml(g.direction)} · 监控 #${esc(g.order_monitor_id || "—")}
+
腿数 ${esc(g.leg_count != null ? g.leg_count : "—")} · SL ${fmtSymbolPrice(g.current_stop_loss, sym, tickMap)} · 首仓TP ${fmtSymbolPrice(g.initial_take_profit, sym, tickMap)}
+
当前均价 ${avg} · 止盈盈利 ${tpProfit}
+ ${legRows} +
`; + }) .join(""); } diff --git a/static/instance_theme.js b/static/instance_theme.js index 1ea7864..1e937ce 100644 --- a/static/instance_theme.js +++ b/static/instance_theme.js @@ -329,19 +329,29 @@ } } - /** 中控 iframe 内:拦截顶栏导航,fetch 后 document.write 原地换页,避免 iframe 卸载白屏 */ - function initHubEmbedInFrameNav() { - if (!isHubLinked()) return; + /** 顶栏软导航:fetch + document.write 换页,避免整页卸载白屏(实例 standalone / 中控 iframe 均启用) */ + function injectNavTransitionGuard(html, theme) { + const t = normalize(theme || get()); + const bg = META[t]; + const guard = ``; + if (html.includes("")) { + return html.replace("", `${guard}`); + } + return guard + html; + } + function initInstanceTopNavSoft() { let navToken = 0; function isSoftNavLink(a) { if (!a || !a.getAttribute) return false; + if (a.hasAttribute("download") || a.target === "_blank") return false; return !!a.closest(".top-nav, .strategy-subnav"); } async function navigateInFrame(href, opts) { const token = ++navToken; + const themeNow = get(); try { const r = await fetch(href, { credentials: "same-origin" }); if (token !== navToken) return; @@ -349,7 +359,7 @@ location.href = href; return; } - const html = await r.text(); + const html = injectNavTransitionGuard(await r.text(), themeNow); if (token !== navToken) return; let path = href; try { @@ -398,13 +408,13 @@ if (isHubLinked()) { apply(get(), { skipStore: true }); window.addEventListener("message", (ev) => initFromHubMessage(ev.data)); - initHubEmbedInFrameNav(); try { window.parent.postMessage({ type: "instance-theme-ready" }, "*"); } catch (_) {} } else { apply(getStandalone()); } + initInstanceTopNavSoft(); function observeDynamicLists() { ["journal-list", "review-list"].forEach((id) => { diff --git a/strategy_roll_ui_lib.py b/strategy_roll_ui_lib.py new file mode 100644 index 0000000..43039e0 --- /dev/null +++ b/strategy_roll_ui_lib.py @@ -0,0 +1,402 @@ +"""顺势加仓 UI:滚仓腿合并均价与止盈盈利展示(实例页 + 中控)。""" +from __future__ import annotations + +from typing import Any, Callable, Optional + +from flask import Flask + +FILLED_LEG_STATUSES = frozenset({"filled", "done", "complete"}) + + +def reward_at_tp_usdt( + direction: str, + avg_entry: float, + take_profit: float, + qty: float, + *, + contract_size: float = 1.0, +) -> Optional[float]: + """与 strategy_roll_lib.preview_roll 一致:线性合约 U 本位盈利。""" + try: + avg = float(avg_entry) + tp = float(take_profit) + q = float(qty) + cs = float(contract_size or 1.0) + except (TypeError, ValueError): + return None + if avg <= 0 or tp <= 0 or q <= 0: + return None + direction = (direction or "long").strip().lower() + if direction == "short": + return (avg - tp) * q * cs + return (tp - avg) * q * cs + + +def leg_fill_price(leg: dict) -> Optional[float]: + if not isinstance(leg, dict): + return None + for key in ("fill_price", "limit_price"): + try: + v = float(leg.get(key) or 0) + if v > 0: + return v + except (TypeError, ValueError): + continue + return None + + +def leg_is_filled(leg: dict) -> bool: + st = str(leg.get("status") or "").strip().lower() + return st in FILLED_LEG_STATUSES + + +def infer_initial_position( + qty_live: float, + entry_live: float, + filled_legs: list[dict], + *, + monitor: dict | None = None, +) -> tuple[Optional[float], Optional[float]]: + """由当前持仓与各腿成交价反推首仓张数/均价。""" + try: + qty_live = float(qty_live) + entry_live = float(entry_live) + except (TypeError, ValueError): + qty_live = entry_live = 0.0 + legs = [ + lg + for lg in filled_legs or [] + if isinstance(lg, dict) and leg_is_filled(lg) and leg_fill_price(lg) and float(lg.get("amount") or 0) > 0 + ] + add_sum = sum(float(lg.get("amount") or 0) for lg in legs) + leg_notional = sum(float(lg.get("amount") or 0) * float(leg_fill_price(lg) or 0) for lg in legs) + q0 = qty_live - add_sum + if q0 > 1e-12 and entry_live > 0 and qty_live > 0: + e0 = (entry_live * qty_live - leg_notional) / q0 + if e0 > 0: + return q0, e0 + mon = monitor if isinstance(monitor, dict) else {} + try: + trig = float(mon.get("trigger_price") or 0) + except (TypeError, ValueError): + trig = 0.0 + try: + mon_amt = float(mon.get("order_amount") or mon.get("amount") or 0) + except (TypeError, ValueError): + mon_amt = 0.0 + if trig > 0: + q_base = q0 if q0 > 1e-12 else (mon_amt if mon_amt > 0 else max(qty_live - add_sum, 0)) + if q_base > 0: + return q_base, trig + return None, None + + +def compute_roll_chain_metrics( + group: dict, + legs: list[dict], + *, + qty_live: Optional[float] = None, + entry_live: Optional[float] = None, + monitor: dict | None = None, + contract_size: float = 1.0, +) -> tuple[dict[Any, dict], dict]: + """ + 返回 (leg_metrics_by_id, group_metrics)。 + leg_metrics: leg id -> {avg_entry_after, reward_at_tp_usdt} + group_metrics: 最后一腿后的 {avg_entry, reward_at_tp_usdt} + """ + per_leg: dict[Any, dict] = {} + group_out: dict[str, Any] = {"avg_entry": None, "reward_at_tp_usdt": None} + if not isinstance(group, dict): + return per_leg, group_out + direction = (group.get("direction") or "long").strip().lower() + try: + tp = float(group.get("initial_take_profit") or 0) + except (TypeError, ValueError): + tp = 0.0 + sorted_legs = sorted( + [lg for lg in legs or [] if isinstance(lg, dict)], + key=lambda x: int(x.get("leg_index") or 0), + ) + filled = [lg for lg in sorted_legs if leg_is_filled(lg)] + q0 = e0 = None + if qty_live is not None and entry_live is not None: + q0, e0 = infer_initial_position(float(qty_live), float(entry_live), filled, monitor=monitor) + if q0 is None or e0 is None: + return per_leg, group_out + qty = float(q0) + avg = float(e0) + if tp > 0: + group_out["avg_entry"] = avg + group_out["reward_at_tp_usdt"] = reward_at_tp_usdt( + direction, avg, tp, qty, contract_size=contract_size + ) + for leg in sorted_legs: + if not leg_is_filled(leg): + continue + try: + amt = float(leg.get("amount") or 0) + except (TypeError, ValueError): + continue + px = leg_fill_price(leg) + if not px or amt <= 0: + continue + prev_qty = qty + qty = prev_qty + amt + avg = (prev_qty * avg + amt * px) / qty + reward = reward_at_tp_usdt(direction, avg, tp, qty, contract_size=contract_size) if tp > 0 else None + lid = leg.get("id") + if lid is None: + lid = f"{group.get('id')}|{leg.get('leg_index')}" + per_leg[lid] = { + "avg_entry_after": round(avg, 10), + "reward_at_tp_usdt": round(reward, 4) if reward is not None else None, + } + group_out["avg_entry"] = round(avg, 10) + group_out["reward_at_tp_usdt"] = round(reward, 4) if reward is not None else None + return per_leg, group_out + + +def _row_to_dict(row) -> dict: + if row is None: + return {} + try: + return dict(row) + except Exception: + return {} + + +def _resolve_roll_live(cfg: dict, group: dict, monitor: dict | None) -> tuple[Optional[float], Optional[float], float]: + """读取交易所持仓张数、均价、contract_size。""" + m = cfg.get("app_module") + ex_sym = group.get("exchange_symbol") + sym = group.get("symbol") or "" + direction = (group.get("direction") or "long").strip().lower() + if not ex_sym and m is not None: + norm = getattr(m, "normalize_exchange_symbol", None) + if callable(norm): + try: + ex_sym = norm(sym) + except Exception: + ex_sym = sym + cs = 1.0 + get_cs = cfg.get("get_contract_size") + if not callable(get_cs) and m is not None: + get_cs = getattr(m, "get_contract_size", None) + if callable(get_cs): + try: + cs = float(get_cs(ex_sym or sym) or 1.0) + except Exception: + cs = 1.0 + get_pos = cfg.get("get_position") + if not callable(get_pos): + return None, None, cs + try: + pos = get_pos(ex_sym or sym, direction) or {} + qty = float(pos.get("contracts") or 0) + entry = float(pos.get("entry_price") or 0) + if qty > 0 and entry > 0: + return qty, entry, cs + except Exception: + pass + metrics_fn = getattr(m, "get_live_position_exchange_metrics", None) if m else None + if callable(metrics_fn): + try: + met = metrics_fn(ex_sym or sym, direction) + if isinstance(met, dict): + qty = float(met.get("contracts") or met.get("size") or 0) + entry = float(met.get("entry_price") or 0) + if qty > 0 and entry > 0: + return qty, entry, cs + except Exception: + pass + if monitor: + try: + trig = float(monitor.get("trigger_price") or 0) + amt = float(monitor.get("order_amount") or monitor.get("amount") or 0) + if trig > 0 and amt > 0: + return amt, trig, cs + except (TypeError, ValueError): + pass + return None, None, cs + + +def enrich_roll_page_data(conn, page_data: dict, cfg: dict | None) -> dict: + """为 roll_groups / roll_legs 附加 avg_entry、reward_at_tp 展示字段。""" + if not isinstance(page_data, dict) or not cfg: + return page_data + groups = list(page_data.get("roll_groups") or []) + legs = list(page_data.get("roll_legs") or []) + if not groups: + return page_data + monitors_by_id: dict[int, dict] = {} + try: + for row in conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall(): + od = _row_to_dict(row) + mid = od.get("id") + if mid is not None: + monitors_by_id[int(mid)] = od + except Exception: + pass + legs_by_gid: dict[int, list] = {} + for leg in legs: + if not isinstance(leg, dict): + continue + try: + gid = int(leg.get("roll_group_id")) + except (TypeError, ValueError): + continue + legs_by_gid.setdefault(gid, []).append(leg) + price_fmt = cfg.get("price_fmt") + for g in groups: + if not isinstance(g, dict) or g.get("id") is None: + continue + gid = int(g["id"]) + mon = monitors_by_id.get(int(g.get("order_monitor_id") or 0)) + qty, entry, cs = _resolve_roll_live(cfg, g, mon) + per_leg, group_metrics = compute_roll_chain_metrics( + g, + legs_by_gid.get(gid, []), + qty_live=qty, + entry_live=entry, + monitor=mon, + contract_size=cs, + ) + g["avg_entry"] = group_metrics.get("avg_entry") + g["reward_at_tp_usdt"] = group_metrics.get("reward_at_tp_usdt") + if callable(price_fmt) and g.get("avg_entry") is not None: + try: + g["avg_entry_display"] = price_fmt(g.get("symbol"), g["avg_entry"]) + except Exception: + pass + for leg in legs_by_gid.get(gid, []): + lid = leg.get("id") + if lid is None: + lid = f"{gid}|{leg.get('leg_index')}" + metrics = per_leg.get(lid) or per_leg.get(leg.get("id")) + if not metrics: + continue + leg["avg_entry_after"] = metrics.get("avg_entry_after") + leg["reward_at_tp_usdt"] = metrics.get("reward_at_tp_usdt") + if callable(price_fmt) and leg.get("avg_entry_after") is not None: + try: + leg["avg_entry_display"] = price_fmt(g.get("symbol"), leg["avg_entry_after"]) + except Exception: + pass + page_data["roll_groups"] = groups + page_data["roll_legs"] = legs + return page_data + + +def enrich_roll_groups_for_hub(rolls: list[dict], conn, cfg: dict | None) -> list[dict]: + """中控 monitor API:每组附带当前均价、止盈盈利与最近滚仓腿。""" + if not rolls or not cfg: + return rolls + out = [] + gid_list = [] + for g in rolls: + if isinstance(g, dict) and g.get("id") is not None: + try: + gid_list.append(int(g["id"])) + except (TypeError, ValueError): + pass + legs_by_gid: dict[int, list] = {gid: [] for gid in gid_list} + if gid_list: + placeholders = ",".join("?" for _ in gid_list) + try: + rows = conn.execute( + f"SELECT * FROM roll_legs WHERE roll_group_id IN ({placeholders}) ORDER BY id DESC", + gid_list, + ).fetchall() + for row in rows: + leg = _row_to_dict(row) + try: + legs_by_gid[int(leg.get("roll_group_id"))].append(leg) + except (TypeError, ValueError): + pass + except Exception: + pass + monitors_by_id: dict[int, dict] = {} + try: + for row in conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall(): + od = _row_to_dict(row) + if od.get("id") is not None: + monitors_by_id[int(od["id"])] = od + except Exception: + pass + price_fmt = cfg.get("price_fmt") + for g in rolls: + if not isinstance(g, dict): + continue + gd = dict(g) + try: + gid = int(gd.get("id")) + except (TypeError, ValueError): + out.append(gd) + continue + mon = monitors_by_id.get(int(gd.get("order_monitor_id") or 0)) + group_legs = legs_by_gid.get(gid, []) + qty, entry, cs = _resolve_roll_live(cfg, gd, mon) + per_leg, group_metrics = compute_roll_chain_metrics( + gd, + group_legs, + qty_live=qty, + entry_live=entry, + monitor=mon, + contract_size=cs, + ) + gd.update(group_metrics) + if callable(price_fmt) and gd.get("avg_entry") is not None: + try: + gd["avg_entry_display"] = price_fmt(gd.get("symbol"), gd["avg_entry"]) + except Exception: + pass + recent = [] + for leg in sorted(group_legs, key=lambda x: int(x.get("leg_index") or 0), reverse=True)[:6]: + ld = dict(leg) + lid = ld.get("id") + if lid is None: + lid = f"{gid}|{ld.get('leg_index')}" + metrics = per_leg.get(lid) or per_leg.get(ld.get("id")) + if metrics: + ld.update(metrics) + if callable(price_fmt) and ld.get("avg_entry_after") is not None: + try: + ld["avg_entry_display"] = price_fmt(gd.get("symbol"), ld["avg_entry_after"]) + except Exception: + pass + recent.append(ld) + gd["recent_legs"] = recent + out.append(gd) + return out + + +def patch_roll_hub_enrich(app: Flask, cfg: dict) -> None: + """hub_bridge install 后:/api/hub/monitor 的 rolls 附带均价/止盈盈利。""" + ctx = dict(app.config.get("HUB_CTX") or {}) + prev: Callable | None = ctx.get("enrich_monitor") + + def enrich_monitor(keys=None, orders=None, trends=None, rolls=None): + payload: dict[str, Any] = {} + if callable(prev): + try: + prev_out = prev(keys=keys, orders=orders, trends=trends, rolls=rolls) + if isinstance(prev_out, dict): + payload.update(prev_out) + except Exception: + pass + if rolls: + get_db = cfg.get("get_db") + if callable(get_db): + conn = get_db() + try: + payload["rolls"] = enrich_roll_groups_for_hub(list(rolls), conn, cfg) + finally: + try: + conn.close() + except Exception: + pass + return payload + + ctx["enrich_monitor"] = enrich_monitor + app.config["HUB_CTX"] = ctx diff --git a/strategy_templates/strategy_roll.html b/strategy_templates/strategy_roll.html index e5bf7ab..281baa2 100644 --- a/strategy_templates/strategy_roll.html +++ b/strategy_templates/strategy_roll.html @@ -80,7 +80,7 @@

活跃滚仓组

- + {% for g in roll_groups %} @@ -89,9 +89,11 @@ + + {% else %} - + {% endfor %}
ID币种方向腿数首仓TP当前SL
ID币种方向腿数首仓TP当前SL当前均价止盈盈利U
{{ g.id }}{{ g.leg_count }} {% if price_fmt %}{{ price_fmt(g.symbol, g.initial_take_profit) }}{% else %}{{ g.initial_take_profit }}{% endif %} {% if price_fmt %}{{ price_fmt(g.symbol, g.current_stop_loss) }}{% else %}{{ g.current_stop_loss }}{% endif %}{% if g.avg_entry_display %}{{ g.avg_entry_display }}{% elif g.avg_entry is not none %}{{ g.avg_entry }}{% else %}—{% endif %}{% if g.reward_at_tp_usdt is not none %}{{ '%.2f'|format(g.reward_at_tp_usdt) }}{% else %}—{% endif %}
暂无
暂无
@@ -99,7 +101,7 @@

最近滚仓腿

- + {% for leg in roll_legs %} @@ -107,10 +109,12 @@ + + {% else %} - + {% endfor %}
#方式张数新SL状态
#方式张数新SL当前均价止盈盈利U状态
{{ leg.leg_index }}{{ leg.add_mode }} {{ leg.amount }} {{ leg.new_stop_loss }}{% if leg.avg_entry_display %}{{ leg.avg_entry_display }}{% elif leg.avg_entry_after is not none %}{{ leg.avg_entry_after }}{% else %}—{% endif %}{% if leg.reward_at_tp_usdt is not none %}{{ '%.2f'|format(leg.reward_at_tp_usdt) }}{% else %}—{% endif %} {{ leg.status_label or leg.status }}
暂无
暂无
diff --git a/strategy_templates/strategy_roll_panel.html b/strategy_templates/strategy_roll_panel.html index 798b409..b272c0c 100644 --- a/strategy_templates/strategy_roll_panel.html +++ b/strategy_templates/strategy_roll_panel.html @@ -39,7 +39,7 @@

活跃滚仓组

- + {% for g in roll_groups %} @@ -48,9 +48,11 @@ + + {% else %} - + {% endfor %}
ID币种方向腿数首仓TP当前SL
ID币种方向腿数首仓TP当前SL当前均价止盈盈利U
{{ g.id }}{{ g.leg_count }} {% if price_fmt %}{{ price_fmt(g.symbol, g.initial_take_profit) }}{% else %}{{ g.initial_take_profit }}{% endif %} {% if price_fmt %}{{ price_fmt(g.symbol, g.current_stop_loss) }}{% else %}{{ g.current_stop_loss }}{% endif %}{% if g.avg_entry_display %}{{ g.avg_entry_display }}{% elif g.avg_entry is not none %}{{ g.avg_entry }}{% else %}—{% endif %}{% if g.reward_at_tp_usdt is not none %}{{ '%.2f'|format(g.reward_at_tp_usdt) }}{% else %}—{% endif %}
暂无
暂无
@@ -58,7 +60,7 @@

最近滚仓腿

- + {% for leg in roll_legs %} @@ -66,10 +68,12 @@ + + {% else %} - + {% endfor %}
#方式张数新SL状态
#方式张数新SL当前均价止盈盈利U状态
{{ leg.leg_index }}{{ leg.add_mode }} {{ leg.amount }} {{ leg.new_stop_loss }}{% if leg.avg_entry_display %}{{ leg.avg_entry_display }}{% elif leg.avg_entry_after is not none %}{{ leg.avg_entry_after }}{% else %}—{% endif %}{% if leg.reward_at_tp_usdt is not none %}{{ '%.2f'|format(leg.reward_at_tp_usdt) }}{% else %}—{% endif %} {{ leg.status_label or leg.status }}
暂无
暂无
diff --git a/strategy_trend_register.py b/strategy_trend_register.py index d81fb59..7d18255 100644 --- a/strategy_trend_register.py +++ b/strategy_trend_register.py @@ -158,6 +158,11 @@ def install_strategy_trend(app: Flask, repo_root: str, app_module: Any = None, * app.extensions["strategy_trend_cfg"] = cfg register_trend_routes(app, cfg) _patch_hub_monitor_enrich(app, cfg) + roll_cfg = app.extensions.get("strategy_roll_cfg") + if isinstance(roll_cfg, dict): + from strategy_roll_ui_lib import patch_roll_hub_enrich + + patch_roll_hub_enrich(app, roll_cfg) _patch_hub_trend_views(app) @app.context_processor diff --git a/strategy_ui.py b/strategy_ui.py index 6057454..52a877a 100644 --- a/strategy_ui.py +++ b/strategy_ui.py @@ -34,6 +34,7 @@ def fetch_roll_page_data( *, default_risk_percent: float = 2.0, count_active_trends: Optional[Callable] = None, + roll_cfg: dict | None = None, ) -> dict[str, Any]: init_strategy_tables(conn) monitors = [] @@ -61,13 +62,18 @@ def fetch_roll_page_data( leg["status_label"] = roll_leg_status_label(leg.get("status")) roll_legs.append(leg) roll_legs = roll_legs[:50] - return { + out = { "roll_monitors": monitors, "roll_groups": roll_groups, "roll_legs": roll_legs, "roll_trend_active": count_active_trend_plans(conn, count_active_trends), "default_risk_percent": default_risk_percent, } + if roll_cfg: + from strategy_roll_ui_lib import enrich_roll_page_data + + enrich_roll_page_data(conn, out, roll_cfg) + return out DEFAULT_TREND_DISABLED_NOTE = ( @@ -116,10 +122,18 @@ def strategy_page_template_vars( """render_main_page 在 conn.close() 前合并进 render_template 的变量。""" if page not in ("strategy", "strategy_trend", "strategy_roll"): return {} + roll_cfg = None + try: + from flask import current_app + + roll_cfg = (current_app.extensions or {}).get("strategy_roll_cfg") + except Exception: + roll_cfg = None out = fetch_roll_page_data( conn, default_risk_percent=default_risk_percent, count_active_trends=count_active_trends, + roll_cfg=roll_cfg if isinstance(roll_cfg, dict) else None, ) if trend_cfg and request_obj is not None: from strategy_trend_register import load_trend_page_context diff --git a/tests/test_strategy_roll_ui_lib.py b/tests/test_strategy_roll_ui_lib.py new file mode 100644 index 0000000..05b00e3 --- /dev/null +++ b/tests/test_strategy_roll_ui_lib.py @@ -0,0 +1,44 @@ +"""strategy_roll_ui_lib 单元测试。""" +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import strategy_roll_ui_lib as roll_ui + + +def test_compute_roll_chain_metrics_short(): + group = { + "id": 1, + "direction": "short", + "initial_take_profit": 60.0, + } + legs = [ + {"id": 10, "leg_index": 1, "amount": 3.0, "fill_price": 65.0, "status": "filled"}, + {"id": 11, "leg_index": 2, "amount": 5.0, "fill_price": 64.0, "status": "filled"}, + ] + per_leg, group_metrics = roll_ui.compute_roll_chain_metrics( + group, + legs, + qty_live=8.0, + entry_live=63.5, + monitor={"trigger_price": 66.0, "order_amount": 3.0}, + ) + assert per_leg[10]["avg_entry_after"] is not None + assert per_leg[11]["avg_entry_after"] is not None + assert group_metrics["reward_at_tp_usdt"] is not None + assert per_leg[11]["reward_at_tp_usdt"] >= per_leg[10]["reward_at_tp_usdt"] + + +def test_infer_initial_position_from_live(): + legs = [{"amount": 2.0, "fill_price": 64.0, "status": "filled"}] + q0, e0 = roll_ui.infer_initial_position(5.0, 63.0, legs) + assert q0 == 3.0 + assert abs(e0 - 62.3333333333) < 0.001 + + +def test_reward_at_tp_long(): + assert roll_ui.reward_at_tp_usdt("long", 100.0, 110.0, 2.0) == 20.0