Gate order cancel to trading hours and sync trade logs from CTP.
Disable cancel UI outside sessions, query exchange fills for records, and label local vs counterparty rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1223,6 +1223,22 @@ def records():
|
|||||||
start, end = parse_review_date_filter(preset, start, end)
|
start, end = parse_review_date_filter(preset, start, end)
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
|
ctp_sync_info = None
|
||||||
|
try:
|
||||||
|
from ctp_trade_sync import sync_trade_logs_from_ctp
|
||||||
|
from trading_context import get_account_capital, get_trading_mode
|
||||||
|
from vnpy_bridge import ctp_status
|
||||||
|
|
||||||
|
mode = get_trading_mode(get_setting)
|
||||||
|
if ctp_status(mode).get("connected"):
|
||||||
|
capital = get_account_capital(conn, get_setting)
|
||||||
|
ctp_sync_info = sync_trade_logs_from_ctp(
|
||||||
|
conn, mode, capital=capital, trading_mode=mode,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Exception as exc:
|
||||||
|
app.logger.warning("ctp trade sync on records page: %s", exc)
|
||||||
|
|
||||||
sql = "SELECT * FROM review_records WHERE 1=1"
|
sql = "SELECT * FROM review_records WHERE 1=1"
|
||||||
params: list = []
|
params: list = []
|
||||||
if start:
|
if start:
|
||||||
@@ -1264,6 +1280,7 @@ def records():
|
|||||||
trades=trades,
|
trades=trades,
|
||||||
equity_curve=equity_curve,
|
equity_curve=equity_curve,
|
||||||
auto_records=auto_list,
|
auto_records=auto_list,
|
||||||
|
ctp_sync_info=ctp_sync_info,
|
||||||
preset=preset,
|
preset=preset,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
"""从 CTP 柜台同步成交,写入 trade_logs(以交易所成交为准)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from contract_specs import calc_position_metrics
|
||||||
|
from ctp_symbol import ths_to_vnpy_symbol
|
||||||
|
from fee_specs import calc_round_trip_fee
|
||||||
|
from symbols import ths_to_codes
|
||||||
|
from trade_log_lib import calc_equity_after, ensure_trade_log_columns
|
||||||
|
from vnpy_bridge import ctp_list_trades, ctp_status
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
TZ = ZoneInfo("Asia/Shanghai")
|
||||||
|
|
||||||
|
|
||||||
|
def _match_symbol(ctp_sym: str, ths: str) -> bool:
|
||||||
|
a = (ctp_sym or "").lower()
|
||||||
|
b = (ths or "").lower()
|
||||||
|
if a == b:
|
||||||
|
return True
|
||||||
|
if a and b and a.split(".")[0] == b.split(".")[0]:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
vnpy_sym, _ = ths_to_vnpy_symbol(ths)
|
||||||
|
if a == vnpy_sym.lower():
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _to_ths_code(symbol: str) -> str:
|
||||||
|
sym = (symbol or "").strip()
|
||||||
|
if not sym:
|
||||||
|
return ""
|
||||||
|
codes = ths_to_codes(sym)
|
||||||
|
if codes:
|
||||||
|
return codes.get("ths_code") or sym
|
||||||
|
return sym.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def build_round_trips(trades: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
"""按 FIFO 将开/平仓成交配对为完整回合。"""
|
||||||
|
stacks: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
|
||||||
|
trips: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
ordered = sorted(
|
||||||
|
trades,
|
||||||
|
key=lambda t: ((t.get("datetime") or ""), str(t.get("trade_id") or "")),
|
||||||
|
)
|
||||||
|
for t in ordered:
|
||||||
|
sym = (t.get("symbol") or "").lower()
|
||||||
|
pos_dir = (t.get("position_direction") or "long").strip().lower()
|
||||||
|
offset = (t.get("offset") or "open").strip().lower()
|
||||||
|
lots = int(t.get("lots") or 0)
|
||||||
|
if not sym or lots <= 0:
|
||||||
|
continue
|
||||||
|
key = (sym, pos_dir)
|
||||||
|
if offset == "open":
|
||||||
|
stacks[key].append({
|
||||||
|
**t,
|
||||||
|
"remaining": lots,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
close_lots_left = lots
|
||||||
|
close_price = float(t.get("price") or 0)
|
||||||
|
close_time = t.get("datetime") or ""
|
||||||
|
close_trade_id = str(t.get("trade_id") or "")
|
||||||
|
while close_lots_left > 0 and stacks[key]:
|
||||||
|
open_t = stacks[key][0]
|
||||||
|
matched = min(close_lots_left, int(open_t.get("remaining") or 0))
|
||||||
|
if matched <= 0:
|
||||||
|
stacks[key].pop(0)
|
||||||
|
continue
|
||||||
|
open_t["remaining"] = int(open_t.get("remaining") or 0) - matched
|
||||||
|
if open_t["remaining"] <= 0:
|
||||||
|
stacks[key].pop(0)
|
||||||
|
close_lots_left -= matched
|
||||||
|
open_trade_id = str(open_t.get("trade_id") or "")
|
||||||
|
ctp_key = f"{open_trade_id}|{close_trade_id}|{sym}|{pos_dir}|{matched}"
|
||||||
|
trips.append({
|
||||||
|
"ctp_trade_key": ctp_key,
|
||||||
|
"symbol": sym,
|
||||||
|
"ths_code": _to_ths_code(sym),
|
||||||
|
"direction": pos_dir,
|
||||||
|
"lots": matched,
|
||||||
|
"entry_price": float(open_t.get("price") or 0),
|
||||||
|
"close_price": close_price,
|
||||||
|
"open_time": open_t.get("datetime") or "",
|
||||||
|
"close_time": close_time,
|
||||||
|
"open_trade_id": open_trade_id,
|
||||||
|
"close_trade_id": close_trade_id,
|
||||||
|
})
|
||||||
|
return trips
|
||||||
|
|
||||||
|
|
||||||
|
def _find_monitor_meta(
|
||||||
|
conn,
|
||||||
|
*,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
open_time: str,
|
||||||
|
match_symbol_fn: Callable[[str, str], bool] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
match = match_symbol_fn or _match_symbol
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
best: Optional[dict[str, Any]] = None
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT * FROM trade_order_monitors ORDER BY id DESC LIMIT 200"
|
||||||
|
).fetchall():
|
||||||
|
row = dict(r)
|
||||||
|
if (row.get("direction") or "long").strip().lower() != direction:
|
||||||
|
continue
|
||||||
|
if not match(symbol, row.get("symbol") or ""):
|
||||||
|
continue
|
||||||
|
if best is None:
|
||||||
|
best = row
|
||||||
|
continue
|
||||||
|
ot = (row.get("open_time") or "").strip()
|
||||||
|
if open_time and ot and abs(len(ot) - len(open_time)) <= 2 and ot[:16] == open_time[:16]:
|
||||||
|
return row
|
||||||
|
return best or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _holding_minutes(open_time: str, close_time: str) -> int:
|
||||||
|
try:
|
||||||
|
from app import holding_to_minutes
|
||||||
|
return int(holding_to_minutes(open_time, close_time) or 0)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def sync_trade_logs_from_ctp(
|
||||||
|
conn,
|
||||||
|
mode: str,
|
||||||
|
*,
|
||||||
|
capital: float = 0.0,
|
||||||
|
trading_mode: str = "simulation",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""查询 CTP 成交并 upsert 到 trade_logs。返回同步摘要。"""
|
||||||
|
stats = {"synced": 0, "updated": 0, "skipped": 0, "connected": False}
|
||||||
|
if not ctp_status(mode).get("connected"):
|
||||||
|
return stats
|
||||||
|
stats["connected"] = True
|
||||||
|
ensure_trade_log_columns(conn)
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE trade_logs ADD COLUMN source TEXT DEFAULT 'local'")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE trade_logs ADD COLUMN ctp_trade_key TEXT")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
trades = ctp_list_trades(mode, refresh=True)
|
||||||
|
trips = build_round_trips(trades)
|
||||||
|
for trip in trips:
|
||||||
|
key = trip.get("ctp_trade_key") or ""
|
||||||
|
if not key:
|
||||||
|
stats["skipped"] += 1
|
||||||
|
continue
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM trade_logs WHERE ctp_trade_key=?",
|
||||||
|
(key,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
ths = trip.get("ths_code") or trip.get("symbol") or ""
|
||||||
|
codes = ths_to_codes(ths) or {}
|
||||||
|
direction = trip.get("direction") or "long"
|
||||||
|
entry = float(trip.get("entry_price") or 0)
|
||||||
|
close_px = float(trip.get("close_price") or 0)
|
||||||
|
lots = float(trip.get("lots") or 0)
|
||||||
|
open_time = trip.get("open_time") or ""
|
||||||
|
close_time = trip.get("close_time") or datetime.now(TZ).strftime("%Y-%m-%dT%H:%M")
|
||||||
|
|
||||||
|
mon = _find_monitor_meta(
|
||||||
|
conn,
|
||||||
|
symbol=trip.get("symbol") or ths,
|
||||||
|
direction=direction,
|
||||||
|
open_time=open_time,
|
||||||
|
)
|
||||||
|
sl = mon.get("stop_loss")
|
||||||
|
tp = mon.get("take_profit")
|
||||||
|
try:
|
||||||
|
sl_f = float(sl) if sl is not None else entry
|
||||||
|
tp_f = float(tp) if tp is not None else entry
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
sl_f, tp_f = entry, entry
|
||||||
|
|
||||||
|
metrics = calc_position_metrics(
|
||||||
|
direction, entry, sl_f, tp_f, lots, close_px, capital, ths,
|
||||||
|
)
|
||||||
|
pnl = float(metrics.get("float_pnl") or 0)
|
||||||
|
fee = calc_round_trip_fee(
|
||||||
|
ths, entry, close_px, lots, open_time, close_time, trading_mode=trading_mode,
|
||||||
|
)
|
||||||
|
pnl_net = round(pnl - fee, 2)
|
||||||
|
margin_pct = metrics.get("position_pct")
|
||||||
|
equity_after = calc_equity_after(capital, pnl_net)
|
||||||
|
minutes = _holding_minutes(open_time, close_time)
|
||||||
|
result = "CTP同步"
|
||||||
|
monitor_type = mon.get("monitor_type") or "CTP同步"
|
||||||
|
|
||||||
|
row_vals = (
|
||||||
|
ths,
|
||||||
|
codes.get("name") or mon.get("symbol_name") or ths,
|
||||||
|
codes.get("market_code") or mon.get("market_code") or "",
|
||||||
|
codes.get("sina_code") or mon.get("sina_code") or "",
|
||||||
|
monitor_type,
|
||||||
|
direction,
|
||||||
|
entry,
|
||||||
|
sl if sl is not None else None,
|
||||||
|
tp if tp is not None else None,
|
||||||
|
close_px,
|
||||||
|
lots,
|
||||||
|
metrics.get("margin"),
|
||||||
|
margin_pct,
|
||||||
|
minutes,
|
||||||
|
open_time,
|
||||||
|
close_time,
|
||||||
|
pnl,
|
||||||
|
fee,
|
||||||
|
pnl_net,
|
||||||
|
equity_after,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
conn.execute(
|
||||||
|
"""UPDATE trade_logs SET
|
||||||
|
symbol=?, symbol_name=?, market_code=?, sina_code=?, monitor_type=?,
|
||||||
|
direction=?, entry_price=?, stop_loss=?, take_profit=?, close_price=?,
|
||||||
|
lots=?, margin=?, margin_pct=?, holding_minutes=?, open_time=?, close_time=?,
|
||||||
|
pnl=?, fee=?, pnl_net=?, equity_after=?, result=?, source='ctp', verified=1
|
||||||
|
WHERE ctp_trade_key=?""",
|
||||||
|
row_vals + (key,),
|
||||||
|
)
|
||||||
|
stats["updated"] += 1
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO trade_logs
|
||||||
|
(symbol, symbol_name, market_code, sina_code, monitor_type, direction,
|
||||||
|
entry_price, stop_loss, take_profit, close_price, lots, margin,
|
||||||
|
margin_pct, holding_minutes, open_time, close_time, pnl, fee, pnl_net,
|
||||||
|
equity_after, result, source, ctp_trade_key, verified)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
row_vals + ("ctp", key, 1),
|
||||||
|
)
|
||||||
|
stats["synced"] += 1
|
||||||
|
|
||||||
|
if stats["synced"] or stats["updated"]:
|
||||||
|
try:
|
||||||
|
from stats_engine import refresh_stats_cache
|
||||||
|
refresh_stats_cache(conn, capital)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("stats refresh after ctp trade sync: %s", exc)
|
||||||
|
return stats
|
||||||
+54
-2
@@ -80,6 +80,7 @@ from trading_context import (
|
|||||||
)
|
)
|
||||||
from ctp_symbol import ths_to_vnpy_symbol
|
from ctp_symbol import ths_to_vnpy_symbol
|
||||||
from vnpy_bridge import (
|
from vnpy_bridge import (
|
||||||
|
ctp_cancel_order,
|
||||||
ctp_connect,
|
ctp_connect,
|
||||||
ctp_get_account,
|
ctp_get_account,
|
||||||
ctp_get_tick_price,
|
ctp_get_tick_price,
|
||||||
@@ -354,6 +355,25 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"label": "止盈监控",
|
"label": "止盈监控",
|
||||||
"price": float(tp),
|
"price": float(tp),
|
||||||
})
|
})
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT * FROM trade_order_monitors WHERE status='pending' ORDER BY id DESC"
|
||||||
|
).fetchall():
|
||||||
|
mon = dict(r)
|
||||||
|
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),
|
||||||
|
"price": float(mon.get("order_price") or mon.get("entry_price") or 0),
|
||||||
|
"order_kind": "open_pending",
|
||||||
|
"label": "开仓挂单中",
|
||||||
|
"source": "monitor",
|
||||||
|
"monitor_id": mon.get("id"),
|
||||||
|
"can_cancel_order": is_trading_session(),
|
||||||
|
"cancel_allowed": is_trading_session(),
|
||||||
|
})
|
||||||
ctp_st = ctp_status(mode)
|
ctp_st = ctp_status(mode)
|
||||||
if ctp_st.get("connected"):
|
if ctp_st.get("connected"):
|
||||||
for o in _ctp_active_orders(mode):
|
for o in _ctp_active_orders(mode):
|
||||||
@@ -374,6 +394,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"label": label,
|
"label": label,
|
||||||
"source": "ctp",
|
"source": "ctp",
|
||||||
"order_id": o.get("order_id"),
|
"order_id": o.get("order_id"),
|
||||||
|
"can_cancel_order": is_trading_session(),
|
||||||
|
"cancel_allowed": is_trading_session(),
|
||||||
})
|
})
|
||||||
return pending
|
return pending
|
||||||
|
|
||||||
@@ -833,7 +855,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"est_fee": None,
|
"est_fee": None,
|
||||||
"can_close": False,
|
"can_close": False,
|
||||||
"close_allowed": False,
|
"close_allowed": False,
|
||||||
"can_cancel_order": True,
|
"can_cancel_order": is_trading_session(),
|
||||||
|
"cancel_allowed": is_trading_session(),
|
||||||
"auto_cancel_sec": remain,
|
"auto_cancel_sec": remain,
|
||||||
"pending_timeout_sec": timeout_sec,
|
"pending_timeout_sec": timeout_sec,
|
||||||
"pending_timeout_min": max(1, timeout_sec // 60),
|
"pending_timeout_min": max(1, timeout_sec // 60),
|
||||||
@@ -1285,6 +1308,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
return jsonify({"ok": False, "error": "记录不存在或已关闭"}), 404
|
return jsonify({"ok": False, "error": "记录不存在或已关闭"}), 404
|
||||||
mon = dict(row)
|
mon = dict(row)
|
||||||
if (mon.get("status") or "").strip().lower() == "pending":
|
if (mon.get("status") or "").strip().lower() == "pending":
|
||||||
|
if not is_trading_session():
|
||||||
|
return jsonify({"ok": False, "error": "不在交易时间段,无法撤单"}), 403
|
||||||
ok, msg = cancel_pending_monitor(conn, mon, mode)
|
ok, msg = cancel_pending_monitor(conn, mon, mode)
|
||||||
_push_position_snapshot_async(fast=False)
|
_push_position_snapshot_async(fast=False)
|
||||||
return jsonify({"ok": ok, "message": msg})
|
return jsonify({"ok": ok, "message": msg})
|
||||||
@@ -1315,6 +1340,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
if not ctp_status(mode).get("connected"):
|
if not ctp_status(mode).get("connected"):
|
||||||
return jsonify({"ok": False, "error": "请先连接 CTP"}), 400
|
return jsonify({"ok": False, "error": "请先连接 CTP"}), 400
|
||||||
|
if not is_trading_session():
|
||||||
|
return jsonify({"ok": False, "error": "不在交易时间段,无法撤单"}), 403
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT * FROM trade_order_monitors WHERE id=? AND status='pending'",
|
"SELECT * FROM trade_order_monitors WHERE id=? AND status='pending'",
|
||||||
(monitor_id,),
|
(monitor_id,),
|
||||||
@@ -1327,6 +1354,25 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
@app.route("/api/trading/order/cancel", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_trading_order_cancel():
|
||||||
|
"""撤销柜台未成交委托(按 vt_order_id)。"""
|
||||||
|
d = request.get_json(silent=True) or {}
|
||||||
|
order_id = (d.get("order_id") or "").strip()
|
||||||
|
if not order_id:
|
||||||
|
return jsonify({"ok": False, "error": "无效的委托号"}), 400
|
||||||
|
mode = get_trading_mode(get_setting)
|
||||||
|
if not ctp_status(mode).get("connected"):
|
||||||
|
return jsonify({"ok": False, "error": "请先连接 CTP"}), 400
|
||||||
|
if not is_trading_session():
|
||||||
|
return jsonify({"ok": False, "error": "不在交易时间段,无法撤单"}), 403
|
||||||
|
ok = ctp_cancel_order(mode, order_id)
|
||||||
|
_push_position_snapshot_async(fast=False)
|
||||||
|
if not ok:
|
||||||
|
return jsonify({"ok": False, "error": "撤单失败,委托可能已成交或已撤销"}), 400
|
||||||
|
return jsonify({"ok": True, "message": "撤单已提交"})
|
||||||
|
|
||||||
@app.route("/api/trading/close", methods=["POST"])
|
@app.route("/api/trading/close", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_trading_close():
|
def api_trading_close():
|
||||||
@@ -1409,9 +1455,15 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
(mid,),
|
(mid,),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
try:
|
||||||
|
from ctp_trade_sync import sync_trade_logs_from_ctp
|
||||||
|
sync_trade_logs_from_ctp(conn, mode, capital=capital, trading_mode=mode)
|
||||||
|
conn.commit()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("sync trades after close: %s", exc)
|
||||||
conn.close()
|
conn.close()
|
||||||
_push_position_snapshot_async()
|
_push_position_snapshot_async()
|
||||||
return jsonify({"ok": True, "message": "已平仓并记入交易记录(手动平仓)"})
|
return jsonify({"ok": True, "message": "已平仓;交易记录将按柜台成交同步"})
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||||
|
|||||||
+2
-1
@@ -7,6 +7,7 @@ from datetime import datetime
|
|||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from market_sessions import is_trading_session
|
||||||
from vnpy_bridge import ctp_cancel_order, ctp_list_active_orders, ctp_status
|
from vnpy_bridge import ctp_cancel_order, ctp_list_active_orders, ctp_status
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -132,7 +133,7 @@ def reconcile_pending_orders(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if vt_oid and vt_oid in active_orders:
|
if vt_oid and vt_oid in active_orders:
|
||||||
if age >= limit_sec:
|
if age >= limit_sec and is_trading_session():
|
||||||
if ctp_cancel_order(mode, vt_oid):
|
if ctp_cancel_order(mode, vt_oid):
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE trade_order_monitors SET status='closed' WHERE id=?",
|
"UPDATE trade_order_monitors SET status='closed' WHERE id=?",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
.pos-card.is-pending .pos-metrics .cell.pnl-pending label{color:var(--accent)}
|
.pos-card.is-pending .pos-metrics .cell.pnl-pending label{color:var(--accent)}
|
||||||
.pos-close-btn{padding:.4rem .85rem;font-size:.78rem;border-radius:8px;border:1px solid var(--loss);background:var(--loss-bg);color:var(--loss);cursor:pointer;white-space:nowrap;width:auto;flex-shrink:0;min-height:36px}
|
.pos-close-btn{padding:.4rem .85rem;font-size:.78rem;border-radius:8px;border:1px solid var(--loss);background:var(--loss-bg);color:var(--loss);cursor:pointer;white-space:nowrap;width:auto;flex-shrink:0;min-height:36px}
|
||||||
.pos-close-btn:disabled,.pos-close-btn.is-session-off{opacity:.45;cursor:not-allowed;border-color:var(--text-muted);background:var(--card-inner);color:var(--text-muted)}
|
.pos-close-btn:disabled,.pos-close-btn.is-session-off{opacity:.45;cursor:not-allowed;border-color:var(--text-muted);background:var(--card-inner);color:var(--text-muted)}
|
||||||
|
.pos-dismiss-btn:disabled,.pos-dismiss-btn.is-session-off{opacity:.45;cursor:not-allowed;color:var(--text-muted)}
|
||||||
.pos-card-meta-line{font-size:.78rem;line-height:1.65;color:var(--text-muted);margin-bottom:.55rem}
|
.pos-card-meta-line{font-size:.78rem;line-height:1.65;color:var(--text-muted);margin-bottom:.55rem}
|
||||||
.pos-card-meta-line strong{color:var(--text)}
|
.pos-card-meta-line strong{color:var(--text)}
|
||||||
.pos-card-actions{display:flex;gap:.35rem;flex-shrink:0;align-items:center}
|
.pos-card-actions{display:flex;gap:.35rem;flex-shrink:0;align-items:center}
|
||||||
|
|||||||
+62
-5
@@ -204,17 +204,29 @@
|
|||||||
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 cancelAllowed = p.cancel_allowed !== false && isTradingSession;
|
||||||
'<button type="button" class="pos-dismiss-btn" data-monitor-id="' + p.monitor_id + '">取消</button>' : '';
|
var actionBtn = '';
|
||||||
|
if (p.monitor_id) {
|
||||||
|
actionBtn = '<button type="button" class="pos-dismiss-btn' +
|
||||||
|
(cancelAllowed ? '' : ' is-session-off') + '"' +
|
||||||
|
(cancelAllowed ? '' : ' disabled title="不在交易时间段"') +
|
||||||
|
' data-monitor-id="' + p.monitor_id + '" data-pending-cancel="1">撤单</button>';
|
||||||
|
} else if (p.order_id && p.source === 'ctp') {
|
||||||
|
actionBtn = '<button type="button" class="pos-dismiss-btn' +
|
||||||
|
(cancelAllowed ? '' : ' is-session-off') + '"' +
|
||||||
|
(cancelAllowed ? '' : ' disabled title="不在交易时间段"') +
|
||||||
|
' data-cancel-order="' + encodeURIComponent(p.order_id) + '">撤单</button>';
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
'<div class="pos-pending-item ' +
|
'<div class="pos-pending-item ' +
|
||||||
(p.order_kind === 'stop_loss' ? 'sl' : (p.order_kind === 'take_profit' ? 'tp' : 'ctp')) +
|
(p.order_kind === 'stop_loss' ? 'sl' : (p.order_kind === 'take_profit' ? 'tp' : 'ctp')) +
|
||||||
'"><span>' + (p.label || '挂单') + ' · ' + (p.symbol || p.symbol_code) + '</span>' +
|
'"><span>' + (p.label || '挂单') + ' · ' + (p.symbol || p.symbol_code) + '</span>' +
|
||||||
'<span class="pos-pending-right"><strong>' + fmtNum(p.price) + '</strong> · ' +
|
'<span class="pos-pending-right"><strong>' + fmtNum(p.price) + '</strong> · ' +
|
||||||
(p.lots || 1) + ' 手' + dismissBtn + '</span></div>'
|
(p.lots || 1) + ' 手' + actionBtn + '</span></div>'
|
||||||
);
|
);
|
||||||
}).join('');
|
}).join('');
|
||||||
bindPendingDismiss(list);
|
bindPendingDismiss(list);
|
||||||
|
bindCancelOrderButtons(list);
|
||||||
} else {
|
} else {
|
||||||
list.innerHTML = '<div class="empty-hint">暂无持仓。</div>';
|
list.innerHTML = '<div class="empty-hint">暂无持仓。</div>';
|
||||||
}
|
}
|
||||||
@@ -674,6 +686,10 @@
|
|||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
if (!monitorId) return;
|
if (!monitorId) return;
|
||||||
var isPending = !!opts.pending;
|
var isPending = !!opts.pending;
|
||||||
|
if (isPending && !isTradingSession) {
|
||||||
|
alert('不在交易时间段,无法撤单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
var confirmMsg = isPending
|
var confirmMsg = isPending
|
||||||
? '撤销该开仓委托?(将向柜台发送撤单)'
|
? '撤销该开仓委托?(将向柜台发送撤单)'
|
||||||
: '取消该本地止盈止损监控?(不影响柜台委托)';
|
: '取消该本地止盈止损监控?(不影响柜台委托)';
|
||||||
@@ -706,6 +722,10 @@
|
|||||||
if (!root) return;
|
if (!root) return;
|
||||||
root.querySelectorAll('[data-cancel-open]').forEach(function (btn) {
|
root.querySelectorAll('[data-cancel-open]').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
|
if (!isTradingSession) {
|
||||||
|
alert('不在交易时间段,无法撤单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
dismissMonitor(parseInt(btn.getAttribute('data-cancel-open'), 10), btn, { pending: true });
|
dismissMonitor(parseInt(btn.getAttribute('data-cancel-open'), 10), btn, { pending: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -715,7 +735,41 @@
|
|||||||
if (!root) return;
|
if (!root) return;
|
||||||
root.querySelectorAll('[data-monitor-id]').forEach(function (btn) {
|
root.querySelectorAll('[data-monitor-id]').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
dismissMonitor(parseInt(btn.getAttribute('data-monitor-id'), 10), btn);
|
var isPendingCancel = btn.getAttribute('data-pending-cancel') === '1';
|
||||||
|
dismissMonitor(
|
||||||
|
parseInt(btn.getAttribute('data-monitor-id'), 10),
|
||||||
|
btn,
|
||||||
|
isPendingCancel ? { pending: true } : {}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCancelOrderButtons(root) {
|
||||||
|
if (!root) return;
|
||||||
|
root.querySelectorAll('[data-cancel-order]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
if (!isTradingSession) {
|
||||||
|
alert('不在交易时间段,无法撤单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var orderId = decodeURIComponent(btn.getAttribute('data-cancel-order') || '');
|
||||||
|
if (!orderId) return;
|
||||||
|
if (!confirm('撤销该柜台委托?')) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '撤单中…';
|
||||||
|
fetch('/api/trading/order/cancel', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ order_id: orderId })
|
||||||
|
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||||
|
if (!d.ok) throw new Error(d.error || d.message || '撤单失败');
|
||||||
|
pollPositions();
|
||||||
|
}).catch(function (e) {
|
||||||
|
alert(e.message || '撤单失败');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '撤单';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -751,8 +805,11 @@
|
|||||||
var remainMin = row.pending_timeout_min != null
|
var remainMin = row.pending_timeout_min != null
|
||||||
? row.pending_timeout_min
|
? row.pending_timeout_min
|
||||||
: (row.auto_cancel_sec != null ? Math.max(1, Math.ceil(row.auto_cancel_sec / 60)) : 5);
|
: (row.auto_cancel_sec != null ? Math.max(1, Math.ceil(row.auto_cancel_sec / 60)) : 5);
|
||||||
|
var cancelAllowed = row.cancel_allowed !== false && isTradingSession;
|
||||||
var cancelBtn = row.can_cancel_order ?
|
var cancelBtn = row.can_cancel_order ?
|
||||||
'<button type="button" class="pos-close-btn" data-cancel-open="' + row.monitor_id + '">撤单</button>' : '';
|
'<button type="button" class="pos-close-btn' + (cancelAllowed ? '' : ' is-session-off') + '"' +
|
||||||
|
(cancelAllowed ? '' : ' disabled title="不在交易时间段"') +
|
||||||
|
' data-cancel-open="' + row.monitor_id + '">撤单</button>' : '';
|
||||||
var metaLine =
|
var metaLine =
|
||||||
'状态 <strong class="text-accent">挂单中</strong>' +
|
'状态 <strong class="text-accent">挂单中</strong>' +
|
||||||
' · 委托价 <strong>' + fmtNum(orderPx) + '</strong>' +
|
' · 委托价 <strong>' + fmtNum(orderPx) + '</strong>' +
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
<div class="card records-trade-card" style="margin-bottom:1.25rem">
|
<div class="card records-trade-card" style="margin-bottom:1.25rem">
|
||||||
<h2>交易记录</h2>
|
<h2>交易记录</h2>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
{% if ctp_sync_info and ctp_sync_info.connected %}
|
||||||
|
<p class="hint" style="margin-top:0">
|
||||||
|
已连接 CTP,本页已自动同步柜台成交(新增 {{ ctp_sync_info.synced or 0 }} 条,更新 {{ ctp_sync_info.updated or 0 }} 条)。
|
||||||
|
带来源「柜台」的记录以交易所成交为准;「本地」为程序写入,可手动删除错误项。
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="hint" style="margin-top:0">CTP 未连接时仅显示本地数据库记录;连接后打开本页会自动同步柜台成交。</p>
|
||||||
|
{% endif %}
|
||||||
<label class="trade-switch-label">
|
<label class="trade-switch-label">
|
||||||
<input type="checkbox" id="trade-edit-switch">
|
<input type="checkbox" id="trade-edit-switch">
|
||||||
<span>修改/核对开关(开启后可编辑关键字段)</span>
|
<span>修改/核对开关(开启后可编辑关键字段)</span>
|
||||||
@@ -32,6 +40,11 @@
|
|||||||
<td><span class="cell-readonly">{{ t.symbol_name or t.symbol }}</span></td>
|
<td><span class="cell-readonly">{{ t.symbol_name or t.symbol }}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="cell-readonly cell-edit-hide">{{ t.monitor_type }}</span>
|
<span class="cell-readonly cell-edit-hide">{{ t.monitor_type }}</span>
|
||||||
|
{% if t.source == 'ctp' %}
|
||||||
|
<span class="badge" style="margin-left:.25rem;font-size:.65rem">柜台</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted" style="margin-left:.25rem;font-size:.65rem">本地</span>
|
||||||
|
{% endif %}
|
||||||
<input class="cell-edit-show" type="hidden" name="monitor_type" value="{{ t.monitor_type }}">
|
<input class="cell-edit-show" type="hidden" name="monitor_type" value="{{ t.monitor_type }}">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from typing import Any
|
|||||||
TRADE_LOG_EXTRA_COLUMNS = (
|
TRADE_LOG_EXTRA_COLUMNS = (
|
||||||
"ALTER TABLE trade_logs ADD COLUMN margin_pct REAL",
|
"ALTER TABLE trade_logs ADD COLUMN margin_pct REAL",
|
||||||
"ALTER TABLE trade_logs ADD COLUMN equity_after REAL",
|
"ALTER TABLE trade_logs ADD COLUMN equity_after REAL",
|
||||||
|
"ALTER TABLE trade_logs ADD COLUMN source TEXT DEFAULT 'local'",
|
||||||
|
"ALTER TABLE trade_logs ADD COLUMN ctp_trade_key TEXT",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+193
@@ -189,6 +189,10 @@ class CtpBridge:
|
|||||||
self._position_margins: dict[str, float] = {}
|
self._position_margins: dict[str, float] = {}
|
||||||
self._position_open_times: dict[str, str] = {}
|
self._position_open_times: dict[str, str] = {}
|
||||||
self._margin_hooked = False
|
self._margin_hooked = False
|
||||||
|
self._trade_hooked = False
|
||||||
|
self._trade_query_results: list[dict[str, Any]] = []
|
||||||
|
self._trade_query_event = threading.Event()
|
||||||
|
self._last_trade_query_ts: float = 0.0
|
||||||
self._tick_hooked = False
|
self._tick_hooked = False
|
||||||
self._bar_generators: dict[str, Any] = {}
|
self._bar_generators: dict[str, Any] = {}
|
||||||
self._bars_1m: dict[str, deque] = {}
|
self._bars_1m: dict[str, deque] = {}
|
||||||
@@ -1055,6 +1059,188 @@ class CtpBridge:
|
|||||||
out = self._collect_positions()
|
out = self._collect_positions()
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_trade_offset(offset_obj: Any) -> str:
|
||||||
|
s = str(offset_obj or "").upper()
|
||||||
|
if "OPEN" in s:
|
||||||
|
return "open"
|
||||||
|
return "close"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_trade_direction(direction_obj: Any) -> str:
|
||||||
|
return "long" if _is_long_direction(direction_obj) else "short"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _position_direction_from_trade(trade_direction: str, offset: str) -> str:
|
||||||
|
td = (trade_direction or "long").strip().lower()
|
||||||
|
if (offset or "open").strip().lower() == "open":
|
||||||
|
return td
|
||||||
|
return "short" if td == "long" else "long"
|
||||||
|
|
||||||
|
def _format_trade_datetime(self, dt_obj: Any, date_raw: str = "", time_raw: str = "") -> str:
|
||||||
|
if dt_obj is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(dt_obj, "strftime"):
|
||||||
|
return dt_obj.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
text = str(dt_obj).strip()
|
||||||
|
if text:
|
||||||
|
return text[:19].replace("T", " ")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
parsed = self._parse_ctp_open_datetime(date_raw, time_raw)
|
||||||
|
return parsed or ""
|
||||||
|
|
||||||
|
def _trade_row_from_vnpy(self, trade: Any) -> Optional[dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
sym = (getattr(trade, "symbol", "") or "").strip()
|
||||||
|
vol = int(getattr(trade, "volume", 0) or 0)
|
||||||
|
if not sym or vol <= 0:
|
||||||
|
return None
|
||||||
|
direction = self._parse_trade_direction(getattr(trade, "direction", None))
|
||||||
|
offset = self._parse_trade_offset(getattr(trade, "offset", None))
|
||||||
|
exchange = getattr(trade, "exchange", None)
|
||||||
|
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
|
||||||
|
dt = self._format_trade_datetime(getattr(trade, "datetime", None))
|
||||||
|
trade_id = str(getattr(trade, "tradeid", "") or getattr(trade, "vt_tradeid", "") or "")
|
||||||
|
order_id = str(getattr(trade, "orderid", "") or getattr(trade, "vt_orderid", "") or "")
|
||||||
|
if not trade_id:
|
||||||
|
trade_id = f"{order_id}:{sym}:{offset}:{direction}:{vol}:{getattr(trade, 'price', 0)}:{dt}"
|
||||||
|
return {
|
||||||
|
"trade_id": trade_id,
|
||||||
|
"order_id": order_id,
|
||||||
|
"symbol": sym,
|
||||||
|
"exchange": ex_name,
|
||||||
|
"direction": direction,
|
||||||
|
"offset": offset,
|
||||||
|
"position_direction": self._position_direction_from_trade(direction, offset),
|
||||||
|
"lots": vol,
|
||||||
|
"price": float(getattr(trade, "price", 0) or 0),
|
||||||
|
"datetime": dt,
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("trade_row_from_vnpy: %s", exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _trade_row_from_ctp_dict(self, data: dict) -> Optional[dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
sym = (data.get("InstrumentID") or data.get("instrument_id") or "").strip()
|
||||||
|
vol = int(float(data.get("Volume") or data.get("volume") or 0))
|
||||||
|
if not sym or vol <= 0:
|
||||||
|
return None
|
||||||
|
dir_raw = str(data.get("Direction") or data.get("direction") or "")
|
||||||
|
direction = "long" if dir_raw in ("0", "2") or "LONG" in dir_raw.upper() or dir_raw == "多" else "short"
|
||||||
|
off_raw = str(data.get("OffsetFlag") or data.get("offset") or "")
|
||||||
|
if off_raw in ("0",) or "OPEN" in off_raw.upper():
|
||||||
|
offset = "open"
|
||||||
|
else:
|
||||||
|
offset = "close"
|
||||||
|
price = float(data.get("Price") or data.get("price") or 0)
|
||||||
|
trade_id = str(data.get("TradeID") or data.get("tradeid") or "").strip()
|
||||||
|
order_sys = str(data.get("OrderSysID") or data.get("orderid") or "").strip()
|
||||||
|
dt = self._format_trade_datetime(
|
||||||
|
None,
|
||||||
|
str(data.get("TradeDate") or data.get("trade_date") or ""),
|
||||||
|
str(data.get("TradeTime") or data.get("trade_time") or ""),
|
||||||
|
)
|
||||||
|
if not trade_id:
|
||||||
|
trade_id = f"{order_sys}:{sym}:{offset}:{direction}:{vol}:{price}:{dt}"
|
||||||
|
return {
|
||||||
|
"trade_id": trade_id,
|
||||||
|
"order_id": order_sys,
|
||||||
|
"symbol": sym,
|
||||||
|
"exchange": str(data.get("ExchangeID") or data.get("exchange") or ""),
|
||||||
|
"direction": direction,
|
||||||
|
"offset": offset,
|
||||||
|
"position_direction": self._position_direction_from_trade(direction, offset),
|
||||||
|
"lots": vol,
|
||||||
|
"price": price,
|
||||||
|
"datetime": dt,
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("trade_row_from_ctp_dict: %s", exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _install_trade_query_hook(self) -> None:
|
||||||
|
if self._trade_hooked or not self._engine:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
gw = self._engine.get_gateway(GATEWAY_NAME)
|
||||||
|
td = getattr(gw, "td_api", None)
|
||||||
|
if not td or not hasattr(td, "onRspQryTrade"):
|
||||||
|
return
|
||||||
|
bridge = self
|
||||||
|
original = td.onRspQryTrade
|
||||||
|
|
||||||
|
def _wrapped(data, error, reqid, last):
|
||||||
|
try:
|
||||||
|
if data and isinstance(data, dict):
|
||||||
|
row = bridge._trade_row_from_ctp_dict(data)
|
||||||
|
if row:
|
||||||
|
bridge._trade_query_results.append(row)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("trade hook row: %s", exc)
|
||||||
|
result = original(data, error, reqid, last)
|
||||||
|
if last:
|
||||||
|
bridge._trade_query_event.set()
|
||||||
|
return result
|
||||||
|
|
||||||
|
td.onRspQryTrade = _wrapped
|
||||||
|
self._trade_hooked = True
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("install trade hook: %s", exc)
|
||||||
|
|
||||||
|
def _collect_engine_trades(self) -> list[dict[str, Any]]:
|
||||||
|
if not self._engine:
|
||||||
|
return []
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
try:
|
||||||
|
trades = self._engine.get_all_trades()
|
||||||
|
except Exception:
|
||||||
|
trades = {}
|
||||||
|
for trade in (trades or {}).values():
|
||||||
|
row = self._trade_row_from_vnpy(trade)
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
key = row["trade_id"]
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def refresh_trades(self) -> None:
|
||||||
|
"""向柜台查询当日成交(并合并内存成交回报)。"""
|
||||||
|
if not self._engine:
|
||||||
|
return
|
||||||
|
now = time.time()
|
||||||
|
if now - self._last_trade_query_ts < 1.0:
|
||||||
|
return
|
||||||
|
self._last_trade_query_ts = now
|
||||||
|
self._trade_query_results = []
|
||||||
|
self._trade_query_event.clear()
|
||||||
|
try:
|
||||||
|
self._install_trade_query_hook()
|
||||||
|
gw = self._engine.get_gateway(GATEWAY_NAME)
|
||||||
|
td = getattr(gw, "td_api", None)
|
||||||
|
if td and hasattr(td, "query_trade"):
|
||||||
|
td.query_trade()
|
||||||
|
self._trade_query_event.wait(timeout=2.0)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("refresh_trades: %s", exc)
|
||||||
|
|
||||||
|
def list_trades(self, *, refresh: bool = False) -> list[dict[str, Any]]:
|
||||||
|
if refresh:
|
||||||
|
self.refresh_trades()
|
||||||
|
merged: dict[str, dict[str, Any]] = {}
|
||||||
|
for row in self._collect_engine_trades():
|
||||||
|
merged[row["trade_id"]] = row
|
||||||
|
for row in self._trade_query_results:
|
||||||
|
merged[row["trade_id"]] = row
|
||||||
|
out = list(merged.values())
|
||||||
|
out.sort(key=lambda r: (r.get("datetime") or "", r.get("trade_id") or ""))
|
||||||
|
return out
|
||||||
|
|
||||||
def list_active_orders(self) -> list[dict[str, Any]]:
|
def list_active_orders(self) -> list[dict[str, Any]]:
|
||||||
if not self._engine:
|
if not self._engine:
|
||||||
return []
|
return []
|
||||||
@@ -1282,6 +1468,13 @@ def ctp_cancel_order(mode: str, vt_orderid: str) -> bool:
|
|||||||
return b.cancel_order(vt_orderid)
|
return b.cancel_order(vt_orderid)
|
||||||
|
|
||||||
|
|
||||||
|
def ctp_list_trades(mode: str, *, refresh: bool = False) -> list[dict[str, Any]]:
|
||||||
|
b = get_bridge()
|
||||||
|
if b.connected_mode != mode or not b.ping():
|
||||||
|
return []
|
||||||
|
return b.list_trades(refresh=refresh)
|
||||||
|
|
||||||
|
|
||||||
def ctp_get_tick_price(mode: str, ths_code: str) -> Optional[float]:
|
def ctp_get_tick_price(mode: str, ths_code: str) -> Optional[float]:
|
||||||
"""CTP 柜台最新价(需已连接并订阅)。"""
|
"""CTP 柜台最新价(需已连接并订阅)。"""
|
||||||
b = get_bridge()
|
b = get_bridge()
|
||||||
|
|||||||
Reference in New Issue
Block a user