修改持仓显示,读取历史仓位

This commit is contained in:
dekun
2026-05-17 16:28:00 +08:00
parent 4a9abf660f
commit 5a59246e18
4 changed files with 573 additions and 31 deletions
+266 -14
View File
@@ -138,6 +138,12 @@ KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_
ORDER_MONITOR_TYPE_MANUAL = "下单监控"
ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控"
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
EXCHANGE_POSITION_SYNC_FROM_BJ = (os.getenv("EXCHANGE_POSITION_SYNC_FROM_BJ") or "").strip()
EXCHANGE_POSITION_HISTORY_LIMIT = max(50, min(1000, int(os.getenv("EXCHANGE_POSITION_HISTORY_LIMIT", "200"))))
BINANCE_NET_INCOME_TYPES = frozenset(
{"REALIZED_PNL", "COMMISSION", "FUNDING_FEE", "INSURANCE_CLEAR", "INTERNAL_AUTO_CLOSE"}
)
_LAST_EXCHANGE_PNL_SYNC_AT = 0.0
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
AUTO_TRANSFER_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true"
AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30"))
@@ -1236,6 +1242,21 @@ def init_db():
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
except Exception:
pass
try:
c.execute("ALTER TABLE order_monitors ADD COLUMN key_signal_type TEXT")
except Exception:
pass
for col, ddl in (
("key_signal_type", "ALTER TABLE trade_records ADD COLUMN key_signal_type TEXT"),
("exchange_realized_pnl", "ALTER TABLE trade_records ADD COLUMN exchange_realized_pnl REAL"),
("exchange_opened_at", "ALTER TABLE trade_records ADD COLUMN exchange_opened_at TEXT"),
("exchange_closed_at", "ALTER TABLE trade_records ADD COLUMN exchange_closed_at TEXT"),
("exchange_sync_key", "ALTER TABLE trade_records ADD COLUMN exchange_sync_key TEXT"),
):
try:
c.execute(ddl)
except Exception:
pass
c.execute(
"""CREATE TABLE IF NOT EXISTS key_monitor_history
@@ -1711,6 +1732,25 @@ def to_effective_trade_dict(row):
item["effective_hold_seconds"] = get_effective_trade_field(row, "reviewed_hold_seconds", "hold_seconds", item.get("hold_seconds"))
er_eff = get_effective_trade_field(row, "reviewed_entry_reason", "entry_reason", item.get("entry_reason"))
item["effective_entry_reason"] = (str(er_eff).strip() if er_eff is not None else "") or ""
reviewed_pnl = get_effective_trade_field(row, "reviewed_pnl_amount", "pnl_amount", None)
has_reviewed_pnl = reviewed_pnl is not None and str(reviewed_pnl).strip() != ""
ex_pnl = item.get("exchange_realized_pnl")
if not has_reviewed_pnl and ex_pnl is not None and str(ex_pnl).strip() != "":
try:
item["effective_pnl_amount"] = round(float(ex_pnl), FUNDS_DECIMALS)
item["display_pnl_source"] = "exchange"
ex_open = (str(item.get("exchange_opened_at") or "").strip() or None)
ex_close = (str(item.get("exchange_closed_at") or "").strip() or None)
if ex_open:
item["effective_opened_at"] = ex_open
if ex_close:
item["effective_closed_at"] = ex_close
except (TypeError, ValueError):
item["display_pnl_source"] = "local"
elif has_reviewed_pnl:
item["display_pnl_source"] = "reviewed"
else:
item["display_pnl_source"] = "local"
return item
@@ -1949,16 +1989,20 @@ def insert_trade_record(
closed_at=None,
closed_at_ms=None,
exchange_trade_id=None,
key_signal_type=None,
):
hold_minutes = calc_hold_minutes(hold_seconds)
open_ts = opened_at or app_now_str()
close_ts = closed_at or app_now_str()
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
kst = (key_signal_type or "").strip()
if kst not in KEY_MONITOR_AUTO_TYPES:
kst = None
conn.execute(
"INSERT INTO trade_records (symbol,monitor_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"INSERT INTO trade_records (symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
symbol, monitor_type, kst, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
margin_capital, leverage, pnl_amount, hold_seconds,
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id
@@ -2030,6 +2074,33 @@ def ensure_exchange_live_ready():
return True, ""
def order_row_monitor_type(row):
if row is None:
return ORDER_MONITOR_TYPE_MANUAL
try:
keys = row.keys() if hasattr(row, "keys") else []
except Exception:
keys = []
if "monitor_type" in keys:
mt = (row["monitor_type"] or "").strip()
if mt:
return mt
return ORDER_MONITOR_TYPE_MANUAL
def order_row_key_signal_type(row):
if row is None:
return None
try:
keys = row.keys() if hasattr(row, "keys") else []
except Exception:
keys = []
if "key_signal_type" not in keys:
return None
kst = (row["key_signal_type"] or "").strip()
return kst if kst in KEY_MONITOR_AUTO_TYPES else None
def exchange_private_api_configured():
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等。"""
return bool(BINANCE_API_KEY and BINANCE_API_SECRET)
@@ -3348,7 +3419,8 @@ def reconcile_external_closes(conn, days=None):
insert_trade_record(
conn,
symbol=r["symbol"],
monitor_type="下单监控",
monitor_type=order_row_monitor_type(r),
key_signal_type=order_row_key_signal_type(r),
direction=r["direction"],
trigger_price=r["trigger_price"],
stop_loss=r["stop_loss"],
@@ -3652,7 +3724,7 @@ def _key_plan_auto_sl_tp(direction, upper, lower, checks, outside_pct):
return E, sl_raw, tp_raw, H
def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_loss, take_profit):
def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_loss, take_profit, key_signal_type=None):
"""
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入(Binance U 本位)。
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
@@ -3769,8 +3841,8 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
"(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
"margin_capital, leverage, trade_style, risk_percent, risk_amount, "
"breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, "
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol,
exchange_symbol,
@@ -3799,6 +3871,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
opened_at_ms,
trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO,
(key_signal_type if key_signal_type in KEY_MONITOR_AUTO_TYPES else None),
),
)
new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
@@ -3923,8 +3996,9 @@ def check_key_monitors():
_finalize_key_monitor_one_shot(conn, r, rr_msg, "rr_insufficient")
continue
key_sig = typ if typ in KEY_MONITOR_AUTO_TYPES else None
ok_trade, trade_err, det = _market_open_for_key_monitor(
conn, sym, direction, exchange_symbol, sl_raw, tp_raw,
conn, sym, direction, exchange_symbol, sl_raw, tp_raw, key_signal_type=key_sig,
)
planned_rr_txt = (
format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
@@ -4155,7 +4229,8 @@ def check_order_monitors():
insert_trade_record(
conn,
symbol=sym,
monitor_type="下单监控",
monitor_type=order_row_monitor_type(r),
key_signal_type=order_row_key_signal_type(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=stop_loss,
@@ -4225,7 +4300,8 @@ def check_order_monitors():
insert_trade_record(
conn,
symbol=sym,
monitor_type="下单监控",
monitor_type=order_row_monitor_type(r),
key_signal_type=order_row_key_signal_type(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=stop_loss,
@@ -4291,7 +4367,8 @@ def force_close_before_reset():
insert_trade_record(
conn,
symbol=r["symbol"],
monitor_type="下单监控",
monitor_type=order_row_monitor_type(r),
key_signal_type=order_row_key_signal_type(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=r["stop_loss"],
@@ -4421,6 +4498,174 @@ def api_sync_positions():
return jsonify({"ok": True, "days": days, "synced": int(synced)})
def _coerce_ts_ms(val):
if val is None or val == "":
return None
try:
v = float(val)
except (TypeError, ValueError):
return None
if v > 1e12:
return int(v)
if v > 1e9:
return int(v)
return int(v * 1000.0)
def _unified_symbol_for_match(symbol_str):
if not symbol_str:
return ""
s = str(symbol_str).strip()
if not s:
return ""
try:
return normalize_exchange_symbol(s).split(":")[0]
except Exception:
x = s.upper().replace(" ", "")
if "/" in x:
return x.split(":")[0]
if x.endswith("USDT") and len(x) > 4:
return f"{x[:-4]}/USDT"
return x
def exchange_position_sync_since_ms():
s = EXCHANGE_POSITION_SYNC_FROM_BJ
if s:
for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d", 10)):
try:
chunk = s[:ln] if len(s) >= ln else s[:10]
dt = datetime.strptime(chunk, fmt)
aware = dt.replace(tzinfo=APP_TZ)
return int(aware.timestamp() * 1000)
except Exception:
continue
dt0 = app_now() - timedelta(days=90)
try:
aware0 = datetime(dt0.year, dt0.month, dt0.day, 0, 0, 0, tzinfo=APP_TZ)
except Exception:
aware0 = datetime.now(APP_TZ)
return int(aware0.timestamp() * 1000)
def _fetch_binance_income_entries(exchange_symbol, start_ms, end_ms):
if not hasattr(exchange, "fapiPrivateGetIncome"):
return []
ensure_markets_loaded()
market = exchange.market(exchange_symbol)
contract_id = market.get("id")
if not contract_id:
return []
out = []
cursor = int(start_ms)
end_ms = int(end_ms)
for _ in range(20):
try:
batch = exchange.fapiPrivateGetIncome(
{"symbol": contract_id, "startTime": cursor, "endTime": end_ms, "limit": 1000}
)
except Exception:
break
if not batch:
break
out.extend(batch)
if len(batch) < 1000:
break
last_t = _coerce_ts_ms(batch[-1].get("time"))
if last_t is None or last_t >= end_ms:
break
cursor = last_t + 1
return out
def fetch_binance_net_pnl_for_trade(exchange_symbol, direction, open_ms, close_ms):
if open_ms is None or close_ms is None or close_ms < open_ms:
return None, None, None, None
buffer_ms = 5 * 60 * 1000
entries = _fetch_binance_income_entries(
exchange_symbol, max(0, int(open_ms) - buffer_ms), int(close_ms) + buffer_ms
)
if not entries:
return None, None, None, None
net = 0.0
first_t = None
last_t = None
for e in entries:
it = (e.get("incomeType") or e.get("income_type") or "").strip()
if it not in BINANCE_NET_INCOME_TYPES:
continue
try:
net += float(e.get("income") or 0)
except (TypeError, ValueError):
pass
t = _coerce_ts_ms(e.get("time"))
if t:
first_t = t if first_t is None else min(first_t, t)
last_t = t if last_t is None else max(last_t, t)
if first_t is None:
return None, None, None, None
net = round(net, FUNDS_DECIMALS)
ensure_markets_loaded()
market = exchange.market(exchange_symbol)
cid = market.get("id") or exchange_symbol
sync_key = f"income|{cid}|{direction}|{open_ms}|{close_ms}|{net}"
eo = ms_to_app_local_str(first_t)
ec = ms_to_app_local_str(last_t)
return net, sync_key, eo, ec
def sync_trade_records_from_exchange(conn):
"""为未同步的 trade_records 回填交易所口径净盈亏(Binance:income 流水汇总)。"""
global _LAST_EXCHANGE_PNL_SYNC_AT
if not exchange_private_api_configured():
return
now = time.time()
if now - _LAST_EXCHANGE_PNL_SYNC_AT < 25.0:
return
candidates = conn.execute(
"""
SELECT id, symbol, direction, opened_at, opened_at_ms, closed_at, closed_at_ms
FROM trade_records
WHERE (exchange_sync_key IS NULL OR TRIM(exchange_sync_key) = '')
ORDER BY id DESC
LIMIT 120
"""
).fetchall()
if not candidates:
_LAST_EXCHANGE_PNL_SYNC_AT = now
return
for tr in candidates:
direction = (tr["direction"] or "long").strip().lower()
open_ms = _to_ms_with_fallback(
tr["opened_at_ms"] if "opened_at_ms" in tr.keys() else None, tr["opened_at"]
)
close_ms = _to_ms_with_fallback(
tr["closed_at_ms"] if "closed_at_ms" in tr.keys() else None, tr["closed_at"]
)
if open_ms is None or close_ms is None:
continue
try:
ex_sym = normalize_exchange_symbol(tr["symbol"])
except Exception:
continue
net, sync_key, eo, ec = fetch_binance_net_pnl_for_trade(ex_sym, direction, open_ms, close_ms)
if net is None or not sync_key:
continue
conn.execute(
"""
UPDATE trade_records
SET exchange_realized_pnl = ?, exchange_opened_at = ?, exchange_closed_at = ?, exchange_sync_key = ?
WHERE id = ?
""",
(float(net), eo, ec, sync_key, int(tr["id"])),
)
_LAST_EXCHANGE_PNL_SYNC_AT = now
try:
conn.commit()
except Exception:
pass
# ====================== 主页面 ======================
def render_main_page(page="trade"):
now = app_now()
@@ -4440,6 +4685,11 @@ def render_main_page(page="trade"):
order_list = []
for o in raw_order_list:
order_list.append(enrich_order_item(row_to_dict(o), current_capital))
if exchange_private_api_configured():
try:
sync_trade_records_from_exchange(conn)
except Exception:
pass
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall()
records = [to_effective_trade_dict(r) for r in raw_records]
total = len(records)
@@ -5686,7 +5936,8 @@ def del_order(id):
insert_trade_record(
conn,
symbol=row["symbol"],
monitor_type="下单监控",
monitor_type=order_row_monitor_type(row),
key_signal_type=order_row_key_signal_type(row),
direction=row["direction"],
trigger_price=row["trigger_price"],
stop_loss=row["stop_loss"],
@@ -5740,9 +5991,10 @@ def del_order(id):
update_session_capital(conn, session_date, pnl_amount)
insert_trade_record(
conn,
symbol=row["symbol"],
monitor_type="下单监控",
direction=row["direction"],
symbol=row["symbol"],
monitor_type=order_row_monitor_type(row),
key_signal_type=order_row_key_signal_type(row),
direction=row["direction"],
trigger_price=row["trigger_price"],
stop_loss=row["stop_loss"],
initial_stop_loss=row["initial_stop_loss"] or row["stop_loss"],
+3 -3
View File
@@ -395,7 +395,7 @@
</div>
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}</span>
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
<span class="pos-meta-item">风险: {{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U</span>
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
@@ -506,7 +506,7 @@
<tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}</td>
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
{% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
@@ -519,7 +519,7 @@
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ funds_fmt(r.effective_pnl_amount or 0) }}</span></td>
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ funds_fmt(r.effective_pnl_amount or 0) }}</span>{% if r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></span>{% elif r.display_pnl_source != 'reviewed' %}<span style="font-size:.68rem;color:#8892b0"></span>{% endif %}</td>
<td>
{% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
+301 -11
View File
@@ -137,6 +137,9 @@ KEY_CONFIRM_BAR = int(os.getenv("KEY_CONFIRM_BAR", "-1"))
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT", "true").lower() == "true"
ORDER_MONITOR_TYPE_MANUAL = "下单监控"
ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控"
EXCHANGE_POSITION_SYNC_FROM_BJ = (os.getenv("EXCHANGE_POSITION_SYNC_FROM_BJ") or "").strip()
EXCHANGE_POSITION_HISTORY_LIMIT = max(50, min(1000, int(os.getenv("EXCHANGE_POSITION_HISTORY_LIMIT", "200"))))
_LAST_EXCHANGE_PNL_SYNC_AT = 0.0
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
@@ -1241,6 +1244,21 @@ def init_db():
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
except Exception:
pass
try:
c.execute("ALTER TABLE order_monitors ADD COLUMN key_signal_type TEXT")
except Exception:
pass
for ddl in (
"ALTER TABLE trade_records ADD COLUMN key_signal_type TEXT",
"ALTER TABLE trade_records ADD COLUMN exchange_realized_pnl REAL",
"ALTER TABLE trade_records ADD COLUMN exchange_opened_at TEXT",
"ALTER TABLE trade_records ADD COLUMN exchange_closed_at TEXT",
"ALTER TABLE trade_records ADD COLUMN exchange_sync_key TEXT",
):
try:
c.execute(ddl)
except Exception:
pass
c.execute(
"""CREATE TABLE IF NOT EXISTS key_monitor_history
@@ -1673,6 +1691,25 @@ def to_effective_trade_dict(row):
item["effective_hold_seconds"] = get_effective_trade_field(row, "reviewed_hold_seconds", "hold_seconds", item.get("hold_seconds"))
er_eff = get_effective_trade_field(row, "reviewed_entry_reason", "entry_reason", item.get("entry_reason"))
item["effective_entry_reason"] = (str(er_eff).strip() if er_eff is not None else "") or ""
reviewed_pnl = get_effective_trade_field(row, "reviewed_pnl_amount", "pnl_amount", None)
has_reviewed_pnl = reviewed_pnl is not None and str(reviewed_pnl).strip() != ""
ex_pnl = item.get("exchange_realized_pnl")
if not has_reviewed_pnl and ex_pnl is not None and str(ex_pnl).strip() != "":
try:
item["effective_pnl_amount"] = round(float(ex_pnl), 2)
item["display_pnl_source"] = "exchange"
ex_open = (str(item.get("exchange_opened_at") or "").strip() or None)
ex_close = (str(item.get("exchange_closed_at") or "").strip() or None)
if ex_open:
item["effective_opened_at"] = ex_open
if ex_close:
item["effective_closed_at"] = ex_close
except (TypeError, ValueError):
item["display_pnl_source"] = "local"
elif has_reviewed_pnl:
item["display_pnl_source"] = "reviewed"
else:
item["display_pnl_source"] = "local"
return item
@@ -1930,16 +1967,20 @@ def insert_trade_record(
closed_at=None,
closed_at_ms=None,
exchange_trade_id=None,
key_signal_type=None,
):
hold_minutes = calc_hold_minutes(hold_seconds)
open_ts = opened_at or app_now_str()
close_ts = closed_at or app_now_str()
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
kst = (key_signal_type or "").strip()
if kst not in KEY_MONITOR_AUTO_TYPES:
kst = None
conn.execute(
"INSERT INTO trade_records (symbol,monitor_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"INSERT INTO trade_records (symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
symbol, monitor_type, kst, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
margin_capital, leverage, pnl_amount, hold_seconds,
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id
@@ -2011,6 +2052,33 @@ def ensure_exchange_live_ready():
return True, ""
def order_row_monitor_type(row):
if row is None:
return ORDER_MONITOR_TYPE_MANUAL
try:
keys = row.keys() if hasattr(row, "keys") else []
except Exception:
keys = []
if "monitor_type" in keys:
mt = (row["monitor_type"] or "").strip()
if mt:
return mt
return ORDER_MONITOR_TYPE_MANUAL
def order_row_key_signal_type(row):
if row is None:
return None
try:
keys = row.keys() if hasattr(row, "keys") else []
except Exception:
keys = []
if "key_signal_type" not in keys:
return None
kst = (row["key_signal_type"] or "").strip()
return kst if kst in KEY_MONITOR_AUTO_TYPES else None
def exchange_private_api_configured():
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等。"""
return bool(GATE_API_KEY and GATE_API_SECRET)
@@ -3475,7 +3543,8 @@ def reconcile_external_closes(conn, days=None):
insert_trade_record(
conn,
symbol=r["symbol"],
monitor_type="下单监控",
monitor_type=order_row_monitor_type(r),
key_signal_type=order_row_key_signal_type(r),
direction=r["direction"],
trigger_price=r["trigger_price"],
stop_loss=r["stop_loss"],
@@ -3779,7 +3848,7 @@ def _key_plan_auto_sl_tp(direction, upper, lower, checks, outside_pct):
return E, sl_raw, tp_raw, H
def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_loss, take_profit):
def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_loss, take_profit, key_signal_type=None):
"""
与手动实盘下单对齐的市价开仓与 order_monitors 写入
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
@@ -3898,8 +3967,8 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
"(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
"margin_capital, leverage, trade_style, risk_percent, risk_amount, "
"breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, "
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol,
exchange_symbol,
@@ -3928,6 +3997,7 @@ def _market_open_for_key_monitor(conn, symbol, direction, exchange_symbol, stop_
opened_at_ms,
trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO,
(key_signal_type if key_signal_type in KEY_MONITOR_AUTO_TYPES else None),
),
)
new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
@@ -4053,8 +4123,9 @@ def check_key_monitors():
_finalize_key_monitor_one_shot(conn, r, rr_msg, "rr_insufficient")
continue
key_sig = typ if typ in KEY_MONITOR_AUTO_TYPES else None
ok_trade, trade_err, det = _market_open_for_key_monitor(
conn, sym, direction, exchange_symbol, sl_raw, tp_raw,
conn, sym, direction, exchange_symbol, sl_raw, tp_raw, key_signal_type=key_sig,
)
planned_rr_txt = (
format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
@@ -4301,7 +4372,8 @@ def check_order_monitors():
insert_trade_record(
conn,
symbol=sym,
monitor_type="下单监控",
monitor_type=order_row_monitor_type(r),
key_signal_type=order_row_key_signal_type(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=stop_loss,
@@ -4371,7 +4443,8 @@ def check_order_monitors():
insert_trade_record(
conn,
symbol=sym,
monitor_type="下单监控",
monitor_type=order_row_monitor_type(r),
key_signal_type=order_row_key_signal_type(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=stop_loss,
@@ -4437,7 +4510,8 @@ def force_close_before_reset():
insert_trade_record(
conn,
symbol=r["symbol"],
monitor_type="下单监控",
monitor_type=order_row_monitor_type(r),
key_signal_type=order_row_key_signal_type(r),
direction=direction,
trigger_price=trigger_price,
stop_loss=r["stop_loss"],
@@ -4567,6 +4641,216 @@ def api_sync_positions():
return jsonify({"ok": True, "days": days, "synced": int(synced)})
def _coerce_ts_ms(val):
if val is None or val == "":
return None
try:
v = float(val)
except (TypeError, ValueError):
return None
if v > 1e12:
return int(v)
if v > 1e9:
return int(v)
return int(v * 1000.0)
def _unified_symbol_for_match(symbol_str):
if not symbol_str:
return ""
s = str(symbol_str).strip()
if not s:
return ""
try:
return normalize_exchange_symbol(s).split(":")[0]
except Exception:
x = s.upper().replace(" ", "")
if "/" in x:
return x.split(":")[0]
if x.endswith("USDT") and len(x) > 4:
return f"{x[:-4]}/USDT"
return x
def exchange_position_sync_since_ms():
s = EXCHANGE_POSITION_SYNC_FROM_BJ
if s:
for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d", 10)):
try:
chunk = s[:ln] if len(s) >= ln else s[:10]
dt = datetime.strptime(chunk, fmt)
aware = dt.replace(tzinfo=APP_TZ)
return int(aware.timestamp() * 1000)
except Exception:
continue
dt0 = app_now() - timedelta(days=90)
try:
aware0 = datetime(dt0.year, dt0.month, dt0.day, 0, 0, 0, tzinfo=APP_TZ)
except Exception:
aware0 = datetime.now(APP_TZ)
return int(aware0.timestamp() * 1000)
def _normalize_gate_position_history_entry(p):
if not p or not isinstance(p, dict):
return None
info = p.get("info") or {}
sym = p.get("symbol") or ""
side = (p.get("side") or "").strip().lower()
if side not in ("long", "short"):
sz = info.get("accum_size") if info.get("accum_size") is not None else info.get("size")
try:
szf = float(sz)
if szf > 0:
side = "long"
elif szf < 0:
side = "short"
except (TypeError, ValueError):
side = ""
rp = p.get("realizedPnl")
if rp is None:
rp = info.get("pnl")
try:
rp_f = float(rp) if rp is not None and str(rp).strip() != "" else None
except (TypeError, ValueError):
rp_f = None
close_ms = _coerce_ts_ms(p.get("lastUpdateTimestamp"))
if close_ms is None:
close_ms = _coerce_ts_ms(info.get("time"))
open_ms = _coerce_ts_ms(p.get("timestamp"))
if open_ms is None:
open_ms = _coerce_ts_ms(info.get("first_open_time"))
c_raw = str(info.get("contract") or "").strip()
t_raw = info.get("time")
sync_key = f"{c_raw}|{t_raw}|{side}"
return {
"symbol_u": _unified_symbol_for_match(sym),
"side": side,
"close_ms": close_ms,
"open_ms": open_ms,
"pnl": rp_f,
"sync_key": sync_key,
}
def fetch_gate_positions_close_history():
if not exchange_private_api_configured():
return []
ensure_markets_loaded()
since_ms = exchange_position_sync_since_ms()
try:
rows = exchange.fetch_positions_history(
None,
since=int(since_ms),
limit=int(EXCHANGE_POSITION_HISTORY_LIMIT),
params={"settle": "usdt"},
)
except Exception:
try:
rows = exchange.fetch_positions_history(
None,
since=int(since_ms),
limit=int(EXCHANGE_POSITION_HISTORY_LIMIT),
params={},
)
except Exception:
return []
out = []
for p in rows or []:
h = _normalize_gate_position_history_entry(p)
if h and h["close_ms"] and h["side"] in ("long", "short") and h["symbol_u"]:
out.append(h)
return out
def sync_trade_records_from_exchange(conn):
"""为未同步的 trade_records 回填 Gate 平仓历史中的已实现盈亏。"""
global _LAST_EXCHANGE_PNL_SYNC_AT
if not exchange_private_api_configured():
return
now = time.time()
if now - _LAST_EXCHANGE_PNL_SYNC_AT < 25.0:
return
try:
hist = fetch_gate_positions_close_history()
except Exception:
return
if not hist:
_LAST_EXCHANGE_PNL_SYNC_AT = now
return
candidates = conn.execute(
"""
SELECT id, symbol, direction, closed_at, opened_at, opened_at_ms
FROM trade_records
WHERE (exchange_sync_key IS NULL OR TRIM(exchange_sync_key) = '')
ORDER BY id DESC
LIMIT 120
"""
).fetchall()
if not candidates:
_LAST_EXCHANGE_PNL_SYNC_AT = now
return
used = set()
for tr in candidates:
close_ms_trade = _to_ms_with_fallback(
tr["closed_at_ms"] if "closed_at_ms" in tr.keys() else None, tr["closed_at"]
) or opened_at_str_to_ms(tr["closed_at"])
open_ms_trade = _to_ms_with_fallback(
tr["opened_at_ms"] if "opened_at_ms" in tr.keys() else None, tr["opened_at"]
) or opened_at_str_to_ms(tr["opened_at"])
if close_ms_trade is None:
continue
best = None
best_d = None
for h in hist:
sk = h["sync_key"]
if not sk or sk in used:
continue
if h["symbol_u"] != _unified_symbol_for_match(tr["symbol"]):
continue
if h["side"] != (tr["direction"] or "long").strip().lower():
continue
cm = h["close_ms"]
if cm is None:
continue
if open_ms_trade is not None:
if cm < open_ms_trade - 15 * 60 * 1000:
continue
if cm > open_ms_trade + 15 * 86400 * 1000:
continue
else:
if abs(cm - close_ms_trade) > 3 * 86400 * 1000:
continue
d = abs(cm - close_ms_trade)
if best_d is None or d < best_d:
best_d = d
best = h
if best is None or best_d is None or best_d > 25 * 60 * 1000:
continue
sk = best["sync_key"]
if sk in used:
continue
eo = ms_to_app_local_str(best["open_ms"]) if best.get("open_ms") else None
ec = ms_to_app_local_str(best["close_ms"]) if best.get("close_ms") else None
pnl_val = best.get("pnl")
if pnl_val is None:
pnl_val = 0.0
conn.execute(
"""
UPDATE trade_records
SET exchange_realized_pnl = ?, exchange_opened_at = ?, exchange_closed_at = ?, exchange_sync_key = ?
WHERE id = ?
""",
(float(pnl_val), eo, ec, sk, int(tr["id"])),
)
used.add(sk)
_LAST_EXCHANGE_PNL_SYNC_AT = now
try:
conn.commit()
except Exception:
pass
# ====================== 主页面 ======================
def render_main_page(page="trade"):
now = app_now()
@@ -4586,6 +4870,11 @@ def render_main_page(page="trade"):
order_list = []
for o in raw_order_list:
order_list.append(enrich_order_item(row_to_dict(o), current_capital))
if exchange_private_api_configured():
try:
sync_trade_records_from_exchange(conn)
except Exception:
pass
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall()
records = [to_effective_trade_dict(r) for r in raw_records]
total = len(records)
@@ -5885,7 +6174,8 @@ def del_order(id):
insert_trade_record(
conn,
symbol=row["symbol"],
monitor_type="下单监控",
monitor_type=order_row_monitor_type(row),
key_signal_type=order_row_key_signal_type(row),
direction=row["direction"],
trigger_price=row["trigger_price"],
stop_loss=row["stop_loss"],
+3 -3
View File
@@ -395,7 +395,7 @@
</div>
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}</span>
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
<span class="pos-meta-item">风险: {{ o.risk_percent or '-' }}%≈{% if o.risk_amount is not none %}{{ usdt_fmt(o.risk_amount) }}{% else %}-{% endif %}U</span>
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
@@ -506,7 +506,7 @@
<tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}</td>
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
{% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
@@ -519,7 +519,7 @@
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ signed_usdt_fmt(r.effective_pnl_amount or 0) }}</span></td>
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ signed_usdt_fmt(r.effective_pnl_amount or 0) }}</span>{% if r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></span>{% elif r.display_pnl_source != 'reviewed' %}<span style="font-size:.68rem;color:#8892b0"></span>{% endif %}</td>
<td>
{% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>