feat: 持仓监控数据库优先显示,修复开仓重复与同步前空白
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+345
-168
@@ -248,142 +248,343 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _canonical_position_key(symbol: str, direction: str) -> str:
|
||||||
|
sym = (symbol or "").strip()
|
||||||
|
d = (direction or "long").strip().lower()
|
||||||
|
try:
|
||||||
|
vnpy_sym, _ = ths_to_vnpy_symbol(sym)
|
||||||
|
return f"{vnpy_sym.lower()}:{d}"
|
||||||
|
except Exception:
|
||||||
|
return f"{sym.lower()}:{d}"
|
||||||
|
|
||||||
|
def _find_active_monitor(conn, symbol: str, direction: str) -> Optional[dict]:
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
|
||||||
|
).fetchall():
|
||||||
|
row = dict(r)
|
||||||
|
if (row.get("direction") or "long") != direction:
|
||||||
|
continue
|
||||||
|
if _match_ctp_symbol(symbol, row.get("symbol") or ""):
|
||||||
|
return row
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _close_duplicate_monitors(conn, symbol: str, direction: str, keep_id: int) -> None:
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT id, symbol, direction FROM trade_order_monitors WHERE status='active'"
|
||||||
|
).fetchall():
|
||||||
|
if int(r["id"]) == int(keep_id):
|
||||||
|
continue
|
||||||
|
if (r["direction"] or "long") != direction:
|
||||||
|
continue
|
||||||
|
if _match_ctp_symbol(symbol, r["symbol"] or ""):
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE trade_order_monitors SET status='closed' WHERE id=?",
|
||||||
|
(r["id"],),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _upsert_open_monitor(
|
||||||
|
conn,
|
||||||
|
*,
|
||||||
|
sym: str,
|
||||||
|
direction: str,
|
||||||
|
lots: int,
|
||||||
|
price: float,
|
||||||
|
sl,
|
||||||
|
tp,
|
||||||
|
trailing_be: int,
|
||||||
|
) -> int:
|
||||||
|
ensure_monitor_order_columns(conn)
|
||||||
|
codes = ths_to_codes(sym) or {}
|
||||||
|
sl_f = float(sl) if sl not in (None, "") else None
|
||||||
|
tp_f = float(tp) if tp not in (None, "") else None
|
||||||
|
now_s = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
existing = _find_active_monitor(conn, sym, direction)
|
||||||
|
if existing:
|
||||||
|
mid = int(existing["id"])
|
||||||
|
initial_sl = existing.get("initial_stop_loss")
|
||||||
|
if sl_f is not None and initial_sl is None:
|
||||||
|
initial_sl = sl_f
|
||||||
|
conn.execute(
|
||||||
|
"""UPDATE trade_order_monitors SET
|
||||||
|
symbol=?, symbol_name=?, market_code=?, lots=?, entry_price=?,
|
||||||
|
stop_loss=?, take_profit=?, initial_stop_loss=?, trailing_be=?, open_time=?
|
||||||
|
WHERE id=?""",
|
||||||
|
(
|
||||||
|
sym,
|
||||||
|
codes.get("name", sym),
|
||||||
|
codes.get("market_code", ""),
|
||||||
|
lots,
|
||||||
|
price,
|
||||||
|
sl_f,
|
||||||
|
tp_f,
|
||||||
|
initial_sl,
|
||||||
|
trailing_be,
|
||||||
|
now_s,
|
||||||
|
mid,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO trade_order_monitors (
|
||||||
|
symbol, symbol_name, market_code, direction, lots, entry_price,
|
||||||
|
stop_loss, take_profit, initial_stop_loss, trailing_be,
|
||||||
|
open_time, monitor_type, status
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?, 'active')""",
|
||||||
|
(
|
||||||
|
sym,
|
||||||
|
codes.get("name", sym),
|
||||||
|
codes.get("market_code", ""),
|
||||||
|
direction,
|
||||||
|
lots,
|
||||||
|
price,
|
||||||
|
sl_f,
|
||||||
|
tp_f,
|
||||||
|
sl_f,
|
||||||
|
trailing_be,
|
||||||
|
now_s,
|
||||||
|
"manual",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mid = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
|
||||||
|
_close_duplicate_monitors(conn, sym, direction, mid)
|
||||||
|
return mid
|
||||||
|
|
||||||
|
def _sync_monitor_lots_from_ctp(conn, mid: int, sym: str, direction: str, mode: str) -> None:
|
||||||
|
for p in _ctp_positions(mode):
|
||||||
|
if int(p.get("lots") or 0) <= 0:
|
||||||
|
continue
|
||||||
|
if (p.get("direction") or "long") != direction:
|
||||||
|
continue
|
||||||
|
if not _match_ctp_symbol(p.get("symbol") or "", sym):
|
||||||
|
continue
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE trade_order_monitors SET lots=?, entry_price=? WHERE id=?",
|
||||||
|
(int(p.get("lots") or 0), float(p.get("avg_price") or 0), mid),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _compose_position_row(
|
||||||
|
conn,
|
||||||
|
*,
|
||||||
|
mon: Optional[dict],
|
||||||
|
ctp: Optional[dict],
|
||||||
|
mode: str,
|
||||||
|
capital: float,
|
||||||
|
now_iso: str,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
if not mon and not ctp:
|
||||||
|
return None
|
||||||
|
if ctp:
|
||||||
|
sym = (ctp.get("symbol") or "").strip()
|
||||||
|
direction = ctp.get("direction") or "long"
|
||||||
|
lots = int(ctp.get("lots") or 0)
|
||||||
|
if lots <= 0:
|
||||||
|
return None
|
||||||
|
entry = float(ctp.get("avg_price") or 0)
|
||||||
|
float_pnl = ctp.get("pnl")
|
||||||
|
if float_pnl is not None:
|
||||||
|
float_pnl = round(float(float_pnl), 2)
|
||||||
|
source_label = "CTP 柜台"
|
||||||
|
else:
|
||||||
|
sym = (mon.get("symbol") or "").strip()
|
||||||
|
direction = mon.get("direction") or "long"
|
||||||
|
lots = int(mon.get("lots") or 0)
|
||||||
|
if lots <= 0:
|
||||||
|
return None
|
||||||
|
entry = float(mon.get("entry_price") or 0)
|
||||||
|
float_pnl = None
|
||||||
|
source_label = "本地监控"
|
||||||
|
|
||||||
|
codes = ths_to_codes(sym)
|
||||||
|
tick = calc_order_tick_metrics(sym, lots, entry)
|
||||||
|
sl = float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None
|
||||||
|
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
|
||||||
|
open_time = (mon.get("open_time") or "") if mon else ""
|
||||||
|
holding = _holding_duration(open_time, now_iso) if open_time else ""
|
||||||
|
|
||||||
|
mark = None
|
||||||
|
if ctp_status(mode).get("connected"):
|
||||||
|
mark = ctp_get_tick_price(mode, sym)
|
||||||
|
if (mark is None or mark <= 0) and codes:
|
||||||
|
mark = fetch_price(
|
||||||
|
sym,
|
||||||
|
codes.get("market_code", ""),
|
||||||
|
codes.get("sina_code", ""),
|
||||||
|
)
|
||||||
|
close_est = float(mark) if mark and mark > 0 else entry
|
||||||
|
if float_pnl is None and mark and entry:
|
||||||
|
pos_tmp = calc_position_metrics(
|
||||||
|
direction, entry, sl or entry, tp or entry, lots, mark, capital, sym,
|
||||||
|
)
|
||||||
|
float_pnl = pos_tmp.get("float_pnl")
|
||||||
|
|
||||||
|
fee_info = calc_fee_breakdown(
|
||||||
|
sym, entry, close_est, lots, open_time or now_iso, now_iso, trading_mode=mode,
|
||||||
|
)
|
||||||
|
est_net = None
|
||||||
|
if float_pnl is not None:
|
||||||
|
est_net = round(float(float_pnl) - fee_info["total_fee"], 2)
|
||||||
|
pos_metrics = calc_position_metrics(
|
||||||
|
direction, entry, sl if sl is not None else entry,
|
||||||
|
tp if tp is not None else entry, lots, mark, capital, sym,
|
||||||
|
)
|
||||||
|
order_st = monitor_order_status(
|
||||||
|
mon or {}, mode=mode, ths_code=sym, direction=direction,
|
||||||
|
)
|
||||||
|
pending_for_row: list[dict] = []
|
||||||
|
if sl is not None:
|
||||||
|
pending_for_row.append({
|
||||||
|
"order_kind": "stop_loss",
|
||||||
|
"label": "止损监控",
|
||||||
|
"price": sl,
|
||||||
|
"lots": lots,
|
||||||
|
"source": "monitor",
|
||||||
|
"monitor_id": mon["id"] if mon else None,
|
||||||
|
})
|
||||||
|
if tp is not None:
|
||||||
|
pending_for_row.append({
|
||||||
|
"order_kind": "take_profit",
|
||||||
|
"label": "止盈监控",
|
||||||
|
"price": tp,
|
||||||
|
"lots": lots,
|
||||||
|
"source": "monitor",
|
||||||
|
"monitor_id": mon["id"] if mon else None,
|
||||||
|
})
|
||||||
|
row_key = _canonical_position_key(sym, direction)
|
||||||
|
return {
|
||||||
|
"key": row_key,
|
||||||
|
"source": "ctp" if ctp else "local",
|
||||||
|
"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,
|
||||||
|
"direction": direction,
|
||||||
|
"direction_label": "做多" if direction == "long" else "做空",
|
||||||
|
"lots": lots,
|
||||||
|
"entry_price": entry,
|
||||||
|
"stop_loss": sl,
|
||||||
|
"take_profit": tp,
|
||||||
|
"open_time": open_time or None,
|
||||||
|
"holding_duration": holding or None,
|
||||||
|
"mark_price": mark,
|
||||||
|
"current_price": mark,
|
||||||
|
"margin": pos_metrics.get("margin"),
|
||||||
|
"position_pct": pos_metrics.get("position_pct"),
|
||||||
|
"float_pnl": float_pnl,
|
||||||
|
"est_fee": fee_info["total_fee"],
|
||||||
|
"est_fee_open": fee_info["open_fee"],
|
||||||
|
"est_fee_close": fee_info["close_fee"],
|
||||||
|
"est_fee_close_type": fee_info["close_type"],
|
||||||
|
"est_pnl_net": est_net,
|
||||||
|
"sl_order_active": order_st.get("sl_monitoring"),
|
||||||
|
"tp_order_active": order_st.get("tp_monitoring"),
|
||||||
|
"sl_monitoring": order_st.get("sl_monitoring"),
|
||||||
|
"tp_monitoring": order_st.get("tp_monitoring"),
|
||||||
|
"can_place_orders": False,
|
||||||
|
"tick_value_total": tick.get("tick_value_total"),
|
||||||
|
"price_precision": tick.get("price_precision"),
|
||||||
|
"tick_size": tick.get("tick_size"),
|
||||||
|
"can_close": True,
|
||||||
|
"pending_orders": pending_for_row,
|
||||||
|
"trailing_be": bool(mon.get("trailing_be")) if mon else False,
|
||||||
|
"trailing_r_locked": int(mon.get("trailing_r_locked") or 0) if mon else 0,
|
||||||
|
}
|
||||||
|
|
||||||
def _build_trading_live_rows(conn) -> list[dict]:
|
def _build_trading_live_rows(conn) -> list[dict]:
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
tz = ZoneInfo("Asia/Shanghai")
|
tz = ZoneInfo("Asia/Shanghai")
|
||||||
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
|
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
|
||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
ctp_st = ctp_status(mode)
|
|
||||||
rows: list[dict] = []
|
|
||||||
capital = _capital(conn)
|
capital = _capital(conn)
|
||||||
|
|
||||||
if not ctp_st.get("connected"):
|
|
||||||
return rows
|
|
||||||
|
|
||||||
ensure_monitor_order_columns(conn)
|
ensure_monitor_order_columns(conn)
|
||||||
|
|
||||||
# 程序监控仅用于补充止损/止盈,持仓以 CTP 柜台为准
|
monitors_raw = [
|
||||||
monitor_map: dict[tuple[str, str], dict] = {}
|
dict(r) for r in conn.execute(
|
||||||
for r in conn.execute(
|
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
|
||||||
"SELECT * FROM trade_order_monitors WHERE status='active'"
|
).fetchall()
|
||||||
).fetchall():
|
]
|
||||||
key = (r["symbol"].lower(), r["direction"])
|
monitor_by_key: dict[str, dict] = {}
|
||||||
monitor_map[key] = dict(r)
|
for mon in monitors_raw:
|
||||||
|
key = _canonical_position_key(mon.get("symbol") or "", mon.get("direction") or "long")
|
||||||
|
if key not in monitor_by_key:
|
||||||
|
monitor_by_key[key] = mon
|
||||||
|
|
||||||
for p in _ctp_positions(mode):
|
ctp_list: list[dict] = _ctp_positions(mode) if ctp_status(mode).get("connected") else []
|
||||||
sym = (p.get("symbol") or "").strip()
|
ctp_by_key: dict[str, dict] = {}
|
||||||
direction = p.get("direction") or "long"
|
for p in ctp_list:
|
||||||
lots = int(p.get("lots") or 0)
|
if int(p.get("lots") or 0) <= 0:
|
||||||
if lots <= 0:
|
|
||||||
continue
|
continue
|
||||||
entry = float(p.get("avg_price") or 0)
|
key = _canonical_position_key(p.get("symbol") or "", p.get("direction") or "long")
|
||||||
float_pnl = p.get("pnl")
|
ctp_by_key[key] = p
|
||||||
if float_pnl is not None:
|
|
||||||
float_pnl = round(float(float_pnl), 2)
|
rows: list[dict] = []
|
||||||
codes = ths_to_codes(sym)
|
used_ctp_keys: set[str] = set()
|
||||||
tick = calc_order_tick_metrics(sym, lots, entry)
|
|
||||||
mon = None
|
for key, mon in monitor_by_key.items():
|
||||||
for (ms, md), mv in monitor_map.items():
|
ctp = ctp_by_key.get(key)
|
||||||
if md != direction:
|
if not ctp:
|
||||||
continue
|
for ck, cp in ctp_by_key.items():
|
||||||
if ms == sym.lower() or _match_ctp_symbol(sym, ms):
|
if ck in used_ctp_keys:
|
||||||
mon = mv
|
continue
|
||||||
break
|
if (cp.get("direction") or "long") != (mon.get("direction") or "long"):
|
||||||
sl = float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None
|
continue
|
||||||
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
|
if _match_ctp_symbol(cp.get("symbol") or "", mon.get("symbol") or ""):
|
||||||
open_time = (mon.get("open_time") or "") if mon else ""
|
ctp = cp
|
||||||
holding = _holding_duration(open_time, now_iso) if open_time else ""
|
used_ctp_keys.add(ck)
|
||||||
mark = ctp_get_tick_price(mode, sym)
|
break
|
||||||
if (mark is None or mark <= 0) and codes:
|
elif key in ctp_by_key:
|
||||||
mark = fetch_price(
|
used_ctp_keys.add(key)
|
||||||
sym,
|
if ctp and mon:
|
||||||
codes.get("market_code", ""),
|
_sync_monitor_lots_from_ctp(
|
||||||
codes.get("sina_code", ""),
|
conn, int(mon["id"]), mon.get("symbol") or "",
|
||||||
|
mon.get("direction") or "long", mode,
|
||||||
)
|
)
|
||||||
close_est = float(mark) if mark and mark > 0 else entry
|
mon = _find_active_monitor(conn, mon.get("symbol") or "", mon.get("direction") or "long") or mon
|
||||||
fee_info = calc_fee_breakdown(
|
row = _compose_position_row(
|
||||||
sym,
|
conn, mon=mon, ctp=ctp, mode=mode, capital=capital, now_iso=now_iso,
|
||||||
entry,
|
|
||||||
close_est,
|
|
||||||
lots,
|
|
||||||
open_time or now_iso,
|
|
||||||
now_iso,
|
|
||||||
trading_mode=mode,
|
|
||||||
)
|
)
|
||||||
est_net = None
|
if row:
|
||||||
if float_pnl is not None:
|
rows.append(row)
|
||||||
est_net = round(float(float_pnl) - fee_info["total_fee"], 2)
|
|
||||||
pos_metrics = calc_position_metrics(
|
for key, ctp in ctp_by_key.items():
|
||||||
direction,
|
if key in used_ctp_keys:
|
||||||
entry,
|
continue
|
||||||
sl if sl is not None else entry,
|
matched = False
|
||||||
tp if tp is not None else entry,
|
for uk in used_ctp_keys:
|
||||||
lots,
|
if uk == key:
|
||||||
mark,
|
matched = True
|
||||||
capital,
|
break
|
||||||
sym,
|
if matched:
|
||||||
|
continue
|
||||||
|
for existing in rows:
|
||||||
|
if _match_ctp_symbol(
|
||||||
|
ctp.get("symbol") or "", existing.get("symbol_code") or "",
|
||||||
|
) and (ctp.get("direction") or "long") == (existing.get("direction") or "long"):
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
if matched:
|
||||||
|
continue
|
||||||
|
mon = _find_active_monitor(
|
||||||
|
conn, ctp.get("symbol") or "", ctp.get("direction") or "long",
|
||||||
)
|
)
|
||||||
order_st = monitor_order_status(
|
row = _compose_position_row(
|
||||||
mon or {}, mode=mode, ths_code=sym, direction=direction,
|
conn, mon=mon, ctp=ctp, mode=mode, capital=capital, now_iso=now_iso,
|
||||||
)
|
)
|
||||||
pending_for_row: list[dict] = []
|
if row:
|
||||||
if sl is not None:
|
rows.append(row)
|
||||||
pending_for_row.append({
|
|
||||||
"order_kind": "stop_loss",
|
seen: set[str] = set()
|
||||||
"label": "止损监控",
|
deduped: list[dict] = []
|
||||||
"price": sl,
|
for row in rows:
|
||||||
"lots": lots,
|
rk = row.get("key") or f"{row.get('symbol_code')}:{row.get('direction')}"
|
||||||
"source": "monitor",
|
if rk in seen:
|
||||||
"monitor_id": mon["id"] if mon else None,
|
continue
|
||||||
})
|
seen.add(rk)
|
||||||
if tp is not None:
|
deduped.append(row)
|
||||||
pending_for_row.append({
|
return deduped
|
||||||
"order_kind": "take_profit",
|
|
||||||
"label": "止盈监控",
|
|
||||||
"price": tp,
|
|
||||||
"lots": lots,
|
|
||||||
"source": "monitor",
|
|
||||||
"monitor_id": mon["id"] if mon else None,
|
|
||||||
})
|
|
||||||
rows.append({
|
|
||||||
"key": f"ctp:{sym.lower()}:{direction}",
|
|
||||||
"source": "ctp",
|
|
||||||
"source_label": "CTP 柜台",
|
|
||||||
"monitor_id": mon["id"] if mon else None,
|
|
||||||
"symbol": codes.get("name", sym) if codes else sym,
|
|
||||||
"symbol_code": sym,
|
|
||||||
"direction": direction,
|
|
||||||
"direction_label": "做多" if direction == "long" else "做空",
|
|
||||||
"lots": lots,
|
|
||||||
"entry_price": entry,
|
|
||||||
"stop_loss": sl,
|
|
||||||
"take_profit": tp,
|
|
||||||
"open_time": open_time or None,
|
|
||||||
"holding_duration": holding or None,
|
|
||||||
"mark_price": mark,
|
|
||||||
"current_price": mark,
|
|
||||||
"margin": pos_metrics.get("margin"),
|
|
||||||
"position_pct": pos_metrics.get("position_pct"),
|
|
||||||
"float_pnl": float_pnl,
|
|
||||||
"est_fee": fee_info["total_fee"],
|
|
||||||
"est_fee_open": fee_info["open_fee"],
|
|
||||||
"est_fee_close": fee_info["close_fee"],
|
|
||||||
"est_fee_close_type": fee_info["close_type"],
|
|
||||||
"est_pnl_net": est_net,
|
|
||||||
"sl_order_active": order_st.get("sl_monitoring"),
|
|
||||||
"tp_order_active": order_st.get("tp_monitoring"),
|
|
||||||
"sl_monitoring": order_st.get("sl_monitoring"),
|
|
||||||
"tp_monitoring": order_st.get("tp_monitoring"),
|
|
||||||
"can_place_orders": False,
|
|
||||||
"tick_value_total": tick.get("tick_value_total"),
|
|
||||||
"price_precision": tick.get("price_precision"),
|
|
||||||
"tick_size": tick.get("tick_size"),
|
|
||||||
"can_close": True,
|
|
||||||
"pending_orders": pending_for_row,
|
|
||||||
"trailing_be": bool(mon.get("trailing_be")) if mon else False,
|
|
||||||
"trailing_r_locked": int(mon.get("trailing_r_locked") or 0) if mon else 0,
|
|
||||||
})
|
|
||||||
return rows
|
|
||||||
|
|
||||||
def _build_trading_live_payload(conn) -> dict:
|
def _build_trading_live_payload(conn) -> dict:
|
||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
@@ -1033,54 +1234,30 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
sl = d.get("stop_loss")
|
sl = d.get("stop_loss")
|
||||||
tp = d.get("take_profit")
|
tp = d.get("take_profit")
|
||||||
trailing_be = 1 if d.get("trailing_be") else 0
|
trailing_be = 1 if d.get("trailing_be") else 0
|
||||||
|
mid = _upsert_open_monitor(
|
||||||
|
conn,
|
||||||
|
sym=sym,
|
||||||
|
direction=direction,
|
||||||
|
lots=lots,
|
||||||
|
price=price,
|
||||||
|
sl=sl,
|
||||||
|
tp=tp,
|
||||||
|
trailing_be=trailing_be,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
_push_position_snapshot_async()
|
||||||
import time
|
import time
|
||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
actual_lots = lots
|
_sync_monitor_lots_from_ctp(conn, mid, sym, direction, mode)
|
||||||
has_pos = False
|
mon_row = conn.execute(
|
||||||
for p in _ctp_positions(mode):
|
"SELECT * FROM trade_order_monitors WHERE id=?", (mid,),
|
||||||
if int(p.get("lots") or 0) <= 0:
|
).fetchone()
|
||||||
continue
|
if mon_row and (sl or tp):
|
||||||
if (p.get("direction") or "long") != direction:
|
try:
|
||||||
continue
|
ensure_monitor_order_columns(conn)
|
||||||
if _match_ctp_symbol(p.get("symbol") or "", sym):
|
cancel_monitor_exit_orders(conn, dict(mon_row), mode=mode)
|
||||||
has_pos = True
|
except Exception as exc:
|
||||||
actual_lots = int(p.get("lots") or lots)
|
logger.warning("清理旧版止盈止损挂单失败: %s", exc)
|
||||||
break
|
|
||||||
if has_pos:
|
|
||||||
codes = ths_to_codes(sym)
|
|
||||||
sl_f = float(sl) if sl else None
|
|
||||||
ensure_monitor_order_columns(conn)
|
|
||||||
conn.execute(
|
|
||||||
"""INSERT INTO trade_order_monitors (
|
|
||||||
symbol, symbol_name, market_code, direction, lots, entry_price,
|
|
||||||
stop_loss, take_profit, initial_stop_loss, trailing_be,
|
|
||||||
open_time, monitor_type, status
|
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?, 'active')""",
|
|
||||||
(
|
|
||||||
sym,
|
|
||||||
codes.get("name", sym) if codes else sym,
|
|
||||||
codes.get("market_code", "") if codes else "",
|
|
||||||
direction,
|
|
||||||
actual_lots,
|
|
||||||
price,
|
|
||||||
sl_f,
|
|
||||||
float(tp) if tp else None,
|
|
||||||
sl_f,
|
|
||||||
trailing_be,
|
|
||||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"manual",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
mid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
|
||||||
mon_row = conn.execute(
|
|
||||||
"SELECT * FROM trade_order_monitors WHERE id=?", (mid,),
|
|
||||||
).fetchone()
|
|
||||||
if mon_row and (sl or tp):
|
|
||||||
try:
|
|
||||||
ensure_monitor_order_columns(conn)
|
|
||||||
cancel_monitor_exit_orders(conn, dict(mon_row), mode=mode)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning("清理旧版止盈止损挂单失败: %s", exc)
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
|
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
+19
-2
@@ -464,8 +464,8 @@ def cancel_monitor_exit_orders(
|
|||||||
return cancelled
|
return cancelled
|
||||||
|
|
||||||
|
|
||||||
def reconcile_monitors_without_position(conn, mode: str) -> int:
|
def reconcile_monitors_without_position(conn, mode: str, *, grace_sec: int = 120) -> int:
|
||||||
"""持仓已平时:关闭监控并撤销残留止盈止损挂单。"""
|
"""持仓已平时:关闭监控并撤销残留止盈止损挂单(新开仓 grace_sec 内不清理)。"""
|
||||||
if not ctp_status(mode).get("connected"):
|
if not ctp_status(mode).get("connected"):
|
||||||
return 0
|
return 0
|
||||||
positions = ctp_list_positions(mode)
|
positions = ctp_list_positions(mode)
|
||||||
@@ -477,9 +477,26 @@ def reconcile_monitors_without_position(conn, mode: str) -> int:
|
|||||||
direction = p.get("direction") or "long"
|
direction = p.get("direction") or "long"
|
||||||
position_keys.add((sym, direction))
|
position_keys.add((sym, direction))
|
||||||
|
|
||||||
|
now_ts = time.time()
|
||||||
|
|
||||||
|
def _monitor_within_grace(mon: dict) -> bool:
|
||||||
|
raw = (mon.get("open_time") or mon.get("created_at") or "").strip()
|
||||||
|
if not raw:
|
||||||
|
return True
|
||||||
|
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"):
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(raw[:19], fmt)
|
||||||
|
if (now_ts - dt.timestamp()) <= grace_sec:
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
closed = 0
|
closed = 0
|
||||||
for r in conn.execute("SELECT * FROM trade_order_monitors WHERE status='active'").fetchall():
|
for r in conn.execute("SELECT * FROM trade_order_monitors WHERE status='active'").fetchall():
|
||||||
mon = dict(r)
|
mon = dict(r)
|
||||||
|
if _monitor_within_grace(mon):
|
||||||
|
continue
|
||||||
ms = mon.get("symbol") or ""
|
ms = mon.get("symbol") or ""
|
||||||
md = mon.get("direction") or "long"
|
md = mon.get("direction") or "long"
|
||||||
matched = False
|
matched = False
|
||||||
|
|||||||
+24
-11
@@ -130,25 +130,32 @@
|
|||||||
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
|
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
|
||||||
}
|
}
|
||||||
var rows = data.rows || [];
|
var rows = data.rows || [];
|
||||||
|
var seenKeys = {};
|
||||||
|
rows = rows.filter(function (row) {
|
||||||
|
var k = row.key || ((row.symbol_code || '') + ':' + (row.direction || ''));
|
||||||
|
if (seenKeys[k]) return false;
|
||||||
|
seenKeys[k] = true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
hasSlTpMonitoring = rows.some(function (row) {
|
hasSlTpMonitoring = rows.some(function (row) {
|
||||||
return row.stop_loss != null || row.take_profit != null;
|
return row.stop_loss != null || row.take_profit != null;
|
||||||
});
|
});
|
||||||
updateSessionUi();
|
updateSessionUi();
|
||||||
savePosCache(data);
|
savePosCache(data);
|
||||||
positionsRendered = true;
|
positionsRendered = true;
|
||||||
if (!connected) {
|
if (!rows.length) {
|
||||||
if (connecting) {
|
if (!connected) {
|
||||||
list.innerHTML = '<div class="empty-hint">CTP 连接中,请稍候…</div>';
|
if (connecting) {
|
||||||
|
list.innerHTML = '<div class="empty-hint">CTP 连接中,请稍候…</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = '<div class="empty-hint">CTP 未连接,正在尝试自动重连…</div>';
|
||||||
|
tryAutoCtpReconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = '<div class="empty-hint">CTP 未连接,正在尝试自动重连…</div>';
|
|
||||||
tryAutoCtpReconnect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!rows.length) {
|
|
||||||
var pendingOnly = data.pending_orders || [];
|
var pendingOnly = data.pending_orders || [];
|
||||||
if (pendingOnly.length) {
|
if (pendingOnly.length) {
|
||||||
list.innerHTML = '<div class="empty-hint" style="margin-bottom:.75rem">柜台暂无持仓</div>' +
|
list.innerHTML = '<div class="empty-hint" style="margin-bottom:.75rem">暂无持仓</div>' +
|
||||||
pendingOnly.map(function (p) {
|
pendingOnly.map(function (p) {
|
||||||
var dismissBtn = p.monitor_id ?
|
var dismissBtn = p.monitor_id ?
|
||||||
'<button type="button" class="pos-dismiss-btn" data-monitor-id="' + p.monitor_id + '">取消</button>' : '';
|
'<button type="button" class="pos-dismiss-btn" data-monitor-id="' + p.monitor_id + '">取消</button>' : '';
|
||||||
@@ -162,10 +169,13 @@
|
|||||||
}).join('');
|
}).join('');
|
||||||
bindPendingDismiss(list);
|
bindPendingDismiss(list);
|
||||||
} else {
|
} else {
|
||||||
list.innerHTML = '<div class="empty-hint">柜台暂无持仓。</div>';
|
list.innerHTML = '<div class="empty-hint">暂无持仓。</div>';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!connected) {
|
||||||
|
tryAutoCtpReconnect();
|
||||||
|
}
|
||||||
list.innerHTML = rows.map(buildPosCard).join('');
|
list.innerHTML = rows.map(buildPosCard).join('');
|
||||||
bindPendingDismiss(list);
|
bindPendingDismiss(list);
|
||||||
bindSlTpButtons(list);
|
bindSlTpButtons(list);
|
||||||
@@ -556,7 +566,9 @@
|
|||||||
'<div class="pos-card-head"><div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div>' +
|
'<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="text-muted" style="font-size:.72rem">' + (row.symbol_code || '') + '</div></div>' +
|
||||||
actionBtns + '</div>' +
|
actionBtns + '</div>' +
|
||||||
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong> · 柜台浮盈' +
|
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong>' +
|
||||||
|
(row.sync_pending ? ' · <span class="text-muted">同步柜台中…</span>' : '') +
|
||||||
|
' · 浮盈' +
|
||||||
(slTpBtn ? ' · ' + slTpBtn : '') +
|
(slTpBtn ? ' · ' + slTpBtn : '') +
|
||||||
(row.sl_order_active ? ' · <span class="text-profit">止损监控中</span>' : '') +
|
(row.sl_order_active ? ' · <span class="text-profit">止损监控中</span>' : '') +
|
||||||
(row.tp_order_active ? ' · <span class="text-profit">止盈监控中</span>' : '') +
|
(row.tp_order_active ? ' · <span class="text-profit">止盈监控中</span>' : '') +
|
||||||
@@ -844,6 +856,7 @@
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
applyPositionsData(cached);
|
applyPositionsData(cached);
|
||||||
}
|
}
|
||||||
|
pollPositions();
|
||||||
connectPositionStream();
|
connectPositionStream();
|
||||||
connectRecommendStream();
|
connectRecommendStream();
|
||||||
fetch('/api/recommend/list')
|
fetch('/api/recommend/list')
|
||||||
|
|||||||
@@ -103,9 +103,9 @@
|
|||||||
|
|
||||||
<div class="card trade-card" id="positions">
|
<div class="card trade-card" id="positions">
|
||||||
<h2>持仓监控</h2>
|
<h2>持仓监控</h2>
|
||||||
<p class="hint pos-hint">后台每秒拉取 CTP 并推送;刷新页面会使用浏览器缓存,不再阻塞读柜台。</p>
|
<p class="hint pos-hint">开仓后立即写入本地监控并显示;后台每秒同步 CTP 柜台更新盈亏与手数。刷新页面优先读本地缓存。</p>
|
||||||
<div class="card-body card-scroll" id="position-live-list">
|
<div class="card-body card-scroll" id="position-live-list">
|
||||||
<div class="empty-hint" id="position-placeholder">{% if ctp_status.connected %}等待持仓推送…{% else %}请先连接 CTP 查看柜台持仓{% endif %}</div>
|
<div class="empty-hint" id="position-placeholder">加载本地持仓…</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user