Show product name, main contract badge, and exchange on position cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 10:55:37 +08:00
parent 42f2dad52a
commit deb9501cbe
4 changed files with 78 additions and 13 deletions
+16 -6
View File
@@ -67,7 +67,7 @@ from strategy.strategy_roll_lib import preview_roll
from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot
from strategy.strategy_trend_lib import compute_trend_plan_futures, trend_dca_level_reached
from strategy.strategy_snapshot_lib import STRATEGY_ROLL, STRATEGY_TREND
from symbols import ths_to_codes, resolve_main_contract, PRODUCTS, PRODUCT_CATEGORIES
from symbols import ths_to_codes, resolve_main_contract, PRODUCTS, PRODUCT_CATEGORIES, position_symbol_meta
from trading_context import (
TRADING_MODE_LIVE,
TRADING_MODE_SIM,
@@ -112,6 +112,16 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
return "固定金额"
return "固定手数"
def _symbol_display_fields(sym: str) -> dict:
meta = position_symbol_meta(sym)
name = meta.get("name") or sym
return {
"symbol": name,
"symbol_name": name,
"symbol_exchange": meta.get("exchange") or "",
"symbol_is_main": bool(meta.get("is_main")),
}
def _schedule_recommend_refresh() -> None:
from db_conn import DB_PATH
@@ -338,12 +348,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
lots = int(mon.get("lots") or 0)
base = {
"symbol_code": sym,
"symbol": mon.get("symbol_name") or sym,
"direction": direction,
"direction_label": "做多" if direction == "long" else "做空",
"lots": lots,
"source": "monitor",
"monitor_id": mon.get("id"),
**_symbol_display_fields(sym),
}
sl = mon.get("stop_loss")
tp = mon.get("take_profit")
@@ -368,7 +378,6 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
sym = mon.get("symbol") or ""
pending.append({
"symbol_code": sym,
"symbol": mon.get("symbol_name") or sym,
"direction": mon.get("direction") or "long",
"direction_label": "做多" if (mon.get("direction") or "long") == "long" else "做空",
"lots": int(mon.get("lots") or 0),
@@ -379,6 +388,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"monitor_id": mon.get("id"),
"can_cancel_order": is_trading_session(),
"cancel_allowed": is_trading_session(),
**_symbol_display_fields(sym),
})
ctp_st = ctp_status(mode)
if ctp_st.get("connected"):
@@ -391,7 +401,6 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
label = "平仓委托"
pending.append({
"symbol_code": sym,
"symbol": sym,
"direction": o.get("direction") or "long",
"direction_label": "做多" if o.get("direction") == "long" else "做空",
"lots": int(o.get("lots") or 0),
@@ -402,6 +411,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"order_id": o.get("order_id"),
"can_cancel_order": is_trading_session(),
"cancel_allowed": is_trading_session(),
**_symbol_display_fields(sym),
})
return pending
@@ -767,8 +777,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"source_label": source_label,
"sync_pending": ctp is None and mon is not None,
"monitor_id": mon["id"] if mon else None,
"symbol": codes.get("name", sym) if codes else (mon.get("symbol_name") if mon else sym),
"symbol_code": sym,
**_symbol_display_fields(sym),
"direction": direction,
"direction_label": "做多" if direction == "long" else "做空",
"lots": lots,
@@ -838,8 +848,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"source_label": "委托挂单中",
"sync_pending": True,
"monitor_id": mon.get("id"),
"symbol": codes.get("name", sym) if codes else (mon.get("symbol_name") or sym),
"symbol_code": sym,
**_symbol_display_fields(sym),
"direction": direction,
"direction_label": "做多" if direction == "long" else "做空",
"lots": lots,
+3 -1
View File
@@ -65,7 +65,9 @@
.gap-badge{font-size:.72rem}
.rec-market-link{color:inherit;text-decoration:none;display:inline-flex;flex-wrap:wrap;align-items:baseline;gap:.2rem .35rem}
.rec-market-link:hover strong,.rec-market-link:hover .text-accent{color:var(--accent);text-decoration:underline}
.rec-change-up{color:var(--profit)}
.pos-symbol-sub{font-size:.72rem;line-height:1.35}
.pos-main-badge{font-size:.68rem;vertical-align:middle}
.pos-change-up{color:var(--profit)}
.rec-change-down{color:var(--loss)}
#recommend .trade-table-wrap{max-height:min(70vh,520px)}
#positions .card-body.card-scroll{flex:1;max-height:none;overflow-y:auto}
+26 -6
View File
@@ -806,6 +806,25 @@
return '<span class="text-muted">未开启</span>';
}
function posSymbolTitleHtml(row, extraBadges) {
extraBadges = extraBadges || '';
var name = row.symbol_name || row.symbol || '';
var code = row.symbol_code || '';
var mainBadge = row.symbol_is_main ? ' <span class="badge planned pos-main-badge">主力</span>' : '';
var title = name + mainBadge;
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
title += ' <span class="text-accent">' + code + '</span>';
} else if (!name && code) {
title = '<span class="text-accent">' + code + '</span>';
}
return title + extraBadges;
}
function posSymbolSubHtml(row) {
if (row.symbol_exchange) return row.symbol_exchange;
return row.symbol_code || '';
}
function buildPendingOrderCard(row) {
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
@@ -827,10 +846,10 @@
' · <span class="text-muted">约 ' + remainMin + ' 分钟内未成交自动撤单</span>';
return (
'<div class="pos-card is-pending">' +
'<div class="pos-card-head"><div><div class="title">' + row.symbol +
' <span class="badge dir">' + dirBadge + '</span>' +
' <span class="badge pending">挂单中</span></div>' +
'<div class="text-muted" style="font-size:.72rem">' + (row.symbol_code || '') + '</div></div>' +
'<div class="pos-card-head"><div><div class="title">' + posSymbolTitleHtml(row,
' <span class="badge dir">' + dirBadge + '</span>' +
' <span class="badge pending">挂单中</span>') + '</div>' +
'<div class="text-muted pos-symbol-sub">' + posSymbolSubHtml(row) + '</div></div>' +
'<div class="pos-card-actions">' + cancelBtn + '</div></div>' +
'<div class="pos-card-meta pos-card-meta-line">' + metaLine + '</div>' +
'<div class="pos-metrics">' +
@@ -897,8 +916,9 @@
var openLabel = '开仓';
return (
'<div class="pos-card">' +
'<div class="pos-card-head"><div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div>' +
'<div class="text-muted" style="font-size:.72rem">' + (row.symbol_code || '') + '</div></div>' +
'<div class="pos-card-head"><div><div class="title">' + posSymbolTitleHtml(row,
' <span class="badge dir">' + dirBadge + '</span>') + '</div>' +
'<div class="text-muted pos-symbol-sub">' + posSymbolSubHtml(row) + '</div></div>' +
actionBtns + '</div>' +
'<div class="pos-card-meta pos-card-meta-line">' + metaLine + '</div>' +
'<div class="pos-metrics">' +
+33
View File
@@ -458,6 +458,39 @@ def _product_for_ths(ths: str) -> Optional[dict]:
return _THS_TO_PRODUCT.get(key) or _THS_TO_PRODUCT.get(key.lower())
def _product_for_contract_code(ths_code: str) -> Optional[dict]:
sym = (ths_code or "").strip()
if not sym:
return None
m = re.match(r"^([A-Za-z]+)", sym)
if not m:
return None
return _find_product_by_letters(m.group(1))
def position_symbol_meta(ths_code: str) -> dict:
"""持仓/委托展示:品种名、交易所、是否主力合约。"""
sym = (ths_code or "").strip()
if not sym:
return {"name": "", "exchange": "", "is_main": False}
product = _product_for_contract_code(sym)
if not product:
return {"name": sym, "exchange": "", "is_main": False}
codes = ths_to_codes(sym)
norm = (codes["ths_code"] if codes else sym).strip().lower()
is_main = False
with _main_index_lock:
main_item = _main_index.get(product["sina"])
if main_item:
main_ths = (main_item.get("ths_code") or "").strip().lower()
is_main = main_ths == norm or main_ths == sym.lower()
return {
"name": product["name"],
"exchange": product.get("exchange") or "",
"is_main": is_main,
}
def _item_from_recommend_row(row: dict, product: dict) -> Optional[dict]:
"""由可开仓缓存行快速构造下拉项(不在 HTTP 请求中解析主力)。"""
name = row.get("name") or product["name"]