Merge orders and positions into one card and hide stale pending when CTP is off.
Stop showing DB pending orders while disconnected, invalidate session cache when CTP is down, and add a local DB clear script without embedded credentials. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+21
-20
@@ -1281,11 +1281,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
capital: float,
|
||||
now_iso: str,
|
||||
) -> list[dict]:
|
||||
"""当前委托:以 CTP 柜台为准,本地 pending 开仓单合并展示。"""
|
||||
"""当前委托:CTP 已连接时读柜台;未连接时不展示本地 pending。"""
|
||||
orders: list[dict] = []
|
||||
seen_keys: set[str] = set()
|
||||
connected = ctp_status(mode).get("connected")
|
||||
|
||||
if ctp_status(mode).get("connected"):
|
||||
if connected:
|
||||
ctp_orders = trading_state.get_active_orders()
|
||||
if not ctp_orders:
|
||||
ctp_orders = _ctp_active_orders(mode)
|
||||
@@ -1304,25 +1305,25 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
except Exception as exc:
|
||||
logger.warning("compose ctp order row failed: %s", exc)
|
||||
|
||||
for r in conn.execute(
|
||||
"SELECT * FROM trade_order_monitors WHERE status='pending' ORDER BY id DESC"
|
||||
).fetchall():
|
||||
mon = dict(r)
|
||||
try:
|
||||
prow = _compose_pending_row(
|
||||
mon, mode=mode, capital=capital, now_iso=now_iso,
|
||||
)
|
||||
if prow and prow.get("key") not in seen_keys:
|
||||
pk = f"{prow.get('symbol_code') or ''}:{prow.get('direction') or ''}"
|
||||
dup = any(
|
||||
(x.get("symbol_code") or "") + ":" + (x.get("direction") or "") == pk
|
||||
and x.get("order_state") == "pending"
|
||||
for x in orders
|
||||
for r in conn.execute(
|
||||
"SELECT * FROM trade_order_monitors WHERE status='pending' ORDER BY id DESC"
|
||||
).fetchall():
|
||||
mon = dict(r)
|
||||
try:
|
||||
prow = _compose_pending_row(
|
||||
mon, mode=mode, capital=capital, now_iso=now_iso,
|
||||
)
|
||||
if not dup:
|
||||
orders.append(prow)
|
||||
except Exception as exc:
|
||||
logger.warning("compose pending order row failed: %s", exc)
|
||||
if prow and prow.get("key") not in seen_keys:
|
||||
pk = f"{prow.get('symbol_code') or ''}:{prow.get('direction') or ''}"
|
||||
dup = any(
|
||||
(x.get("symbol_code") or "") + ":" + (x.get("direction") or "") == pk
|
||||
and x.get("order_state") == "pending"
|
||||
for x in orders
|
||||
)
|
||||
if not dup:
|
||||
orders.append(prow)
|
||||
except Exception as exc:
|
||||
logger.warning("compose pending order row failed: %s", exc)
|
||||
return orders
|
||||
|
||||
def _compose_ctp_order_row_any(
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""清空本地 futures.db 交易/监控数据,保留系统设置与 CTP 配置。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from db_conn import DB_PATH
|
||||
|
||||
# 保留:settings、fee_rates(柜台费率)、stats_cache 等配置/缓存
|
||||
KEEP_TABLES = frozenset({
|
||||
"settings",
|
||||
"fee_rates",
|
||||
"stats_cache",
|
||||
"sqlite_sequence",
|
||||
})
|
||||
|
||||
# 清空后重置 sim 账户默认值
|
||||
RESET_SIM_ACCOUNT = True
|
||||
|
||||
|
||||
def _backup_db(db_path: Path) -> Path:
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
dest = db_path.with_suffix(f".bak.{ts}.db")
|
||||
shutil.copy2(db_path, dest)
|
||||
return dest
|
||||
|
||||
|
||||
def clear_trading_data(db_path: Path | None = None, *, backup: bool = True) -> dict:
|
||||
path = Path(db_path or DB_PATH)
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"数据库不存在: {path}")
|
||||
|
||||
if backup:
|
||||
bak = _backup_db(path)
|
||||
print(f"已备份 -> {bak}")
|
||||
|
||||
conn = sqlite3.connect(str(path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
tables = [
|
||||
r[0]
|
||||
for r in conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
||||
).fetchall()
|
||||
]
|
||||
cleared: dict[str, int] = {}
|
||||
for name in tables:
|
||||
if name in KEEP_TABLES:
|
||||
continue
|
||||
before = conn.execute(f"SELECT COUNT(*) AS n FROM [{name}]").fetchone()[0]
|
||||
conn.execute(f"DELETE FROM [{name}]")
|
||||
cleared[name] = int(before)
|
||||
|
||||
if RESET_SIM_ACCOUNT and "ctp_sim_account" in tables:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO ctp_sim_account (id, balance, available, updated_at) "
|
||||
"VALUES (1, 100000, 100000, datetime('now'))"
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.execute("VACUUM")
|
||||
conn.commit()
|
||||
return cleared
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="清空 futures.db 交易数据,保留 settings")
|
||||
parser.add_argument(
|
||||
"--remote",
|
||||
action="store_true",
|
||||
help="清空服务器 /opt/qihuo/futures.db(需 scripts 内 SSH 配置)",
|
||||
)
|
||||
parser.add_argument("--no-backup", action="store_true", help="不备份原库")
|
||||
args = parser.parse_args()
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
if args.remote:
|
||||
import paramiko
|
||||
import time
|
||||
|
||||
remote_path = "/opt/qihuo/futures.db"
|
||||
script_body = Path(__file__).read_text(encoding="utf-8")
|
||||
remote_py = (
|
||||
"import sqlite3, shutil, sys\n"
|
||||
"from datetime import datetime\n"
|
||||
f"KEEP={sorted(KEEP_TABLES)!r}\n"
|
||||
f"RESET_SIM={RESET_SIM_ACCOUNT!r}\n"
|
||||
f"path='{remote_path}'\n"
|
||||
"bak=path.replace('.db', f'.bak.{datetime.now():%Y%m%d_%H%M%S}.db')\n"
|
||||
"shutil.copy2(path, bak); print('backup', bak)\n"
|
||||
"c=sqlite3.connect(path)\n"
|
||||
"tables=[r[0] for r in c.execute(\"SELECT name FROM sqlite_master WHERE type='table'\")]\n"
|
||||
"cleared={}\n"
|
||||
"for name in tables:\n"
|
||||
" if name in KEEP: continue\n"
|
||||
" n=c.execute(f'SELECT COUNT(*) FROM [{name}]').fetchone()[0]\n"
|
||||
" c.execute(f'DELETE FROM [{name}]'); cleared[name]=n\n"
|
||||
"if RESET_SIM and 'ctp_sim_account' in tables:\n"
|
||||
" c.execute(\"INSERT OR REPLACE INTO ctp_sim_account (id,balance,available,updated_at) VALUES (1,100000,100000,datetime('now'))\")\n"
|
||||
"c.commit(); c.execute('VACUUM'); c.commit(); c.close()\n"
|
||||
"print('cleared', cleared)\n"
|
||||
)
|
||||
c = paramiko.SSHClient()
|
||||
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
host = os.getenv("QIHUO_SERVER_HOST", "192.168.8.21")
|
||||
user = os.getenv("QIHUO_SERVER_USER", "root")
|
||||
password = os.getenv("QIHUO_SERVER_PASSWORD", "")
|
||||
if not password:
|
||||
raise SystemExit("远程清库需设置环境变量 QIHUO_SERVER_PASSWORD")
|
||||
c.connect(host, username=user, password=password, timeout=15)
|
||||
sftp = c.open_sftp()
|
||||
with sftp.file("/tmp/clear_qihuo_db.py", "w") as f:
|
||||
f.write(remote_py)
|
||||
sftp.close()
|
||||
_, o, e = c.exec_command(
|
||||
"cd /opt/qihuo && python3 /tmp/clear_qihuo_db.py && pm2 restart qihuo",
|
||||
timeout=120,
|
||||
)
|
||||
print(o.read().decode("utf-8", "replace"))
|
||||
err = e.read().decode("utf-8", "replace")
|
||||
if err.strip():
|
||||
print(err)
|
||||
time.sleep(2)
|
||||
c.close()
|
||||
return
|
||||
|
||||
print(f"目标库: {DB_PATH}")
|
||||
cleared = clear_trading_data(backup=not args.no_backup)
|
||||
if not cleared:
|
||||
print("无需要清空的表")
|
||||
return
|
||||
print("已清空:")
|
||||
for name, n in sorted(cleared.items()):
|
||||
print(f" {name}: {n} 行")
|
||||
print("保留表:", ", ".join(sorted(KEEP_TABLES - {"sqlite_sequence"})))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+10
-1
@@ -2,8 +2,17 @@
|
||||
/* 持仓监控页 — 与 split-grid(关键位监控)同宽,全端自适应 */
|
||||
.trade-page{width:100%}
|
||||
.trade-split{margin-bottom:1.25rem}
|
||||
.trade-split .card{min-height:320px}
|
||||
.trade-split .card{min-height:360px}
|
||||
.trade-split .trade-card#order{margin-bottom:.75rem}
|
||||
.trading-live-body{gap:0}
|
||||
.trading-live-section{padding-bottom:.5rem}
|
||||
.trading-live-section.trading-live-positions{
|
||||
margin-top:.65rem;padding-top:.75rem;border-top:1px solid var(--card-border);
|
||||
}
|
||||
.trading-live-subtitle{
|
||||
font-size:.82rem;font-weight:600;color:var(--text-muted);
|
||||
margin:0 0 .45rem .15rem;letter-spacing:.02em;
|
||||
}
|
||||
.sync-badge{font-size:.72rem;font-weight:400;margin-left:.35rem}
|
||||
.trade-top-bar{
|
||||
display:flex;flex-wrap:wrap;gap:.65rem 1rem;
|
||||
|
||||
+24
-7
@@ -47,7 +47,7 @@
|
||||
var REC_COLSPAN = 18;
|
||||
var marketNavEnabled = !!window.MARKET_NAV_ENABLED;
|
||||
var productCategories = window.PRODUCT_CATEGORIES || [];
|
||||
var POS_CACHE_KEY = 'qihuo_trading_live_v4';
|
||||
var POS_CACHE_KEY = 'qihuo_trading_live_v5';
|
||||
|
||||
function runWhenReady(fn) {
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -133,9 +133,17 @@
|
||||
|
||||
function savePosCache(data) {
|
||||
try {
|
||||
if (!data || !data.rows || !data.rows.length) {
|
||||
var prev = loadPosCache();
|
||||
if (prev && prev.rows && prev.rows.length) return;
|
||||
if (!data) return;
|
||||
var connected = data.ctp_status && data.ctp_status.connected;
|
||||
if (!connected) {
|
||||
sessionStorage.removeItem(POS_CACHE_KEY);
|
||||
return;
|
||||
}
|
||||
if (!data.rows || !data.rows.length) {
|
||||
if (!data.active_orders || !data.active_orders.length) {
|
||||
sessionStorage.removeItem(POS_CACHE_KEY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
sessionStorage.setItem(POS_CACHE_KEY, JSON.stringify(data));
|
||||
} catch (e) { /* quota */ }
|
||||
@@ -159,10 +167,17 @@
|
||||
return !!(msg && (msg.indexOf('不可达') >= 0 || msg.indexOf('Connection refused') >= 0 || msg.indexOf('timed out') >= 0));
|
||||
}
|
||||
|
||||
function applyActiveOrders(orders) {
|
||||
function applyActiveOrders(orders, data) {
|
||||
if (!orderList) return;
|
||||
orders = orders || [];
|
||||
if (!orders.length) {
|
||||
var connected = data && data.ctp_status && data.ctp_status.connected;
|
||||
if (!connected) {
|
||||
var hint = (data && data.ctp_status && data.ctp_status.disabled_hint) ||
|
||||
'CTP 未连接,委托以柜台为准';
|
||||
orderList.innerHTML = '<div class="empty-hint text-muted">' + hint + '</div>';
|
||||
return;
|
||||
}
|
||||
orderList.innerHTML = '<div class="empty-hint">暂无委托。</div>';
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +228,7 @@
|
||||
riskBadge.textContent = data.risk_status.status_label || '';
|
||||
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
|
||||
}
|
||||
applyActiveOrders(data.active_orders || []);
|
||||
applyActiveOrders(data.active_orders || [], data);
|
||||
if (!list) return;
|
||||
var rows = (data.rows || []).filter(function (row) {
|
||||
return row.order_state !== 'pending';
|
||||
@@ -1592,7 +1607,9 @@
|
||||
if (cached.ctp_status) {
|
||||
cached.ctp_status = Object.assign({}, cached.ctp_status, { connecting: false });
|
||||
}
|
||||
applyPositionsData(cached);
|
||||
if (cached.ctp_status && cached.ctp_status.connected) {
|
||||
applyPositionsData(cached);
|
||||
}
|
||||
}
|
||||
pollPositions();
|
||||
connectPositionStream();
|
||||
|
||||
+16
-13
@@ -108,19 +108,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card trade-card" id="active-orders">
|
||||
<h2>当前委托 <span class="sync-badge text-muted" id="sync-badge" hidden></span></h2>
|
||||
<p class="hint pos-hint">委托以 CTP 柜台为准;未成交可撤单,超时自动撤开仓单。</p>
|
||||
<div class="card-body card-scroll" id="order-live-list">
|
||||
<div class="empty-hint" id="order-placeholder">加载委托…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card trade-card" id="positions">
|
||||
<h2>当前持仓</h2>
|
||||
<p class="hint pos-hint">持仓以 CTP 柜台为准;止盈止损为程序本地监控,触发后市价平仓。</p>
|
||||
<div class="card-body card-scroll" id="position-live-list">
|
||||
<div class="empty-hint" id="position-placeholder">加载持仓…</div>
|
||||
<div class="card trade-card" id="trading-live">
|
||||
<h2>委托与持仓 <span class="sync-badge text-muted" id="sync-badge" hidden></span></h2>
|
||||
<p class="hint pos-hint">委托、持仓均以 CTP 柜台为准;止盈止损为程序本地监控,触发后市价平仓。</p>
|
||||
<div class="card-body card-scroll trading-live-body">
|
||||
<section class="trading-live-section" id="active-orders">
|
||||
<h3 class="trading-live-subtitle">当前委托</h3>
|
||||
<div id="order-live-list">
|
||||
<div class="empty-hint" id="order-placeholder">加载委托…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="trading-live-section trading-live-positions" id="positions">
|
||||
<h3 class="trading-live-subtitle">当前持仓</h3>
|
||||
<div id="position-live-list">
|
||||
<div class="empty-hint" id="position-placeholder">加载持仓…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user