diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 72e71e8..04d9b98 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -181,6 +181,7 @@ from lib.trade.entry_model_lib import ( hub_meta_entry_context, migrate_entry_model_columns, order_entry_template_context, + open_position_button_label, parse_manual_order_style_fields, resolve_trade_record_entry_reason, trend_manual_entry_reason_count, @@ -7261,9 +7262,7 @@ def render_main_page(page="trade", embed_mode=None): position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE), trade_policy=trade_policy_template_context(TRADE_POLICY), **order_entry_template_context(TRADE_POLICY), - open_position_button_label=( - "开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)" - ), + open_position_button_label=open_position_button_label(TRADE_POLICY, POSITION_SIZING_MODE), breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER, breakeven_offset_pct=BREAKEVEN_OFFSET_PCT, price_fmt=format_price_for_symbol, @@ -7551,25 +7550,31 @@ def api_price_snapshot(): }) order_prices = [] + from lib.hub.price_snapshot_lib import resolve_order_snapshot_price + for r in order_rows: - price = prices.get(r["symbol"]) - if price is None: - continue margin = float(r["margin_capital"] or 0) leverage = float(r["leverage"] or 0) entry = float(r["trigger_price"] or 0) - pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0 - pnl_pct = round((pnl / margin * 100), 2) if margin > 0 else 0 exchange_tpsl = {"sl": None, "tp": None} ex_sym = resolve_monitor_exchange_symbol(r) prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"]) lev_row = r["leverage"] if "leverage" in r.keys() else None ex_metrics = parse_ccxt_position_metrics(prow, order_leverage=lev_row) if prow else None + price = resolve_order_snapshot_price( + r["symbol"], + prices, + position_row=prow, + order_leverage=lev_row, + parse_position_metrics_fn=parse_ccxt_position_metrics, + get_mark_price_fn=get_symbol_mark_price, + fallback_entry=entry if entry > 0 else None, + ) + pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 and price else 0 + pnl_pct = round((pnl / margin * 100), 2) if margin > 0 else 0 payload = { "id": r["id"], "symbol": r["symbol"], - "price": round(price, 6), - "price_display": format_price_for_symbol(ex_sym, price), "float_pnl": round(pnl, FUNDS_DECIMALS), "float_pct": pnl_pct, "plan_margin": round(margin, FUNDS_DECIMALS) if margin else None, @@ -7579,6 +7584,12 @@ def api_price_snapshot(): "exchange_mark_price_display": None, "pnl_source": "plan", } + if price is not None: + payload["price"] = round(price, 6) + payload["price_display"] = format_price_for_symbol(ex_sym, price) + else: + payload["price"] = None + payload["price_display"] = "-" if ex_metrics: if ex_metrics.get("initial_margin") is not None: payload["exchange_initial_margin"] = ex_metrics["initial_margin"] diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 06eece6..9400274 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -180,6 +180,7 @@ from lib.trade.entry_model_lib import ( hub_meta_entry_context, migrate_entry_model_columns, order_entry_template_context, + open_position_button_label, parse_manual_order_style_fields, resolve_trade_record_entry_reason, trend_manual_entry_reason_count, @@ -7042,9 +7043,7 @@ def render_main_page(page="trade", embed_mode=None): position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE), trade_policy=trade_policy_template_context(TRADE_POLICY), **order_entry_template_context(TRADE_POLICY), - open_position_button_label=( - "开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)" - ), + open_position_button_label=open_position_button_label(TRADE_POLICY, POSITION_SIZING_MODE), breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER, breakeven_offset_pct=BREAKEVEN_OFFSET_PCT, price_fmt=format_price_for_symbol, @@ -7365,20 +7364,28 @@ def api_price_snapshot(): }) order_prices = [] + from lib.hub.price_snapshot_lib import resolve_order_snapshot_price + for r in order_rows: - price = prices.get(r["symbol"]) - if price is None: - continue margin = float(r["margin_capital"] or 0) leverage = float(r["leverage"] or 0) entry = float(r["trigger_price"] or 0) - pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0 - pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0 exchange_tpsl = {"sl": None, "tp": None} ex_sym = resolve_monitor_exchange_symbol(r) prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"]) lev_row = r["leverage"] if "leverage" in r.keys() else None ex_metrics = parse_ccxt_position_metrics(prow, order_leverage=lev_row) if prow else None + price = resolve_order_snapshot_price( + r["symbol"], + prices, + position_row=prow, + order_leverage=lev_row, + parse_position_metrics_fn=parse_ccxt_position_metrics, + get_mark_price_fn=get_symbol_mark_price, + fallback_entry=entry if entry > 0 else None, + ) + pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 and price else 0 + pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0 payload = { "id": r["id"], "symbol": r["symbol"], @@ -7404,18 +7411,27 @@ def api_price_snapshot(): payload["float_pct"] = ( round((payload["float_pnl"] / float(denom)) * 100, 4) if denom and float(denom) > 0 else pnl_pct ) - px_for_fmt = float(price) + px_for_fmt = None + if price is not None: + try: + px_for_fmt = float(price) + except (TypeError, ValueError): + px_for_fmt = None if ex_metrics and ex_metrics.get("mark_price") is not None: try: px_for_fmt = float(ex_metrics["mark_price"]) except (TypeError, ValueError): pass - px_disp = format_price_for_symbol(r["symbol"], px_for_fmt) - try: - payload["price"] = float(px_disp) if px_disp != "-" else px_for_fmt - except Exception: - payload["price"] = px_for_fmt - payload["price_display"] = px_disp + if px_for_fmt is not None: + px_disp = format_price_for_symbol(r["symbol"], px_for_fmt) + try: + payload["price"] = float(px_disp) if px_disp != "-" else px_for_fmt + except Exception: + payload["price"] = px_for_fmt + payload["price_display"] = px_disp + else: + payload["price"] = None + payload["price_display"] = "-" if exchange_private_api_configured(): try: exchange_tpsl = fetch_exchange_tpsl_slots( diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index bec05d0..0796e8b 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -179,6 +179,7 @@ from lib.trade.entry_model_lib import ( hub_meta_entry_context, migrate_entry_model_columns, order_entry_template_context, + open_position_button_label, parse_manual_order_style_fields, resolve_trade_record_entry_reason, trend_manual_entry_reason_count, @@ -6605,9 +6606,7 @@ def render_main_page(page="trade", embed_mode=None): position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE), trade_policy=trade_policy_template_context(TRADE_POLICY), **order_entry_template_context(TRADE_POLICY), - open_position_button_label=( - "开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)" - ), + open_position_button_label=open_position_button_label(TRADE_POLICY, POSITION_SIZING_MODE), breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER, breakeven_offset_pct=BREAKEVEN_OFFSET_PCT, price_fmt=format_price_for_symbol, @@ -6967,20 +6966,28 @@ def api_price_snapshot(): }) order_prices = [] + from lib.hub.price_snapshot_lib import resolve_order_snapshot_price + for r in order_rows: - price = prices.get(r["symbol"]) - if price is None: - continue margin = float(r["margin_capital"] or 0) leverage = float(r["leverage"] or 0) entry = float(r["trigger_price"] or 0) - pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0 - pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0 exchange_tpsl = {"sl": None, "tp": None} ex_sym = resolve_monitor_exchange_symbol(r) prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"]) lev_row = r["leverage"] if "leverage" in r.keys() else None ex_metrics = parse_ccxt_position_metrics(prow, order_leverage=lev_row) if prow else None + price = resolve_order_snapshot_price( + r["symbol"], + prices, + position_row=prow, + order_leverage=lev_row, + parse_position_metrics_fn=parse_ccxt_position_metrics, + get_mark_price_fn=get_symbol_mark_price, + fallback_entry=entry if entry > 0 else None, + ) + pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 and price else 0 + pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0 payload = { "id": r["id"], "symbol": r["symbol"], @@ -7007,18 +7014,27 @@ def api_price_snapshot(): payload["float_pct"] = ( round((payload["float_pnl"] / float(denom)) * 100, 4) if denom and float(denom) > 0 else pnl_pct ) - px_for_fmt = float(price) + px_for_fmt = None + if price is not None: + try: + px_for_fmt = float(price) + except (TypeError, ValueError): + px_for_fmt = None if ex_metrics and ex_metrics.get("mark_price") is not None: try: px_for_fmt = float(ex_metrics["mark_price"]) except (TypeError, ValueError): pass - px_disp = format_price_for_symbol(r["symbol"], px_for_fmt) - try: - payload["price"] = float(px_disp) if px_disp != "-" else px_for_fmt - except Exception: - payload["price"] = px_for_fmt - payload["price_display"] = px_disp + if px_for_fmt is not None: + px_disp = format_price_for_symbol(r["symbol"], px_for_fmt) + try: + payload["price"] = float(px_disp) if px_disp != "-" else px_for_fmt + except Exception: + payload["price"] = px_for_fmt + payload["price_display"] = px_disp + else: + payload["price"] = None + payload["price_display"] = "-" if exchange_private_api_configured(): try: exchange_tpsl = fetch_exchange_tpsl_slots( diff --git a/lib/hub/price_snapshot_lib.py b/lib/hub/price_snapshot_lib.py new file mode 100644 index 0000000..8550ca1 --- /dev/null +++ b/lib/hub/price_snapshot_lib.py @@ -0,0 +1,75 @@ +"""price_snapshot 共用:订单行情价兜底,避免 get_price 失败时整单不入 order_prices。""" +from __future__ import annotations + +from typing import Any, Callable, Mapping, Optional + +from lib.hub.hub_position_metrics import parse_position_mark_price + + +def resolve_order_snapshot_price( + symbol: str, + prices: Mapping[str, float], + *, + position_row: Optional[dict[str, Any]] = None, + order_leverage=None, + parse_position_metrics_fn: Callable[..., dict[str, Any] | None] | None = None, + get_mark_price_fn: Callable[[str], float | None] | None = None, + fallback_entry: float | None = None, +) -> float | None: + """ + 解析下单监控轮询用的现价/标记价,优先级: + 1. 已批量拉取的 ticker last + 2. get_symbol_mark_price(含 mark) + 3. 交易所持仓 mark(parse_ccxt_position_metrics / parse_position_mark_price) + 4. 计划成交价 trigger_price + """ + sym = (symbol or "").strip() + if not sym: + return None + + cached = prices.get(sym) + if cached is not None: + try: + v = float(cached) + if v > 0: + return v + except (TypeError, ValueError): + pass + + if get_mark_price_fn is not None: + try: + mp = get_mark_price_fn(sym) + if mp is not None and float(mp) > 0: + return float(mp) + except Exception: + pass + + if position_row: + mark = None + if parse_position_metrics_fn is not None: + try: + metrics = parse_position_metrics_fn( + position_row, order_leverage=order_leverage + ) + if isinstance(metrics, dict) and metrics.get("mark_price") is not None: + mark = float(metrics["mark_price"]) + except Exception: + mark = None + if mark is None or mark <= 0: + try: + mp = parse_position_mark_price(position_row) + if mp is not None and mp > 0: + mark = float(mp) + except Exception: + mark = None + if mark is not None and mark > 0: + return mark + + if fallback_entry is not None: + try: + entry = float(fallback_entry) + if entry > 0: + return entry + except (TypeError, ValueError): + pass + return None diff --git a/lib/instance/templates/embed_page_fragment.html b/lib/instance/templates/embed_page_fragment.html index 35b8ee0..0517eec 100644 --- a/lib/instance/templates/embed_page_fragment.html +++ b/lib/instance/templates/embed_page_fragment.html @@ -46,6 +46,7 @@ {{ order_entry_type_fields() }} {% from 'order_leverage_fields.html' import order_leverage_fields with context %} {{ order_leverage_fields() }} + {% if not intraday_discipline %} @@ -59,6 +60,9 @@ + {% else %} + + {% endif %} @@ -99,8 +103,10 @@ {{ '做多' if o.direction == 'long' else '做空' }}
+ {% if not intraday_discipline %} 平仓 + {% endif %}
@@ -108,8 +114,9 @@ {% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %} 风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %} - - {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %} + + {% if intraday_discipline %} + {% elif o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 6a0177d..fdf7604 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -144,6 +144,7 @@ {{ order_entry_type_fields() }} {% from 'order_leverage_fields.html' import order_leverage_fields with context %} {{ order_leverage_fields() }} + {% if not intraday_discipline %} @@ -157,6 +158,9 @@ + {% else %} + + {% endif %} @@ -212,8 +216,10 @@ {{ '做多' if o.direction == 'long' else '做空' }}
+ {% if not intraday_discipline %} 平仓 + {% endif %}
@@ -221,8 +227,9 @@ {% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %} 风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %} - - {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %} + + {% if intraday_discipline %} + {% elif o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
diff --git a/lib/instance/templates/order_entry_model_fields.html b/lib/instance/templates/order_entry_model_fields.html index b1608d9..e37e78a 100644 --- a/lib/instance/templates/order_entry_model_fields.html +++ b/lib/instance/templates/order_entry_model_fields.html @@ -1,4 +1,4 @@ -{# 趋势户:两级开仓类型 → 自动 trade_style;日内户:仍选手动 trend/swing #} +{# 趋势户:两级开仓类型 → 自动 trade_style;日内户:假破 / 结构突破 #} {% macro order_entry_type_fields() -%} {% if order_entry_profile == 'trend_div' %}
@@ -19,6 +19,14 @@ 趋势单
+{% elif order_entry_profile == 'intraday' %} + + {% else %}