Fix Gate mark/PnL/TP-SL snapshot and intraday order UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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"]
|
||||
|
||||
+31
-15
@@ -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(
|
||||
|
||||
+31
-15
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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 %}
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
||||
</label>
|
||||
@@ -59,6 +60,9 @@
|
||||
<option value="4" selected>4h</option>
|
||||
</select>
|
||||
</span>
|
||||
{% else %}
|
||||
<input type="hidden" name="breakeven_enabled" value="0">
|
||||
{% endif %}
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
|
||||
</label>
|
||||
@@ -99,8 +103,10 @@
|
||||
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
|
||||
</div>
|
||||
<div class="pos-head-actions">
|
||||
{% if not intraday_discipline %}
|
||||
<button type="button" class="pos-entrust-btn" onclick="openTpslEntrustModal({{ o.id }})">委托</button>
|
||||
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pos-meta">
|
||||
@@ -108,8 +114,9 @@
|
||||
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
|
||||
<span class="pos-meta-item">风险: {% 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 %}</span>
|
||||
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
|
||||
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
|
||||
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||
<span class="pos-meta-item {% if not intraday_discipline %}{% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}{% endif %}">
|
||||
{% if intraday_discipline %}
|
||||
{% elif o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||
</span>
|
||||
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
|
||||
</div>
|
||||
|
||||
@@ -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 %}
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
||||
</label>
|
||||
@@ -157,6 +158,9 @@
|
||||
<option value="4" selected>4h</option>
|
||||
</select>
|
||||
</span>
|
||||
{% else %}
|
||||
<input type="hidden" name="breakeven_enabled" value="0">
|
||||
{% endif %}
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
|
||||
</label>
|
||||
@@ -212,8 +216,10 @@
|
||||
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
|
||||
</div>
|
||||
<div class="pos-head-actions">
|
||||
{% if not intraday_discipline %}
|
||||
<button type="button" class="pos-entrust-btn" onclick="openTpslEntrustModal({{ o.id }})">委托</button>
|
||||
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pos-meta">
|
||||
@@ -221,8 +227,9 @@
|
||||
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
|
||||
<span class="pos-meta-item">风险: {% 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 %}</span>
|
||||
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
|
||||
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
|
||||
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||
<span class="pos-meta-item {% if not intraday_discipline %}{% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}{% endif %}">
|
||||
{% if intraday_discipline %}
|
||||
{% elif o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||
</span>
|
||||
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{# 趋势户:两级开仓类型 → 自动 trade_style;日内户:仍选手动 trend/swing #}
|
||||
{# 趋势户:两级开仓类型 → 自动 trade_style;日内户:假破 / 结构突破 #}
|
||||
|
||||
{% macro order_entry_type_fields() -%}
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
</select>
|
||||
|
||||
<select name="entry_model" id="order-entry-model" class="order-entry-model-sub" required disabled title="启动A/B、大分歧A/B、小分歧" aria-label="开仓类型">
|
||||
|
||||
<option value="">类型</option>
|
||||
|
||||
{% for cat in entry_model_categories %}
|
||||
|
||||
{% for opt in cat.options %}
|
||||
|
||||
<option value="{{ opt.code }}" data-entry-category="{{ cat.key }}" data-trade-style="{{ opt.trade_style }}"{% if opt.help %} title="{{ opt.help }}"{% endif %} hidden disabled>{{ opt.label }}</option>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ ENTRY_MODEL_BIG_DIV_A = "big_div_a"
|
||||
ENTRY_MODEL_BIG_DIV_B = "big_div_b"
|
||||
ENTRY_MODEL_SMALL_DIV = "small_div"
|
||||
|
||||
ENTRY_CATEGORY_INTRADAY = "intraday"
|
||||
ENTRY_MODEL_LIQUIDITY_FALSE_BREAK = "liquidity_false_break"
|
||||
ENTRY_MODEL_STRUCTURE_BREAKOUT = "structure_breakout"
|
||||
|
||||
VALID_ENTRY_MODEL_CODES = frozenset(
|
||||
{
|
||||
ENTRY_MODEL_LAUNCH_A,
|
||||
@@ -29,6 +33,15 @@ VALID_ENTRY_MODEL_CODES = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
INTRADAY_ENTRY_MODEL_CODES = frozenset(
|
||||
{
|
||||
ENTRY_MODEL_LIQUIDITY_FALSE_BREAK,
|
||||
ENTRY_MODEL_STRUCTURE_BREAKOUT,
|
||||
}
|
||||
)
|
||||
|
||||
ALL_ENTRY_MODEL_CODES = VALID_ENTRY_MODEL_CODES | INTRADAY_ENTRY_MODEL_CODES
|
||||
|
||||
ENTRY_CATEGORY_LABELS: dict[str, str] = {
|
||||
ENTRY_CATEGORY_REVERSAL: "反转",
|
||||
ENTRY_CATEGORY_TREND: "顺势",
|
||||
@@ -90,11 +103,33 @@ _ENTRY_SPECS: Tuple[Tuple[str, str, str, str, str], ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
_INTRADAY_ENTRY_SPECS: Tuple[Tuple[str, str, str, str, str], ...] = (
|
||||
(
|
||||
ENTRY_MODEL_LIQUIDITY_FALSE_BREAK,
|
||||
"假破",
|
||||
ENTRY_CATEGORY_INTRADAY,
|
||||
"trend",
|
||||
"流动性扫单 → 假突破验证 → 5m N 字 → 15m 顶/底分型",
|
||||
),
|
||||
(
|
||||
ENTRY_MODEL_STRUCTURE_BREAKOUT,
|
||||
"结构突破",
|
||||
ENTRY_CATEGORY_INTRADAY,
|
||||
"trend",
|
||||
"15m 结构有效突破(收盘确认)",
|
||||
),
|
||||
)
|
||||
|
||||
_CODE_TO_LABEL = {code: label for code, label, _, _, _ in _ENTRY_SPECS}
|
||||
_CODE_TO_LABEL.update({code: label for code, label, _, _, _ in _INTRADAY_ENTRY_SPECS})
|
||||
_CODE_TO_STYLE = {code: style for code, _, _, style, _ in _ENTRY_SPECS}
|
||||
_CODE_TO_STYLE.update({code: style for code, _, _, style, _ in _INTRADAY_ENTRY_SPECS})
|
||||
_CODE_TO_CATEGORY = {code: cat for code, _, cat, _, _ in _ENTRY_SPECS}
|
||||
_CODE_TO_CATEGORY.update({code: cat for code, _, cat, _, _ in _INTRADAY_ENTRY_SPECS})
|
||||
_LABEL_TO_CODE = {label: code for code, label, _, _, _ in _ENTRY_SPECS}
|
||||
_LABEL_TO_CODE.update({label: code for code, label, _, _, _ in _INTRADAY_ENTRY_SPECS})
|
||||
_CODE_TO_HELP = {code: help for code, _, _, _, help in _ENTRY_SPECS}
|
||||
_CODE_TO_HELP.update({code: help for code, _, _, _, help in _INTRADAY_ENTRY_SPECS})
|
||||
|
||||
_CATEGORY_ORDER: Tuple[str, ...] = (
|
||||
ENTRY_CATEGORY_REVERSAL,
|
||||
@@ -134,6 +169,13 @@ def entry_model_options() -> Tuple[EntryModelOption, ...]:
|
||||
)
|
||||
|
||||
|
||||
def intraday_entry_model_options() -> Tuple[EntryModelOption, ...]:
|
||||
return tuple(
|
||||
EntryModelOption(code=code, label=label, category=cat, trade_style=style, help=help)
|
||||
for code, label, cat, style, help in _INTRADAY_ENTRY_SPECS
|
||||
)
|
||||
|
||||
|
||||
def entry_model_categories() -> list[dict[str, Any]]:
|
||||
"""两级 UI:反转 / 顺势 / 波段 → 子选项。"""
|
||||
opts = entry_model_options()
|
||||
@@ -168,7 +210,7 @@ def entry_model_category(code: Optional[str]) -> str:
|
||||
|
||||
def normalize_entry_model_code(raw: Optional[str]) -> str:
|
||||
v = (raw or "").strip().lower()
|
||||
if v in VALID_ENTRY_MODEL_CODES:
|
||||
if v in ALL_ENTRY_MODEL_CODES:
|
||||
return v
|
||||
label = (raw or "").strip()
|
||||
if label in _LABEL_TO_CODE:
|
||||
@@ -227,10 +269,13 @@ def parse_manual_order_style_fields(
|
||||
) -> Tuple[str, Optional[str], Optional[str]]:
|
||||
"""返回 (trade_style, entry_model_code|None, error_message|None)。"""
|
||||
if is_intraday_trading_profile(policy):
|
||||
trade_style = (form.get("trade_style") or default_trade_style or "trend").strip().lower()
|
||||
if trade_style not in ("trend", "swing"):
|
||||
trade_style = "trend"
|
||||
return trade_style, None, None
|
||||
entry_model = normalize_entry_model_code(form.get("entry_model"))
|
||||
if entry_model in INTRADAY_ENTRY_MODEL_CODES:
|
||||
return "trend", entry_model, None
|
||||
raw_style = (form.get("trade_style") or "").strip().lower()
|
||||
if raw_style in ("trend", "swing"):
|
||||
return raw_style, None, None
|
||||
return "", None, "请选择开仓类型(假破 / 结构突破)"
|
||||
|
||||
entry_model = normalize_entry_model_code(form.get("entry_model"))
|
||||
if not entry_model:
|
||||
@@ -280,12 +325,31 @@ def enrich_entry_model_display(item: dict) -> dict:
|
||||
return item
|
||||
|
||||
|
||||
def open_position_button_label(policy: TradePolicy, sizing_mode: str) -> str:
|
||||
from lib.trade.position_sizing_lib import mode_label_zh
|
||||
|
||||
mode_txt = mode_label_zh(sizing_mode)
|
||||
if is_intraday_trading_profile(policy):
|
||||
return f"开仓(日内·{mode_txt})"
|
||||
return f"开仓({mode_txt})"
|
||||
|
||||
|
||||
def order_entry_template_context(policy: TradePolicy) -> dict:
|
||||
profile = order_entry_profile(policy)
|
||||
opts = entry_model_options()
|
||||
intraday_opts = intraday_entry_model_options()
|
||||
return {
|
||||
"order_entry_profile": profile,
|
||||
"intraday_discipline": profile == PROFILE_INTRADAY,
|
||||
"intraday_entry_model_options": [
|
||||
{
|
||||
"code": o.code,
|
||||
"label": o.label,
|
||||
"trade_style": o.trade_style,
|
||||
"help": o.help,
|
||||
}
|
||||
for o in intraday_opts
|
||||
],
|
||||
"entry_model_options": [
|
||||
{
|
||||
"code": o.code,
|
||||
|
||||
@@ -13,7 +13,9 @@ from lib.trade.entry_model_lib import (
|
||||
entry_model_category,
|
||||
entry_model_label,
|
||||
hub_meta_entry_context,
|
||||
intraday_entry_model_options,
|
||||
is_intraday_trading_profile,
|
||||
open_position_button_label,
|
||||
parse_manual_order_style_fields,
|
||||
resolve_trade_record_entry_reason,
|
||||
trade_style_for_entry_model,
|
||||
@@ -95,6 +97,30 @@ class TestEntryModelLib(unittest.TestCase):
|
||||
self.assertIsNone(code)
|
||||
self.assertEqual(style, "swing")
|
||||
|
||||
def test_intraday_entry_model_options(self):
|
||||
opts = intraday_entry_model_options()
|
||||
codes = [o.code for o in opts]
|
||||
self.assertEqual(codes, ["liquidity_false_break", "structure_breakout"])
|
||||
self.assertEqual(entry_model_label("liquidity_false_break"), "假破")
|
||||
|
||||
def test_parse_intraday_requires_entry_model(self):
|
||||
policy = TradePolicy(True, "both", True, ("BTC", "ETH"))
|
||||
style, code, err = parse_manual_order_style_fields(policy, {})
|
||||
self.assertTrue(err)
|
||||
style, code, err = parse_manual_order_style_fields(
|
||||
policy, {"entry_model": "structure_breakout"}
|
||||
)
|
||||
self.assertIsNone(err)
|
||||
self.assertEqual(code, "structure_breakout")
|
||||
self.assertEqual(style, "trend")
|
||||
|
||||
def test_open_position_button_intraday(self):
|
||||
policy = TradePolicy(True, "both", True, ("BTC", "ETH"))
|
||||
self.assertEqual(
|
||||
open_position_button_label(policy, "full_margin"),
|
||||
"开仓(日内·全仓杠杆)",
|
||||
)
|
||||
|
||||
def test_resolve_entry_reason_from_model(self):
|
||||
er = resolve_trade_record_entry_reason(entry_model=ENTRY_MODEL_BIG_DIV_B)
|
||||
self.assertEqual(er, "大分歧B")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import unittest
|
||||
|
||||
from lib.hub.price_snapshot_lib import resolve_order_snapshot_price
|
||||
|
||||
|
||||
class TestPriceSnapshotLib(unittest.TestCase):
|
||||
def test_resolve_from_cached_prices(self):
|
||||
px = resolve_order_snapshot_price("ETH/USDT", {"ETH/USDT": 1750.5})
|
||||
self.assertEqual(px, 1750.5)
|
||||
|
||||
def test_resolve_from_position_mark(self):
|
||||
prow = {"info": {"mark_price": 1760.0}, "contracts": 1}
|
||||
px = resolve_order_snapshot_price("ETH/USDT", {}, position_row=prow)
|
||||
self.assertEqual(px, 1760.0)
|
||||
|
||||
def test_resolve_mark_fn_before_entry(self):
|
||||
px = resolve_order_snapshot_price(
|
||||
"ETH/USDT",
|
||||
{},
|
||||
get_mark_price_fn=lambda s: 1755.0,
|
||||
fallback_entry=1700.0,
|
||||
)
|
||||
self.assertEqual(px, 1755.0)
|
||||
|
||||
def test_resolve_fallback_entry(self):
|
||||
px = resolve_order_snapshot_price(
|
||||
"ETH/USDT",
|
||||
{},
|
||||
fallback_entry=1700.0,
|
||||
)
|
||||
self.assertEqual(px, 1700.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user