修改币种精度

This commit is contained in:
dekun
2026-05-14 11:30:52 +08:00
parent 8290bbc060
commit 7978d40a74
14 changed files with 1254 additions and 263 deletions
+123 -52
View File
@@ -239,10 +239,10 @@ def _wechat_trading_capital_text(fallback=None):
except Exception: except Exception:
trading_capital = None trading_capital = None
if trading_capital is not None: if trading_capital is not None:
return f"{round(float(trading_capital), 4)}U" return f"{round(float(trading_capital), FUNDS_DECIMALS)}U"
if fallback is not None: if fallback is not None:
try: try:
return f"{round(float(fallback), 4)}U" return f"{round(float(fallback), FUNDS_DECIMALS)}U"
except Exception: except Exception:
pass pass
return "-" return "-"
@@ -271,7 +271,7 @@ def build_wechat_close_message(
try: try:
if pnl_amount is not None: if pnl_amount is not None:
pv = float(pnl_amount) pv = float(pnl_amount)
pnl_disp = f"{'+' if pv > 0 else ''}{round(pv, 4)} U" pnl_disp = f"{'+' if pv > 0 else ''}{round(pv, FUNDS_DECIMALS)} U"
else: else:
pnl_disp = "-" pnl_disp = "-"
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -1352,19 +1352,19 @@ def _compute_period_metrics(trades):
closed = len(trades) closed = len(trades)
wins = sum(1 for p, _, _ in trades if p > 0) wins = sum(1 for p, _, _ in trades if p > 0)
losses = sum(1 for p, _, _ in trades if p < 0) losses = sum(1 for p, _, _ in trades if p < 0)
net = round(sum(p for p, _, _ in trades), 4) net = round(sum(p for p, _, _ in trades), FUNDS_DECIMALS)
loss_sum_raw = sum(p for p, _, _ in trades if p < 0) loss_sum_raw = sum(p for p, _, _ in trades if p < 0)
loss_sum_u = round(abs(loss_sum_raw), 4) if loss_sum_raw < 0 else 0.0 loss_sum_u = round(abs(loss_sum_raw), FUNDS_DECIMALS) if loss_sum_raw < 0 else 0.0
neg_pnls = [p for p, _, _ in trades if p < 0] neg_pnls = [p for p, _, _ in trades if p < 0]
pos_pnls = [p for p, _, _ in trades if p > 0] pos_pnls = [p for p, _, _ in trades if p > 0]
max_single_loss = round(min(neg_pnls), 4) if neg_pnls else None max_single_loss = round(min(neg_pnls), FUNDS_DECIMALS) if neg_pnls else None
max_single_profit = round(max(pos_pnls), 4) if pos_pnls else None max_single_profit = round(max(pos_pnls), FUNDS_DECIMALS) if pos_pnls else None
cum = peak = max_dd = 0.0 cum = peak = max_dd = 0.0
for p, _, _ in trades: for p, _, _ in trades:
cum += p cum += p
peak = max(peak, cum) peak = max(peak, cum)
max_dd = max(max_dd, peak - cum) max_dd = max(max_dd, peak - cum)
max_dd = round(max_dd, 4) max_dd = round(max_dd, FUNDS_DECIMALS)
streak = 0 streak = 0
for p, _, _ in reversed(trades): for p, _, _ in reversed(trades):
if p < 0: if p < 0:
@@ -1388,7 +1388,7 @@ def _compute_period_metrics(trades):
else: else:
run = 0 run = 0
worst_day = min(daily.keys(), key=lambda x: daily[x]) worst_day = min(daily.keys(), key=lambda x: daily[x])
worst_day_pnl = round(daily[worst_day], 4) worst_day_pnl = round(daily[worst_day], FUNDS_DECIMALS)
win_rate_pct = round(wins / (wins + losses) * 100, 2) if (wins + losses) else None win_rate_pct = round(wins / (wins + losses) * 100, 2) if (wins + losses) else None
return { return {
"closed_count": closed, "closed_count": closed,
@@ -1619,10 +1619,10 @@ def update_session_capital(conn, session_date, pnl_amount):
new_capital = float(session_row["current_capital"]) + float(pnl_amount) new_capital = float(session_row["current_capital"]) + float(pnl_amount)
conn.execute( conn.execute(
"UPDATE trading_sessions SET current_capital = ?, updated_at = CURRENT_TIMESTAMP WHERE session_date = ?", "UPDATE trading_sessions SET current_capital = ?, updated_at = CURRENT_TIMESTAMP WHERE session_date = ?",
(round(new_capital, 4), session_date) (round(new_capital, FUNDS_DECIMALS), session_date)
) )
conn.commit() conn.commit()
return round(new_capital, 4) return round(new_capital, FUNDS_DECIMALS)
def calc_hold_seconds(opened_at_str, closed_at_dt): def calc_hold_seconds(opened_at_str, closed_at_dt):
@@ -1684,17 +1684,66 @@ def to_effective_trade_dict(row):
return item return item
# USDT 等资金类:展示与入库舍入统一为 2 位小数(与交易所常见口径一致)
FUNDS_DECIMALS = 2
def format_funds_u(value):
if value in (None, ""):
return "-"
try:
return f"{float(value):.{FUNDS_DECIMALS}f}"
except (TypeError, ValueError):
return str(value)
def round_funds(value):
try:
return round(float(value), FUNDS_DECIMALS)
except (TypeError, ValueError):
return None
def _ccxt_swap_symbol_for_precision(symbol):
"""解析为 ccxt markets 中的永续 symbol,供 price_to_precision 使用。"""
raw = (symbol or "").strip()
if not raw:
return None
try:
ensure_markets_loaded()
markets = getattr(exchange, "markets", {}) or {}
except Exception:
return None
upper = raw.upper().replace(" ", "")
candidates = []
candidates.append(normalize_exchange_symbol(raw))
if upper.endswith("USDT") and len(upper) > 4 and "/" not in raw and ":" not in raw:
candidates.append(f"{upper[:-4]}/USDT:USDT")
if "/" not in raw and ":" not in raw and upper.isalnum() and not upper.endswith("USDT"):
candidates.append(f"{upper}/USDT:USDT")
for c in candidates:
if c and c in markets:
return c
return None
def format_price_for_symbol(symbol, value): def format_price_for_symbol(symbol, value):
if value in (None, ""): if value in (None, ""):
return "-" return "-"
try: try:
v = float(value) v = float(value)
except Exception: except (TypeError, ValueError):
return str(value) return str(value)
if v == 0: if v == 0:
return "0" return "0"
try:
ex_sym = _ccxt_swap_symbol_for_precision(symbol)
if ex_sym:
return str(exchange.price_to_precision(ex_sym, v))
except Exception:
pass
av = abs(v) av = abs(v)
# 根据币价量级动态精度:低价币保留更多小数,高价币减少噪音位数 # 无法加载市场或无该合约时:按价格量级回退(尽量不阻断页面)
if av >= 10000: if av >= 10000:
d = 2 d = 2
elif av >= 100: elif av >= 100:
@@ -1734,7 +1783,7 @@ def calc_pnl(direction, trigger_price, exit_price, margin_capital, leverage):
pnl_ratio = (trigger - exit_p) / trigger pnl_ratio = (trigger - exit_p) / trigger
else: else:
pnl_ratio = (exit_p - trigger) / trigger pnl_ratio = (exit_p - trigger) / trigger
return round(margin * lev * pnl_ratio, 4) return round(margin * lev * pnl_ratio, FUNDS_DECIMALS)
except Exception: except Exception:
return 0.0 return 0.0
@@ -1784,7 +1833,7 @@ def calc_risk_amount_from_plan(direction, entry_price, stop_loss, margin_capital
notional = float(margin_capital) * float(leverage) notional = float(margin_capital) * float(leverage)
if notional <= 0: if notional <= 0:
return None return None
return round(notional * rf, 6) return round(notional * rf, FUNDS_DECIMALS)
except Exception: except Exception:
return None return None
@@ -1910,7 +1959,7 @@ def enrich_order_item(raw_item, current_capital):
notional = item.get("notional_value") notional = item.get("notional_value")
ratio = item.get("position_ratio") ratio = item.get("position_ratio")
if notional is None: if notional is None:
notional = round(margin * lev, 4) if margin and lev else 0 notional = round(margin * lev, FUNDS_DECIMALS) if margin and lev else 0
if ratio is None: if ratio is None:
ratio = round(margin / current_capital * 100, 2) if current_capital else 0 ratio = round(margin / current_capital * 100, 2) if current_capital else 0
item["notional_value"] = notional item["notional_value"] = notional
@@ -2083,7 +2132,7 @@ def friendly_exchange_error(err, available_usdt=None):
or "margin" in low and ("not enough" in low or "不足" in msg) or "margin" in low and ("not enough" in low or "不足" in msg)
or "balance" in low and "insufficient" in low or "balance" in low and "insufficient" in low
): ):
tail = f"(当前交易账户可用约 {round(available_usdt, 4)}U" if available_usdt is not None else "" tail = f"(当前交易账户可用约 {round(available_usdt, FUNDS_DECIMALS)}U" if available_usdt is not None else ""
return f"交易所下单失败:保证金不足 {tail}。请降低保证金/杠杆,或先划转USDT到合约账户。" return f"交易所下单失败:保证金不足 {tail}。请降低保证金/杠杆,或先划转USDT到合约账户。"
clean = re.sub(r"\s+", " ", msg).strip() clean = re.sub(r"\s+", " ", msg).strip()
return f"交易所下单失败:{clean}" return f"交易所下单失败:{clean}"
@@ -2173,7 +2222,7 @@ def auto_transfer_once_per_day():
conn.commit() conn.commit()
conn.close() conn.close()
return return
needed = round(max(target_amount - float(to_balance), 0), 4) needed = round(max(target_amount - float(to_balance), 0), FUNDS_DECIMALS)
if needed <= 0: if needed <= 0:
conn.execute( conn.execute(
"INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)",
@@ -2185,12 +2234,12 @@ def auto_transfer_once_per_day():
if from_balance is not None and from_balance < needed: if from_balance is not None and from_balance < needed:
conn.execute( conn.execute(
"INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)",
("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{needed}U,当前{round(from_balance,4)}U") ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{needed}U,当前{round(from_balance, FUNDS_DECIMALS)}U")
) )
conn.commit() conn.commit()
conn.close() conn.close()
send_wechat_msg( send_wechat_msg(
f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{needed}U,当前{round(from_balance,4)}U\n" f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{needed}U,当前{round(from_balance, FUNDS_DECIMALS)}U\n"
f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}" f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}"
) )
return return
@@ -2688,12 +2737,20 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice")) mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice"))
out = {} out = {}
if initial is not None and initial > 0: if initial is not None and initial > 0:
out["initial_margin"] = round(initial, 4) out["initial_margin"] = round(initial, FUNDS_DECIMALS)
if notional is not None and notional > 0: if notional is not None and notional > 0:
out["notional"] = round(notional, 4) out["notional"] = round(notional, FUNDS_DECIMALS)
if unrealized is not None: if unrealized is not None:
out["unrealized_pnl"] = round(unrealized, 6) out["unrealized_pnl"] = round(unrealized, FUNDS_DECIMALS)
if mark is not None and mark > 0: if mark is not None and mark > 0:
ps = p.get("symbol")
try:
ex_sym = _ccxt_swap_symbol_for_precision(ps or "")
if ex_sym:
out["mark_price"] = float(exchange.price_to_precision(ex_sym, mark))
else:
out["mark_price"] = round(mark, 8)
except Exception:
out["mark_price"] = round(mark, 8) out["mark_price"] = round(mark, 8)
return out or None return out or None
@@ -3817,8 +3874,8 @@ def render_main_page(page="trade"):
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
funding_capital, trading_capital = get_exchange_capitals() funding_capital, trading_capital = get_exchange_capitals()
# 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。 # 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。
funding_usdt = round(funding_capital, 4) if funding_capital is not None else None funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4) current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
recommended_capital = get_recommended_capital(current_capital) recommended_capital = get_recommended_capital(current_capital)
key_list = conn.execute("SELECT * FROM key_monitors").fetchall() key_list = conn.execute("SELECT * FROM key_monitors").fetchall()
key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall() key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall()
@@ -3880,6 +3937,7 @@ def render_main_page(page="trade"):
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT, breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
occupied_miss_total=occupied_miss_total, occupied_miss_total=occupied_miss_total,
price_fmt=format_price_for_symbol, price_fmt=format_price_for_symbol,
funds_fmt=format_funds_u,
entry_reason_options=list(ENTRY_REASON_OPTIONS), entry_reason_options=list(ENTRY_REASON_OPTIONS),
entry_reason_other_value=ENTRY_REASON_OTHER, entry_reason_other_value=ENTRY_REASON_OTHER,
exchange_display=EXCHANGE_DISPLAY_NAME, exchange_display=EXCHANGE_DISPLAY_NAME,
@@ -3919,8 +3977,8 @@ def api_account_snapshot():
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
funding_capital, trading_capital = get_exchange_capitals(force=True) funding_capital, trading_capital = get_exchange_capitals(force=True)
funding_usdt = round(funding_capital, 4) if funding_capital is not None else None funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4) current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
recommended_capital = get_recommended_capital(current_capital) recommended_capital = get_recommended_capital(current_capital)
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0] active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
conn.close() conn.close()
@@ -3929,7 +3987,7 @@ def api_account_snapshot():
return jsonify({ return jsonify({
"funding_usdt": funding_usdt, "funding_usdt": funding_usdt,
"current_capital": current_capital, "current_capital": current_capital,
"available_trading_usdt": round(available_trading_usdt, 4) if available_trading_usdt is not None else None, "available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None,
"recommended_capital": recommended_capital, "recommended_capital": recommended_capital,
"active_count": active_count, "active_count": active_count,
"can_trade": can_trade, "can_trade": can_trade,
@@ -3995,19 +4053,21 @@ def api_price_snapshot():
vol_now = round(float(gate.get("vol_break") or 0), 4) vol_now = round(float(gate.get("vol_break") or 0), 4)
vol_avg = round(float(gate.get("avg20") or 0), 4) vol_avg = round(float(gate.get("avg20") or 0), 4)
amp_pct = round(float(gate.get("amp_pct") or 0), 4) amp_pct = round(float(gate.get("amp_pct") or 0), 4)
cfm_close = round(float(gate.get("confirm_close") or 0), 8) cfm_close = float(gate.get("confirm_close") or 0)
edge = round(float(gate.get("edge_price") or 0), 8) edge = float(gate.get("edge_price") or 0)
gate_metrics = ( gate_metrics = (
f"量值:{vol_now}/{vol_avg} " f"量值:{vol_now}/{vol_avg} "
f"幅值:{amp_pct}% " f"幅值:{amp_pct}% "
f"二确值:{cfm_close}@{edge}" f"二确值:{format_price_for_symbol(r['symbol'], cfm_close)}@{format_price_for_symbol(r['symbol'], edge)}"
) )
except Exception: except Exception:
gate_metrics = "" gate_metrics = ""
sym_k = r["symbol"]
key_prices.append({ key_prices.append({
"id": r["id"], "id": r["id"],
"symbol": r["symbol"], "symbol": sym_k,
"price": round(price, 6), "price": round(price, 6),
"price_display": format_price_for_symbol(sym_k, price),
"upper_diff": upper_diff, "upper_diff": upper_diff,
"upper_pct": upper_pct, "upper_pct": upper_pct,
"lower_diff": lower_diff, "lower_diff": lower_diff,
@@ -4026,7 +4086,7 @@ def api_price_snapshot():
leverage = float(r["leverage"] or 0) leverage = float(r["leverage"] or 0)
entry = float(r["trigger_price"] or 0) entry = float(r["trigger_price"] or 0)
pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0 pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0
pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0 pnl_pct = round((pnl / margin * 100), 2) if margin > 0 else 0
rr_ratio = calc_rr_ratio(r["direction"], entry, r["initial_stop_loss"] or r["stop_loss"], r["take_profit"]) rr_ratio = calc_rr_ratio(r["direction"], entry, r["initial_stop_loss"] or r["stop_loss"], r["take_profit"])
ex_sym = resolve_monitor_exchange_symbol(r) ex_sym = resolve_monitor_exchange_symbol(r)
prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"]) prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"])
@@ -4036,13 +4096,15 @@ def api_price_snapshot():
"id": r["id"], "id": r["id"],
"symbol": r["symbol"], "symbol": r["symbol"],
"price": round(price, 6), "price": round(price, 6),
"float_pnl": round(pnl, 6), "price_display": format_price_for_symbol(ex_sym, price),
"float_pnl": round(pnl, FUNDS_DECIMALS),
"float_pct": pnl_pct, "float_pct": pnl_pct,
"rr_ratio": rr_ratio, "rr_ratio": rr_ratio,
"plan_margin": round(margin, 4) if margin else None, "plan_margin": round(margin, FUNDS_DECIMALS) if margin else None,
"exchange_initial_margin": None, "exchange_initial_margin": None,
"exchange_notional": None, "exchange_notional": None,
"exchange_mark_price": None, "exchange_mark_price": None,
"exchange_mark_price_display": None,
"pnl_source": "plan", "pnl_source": "plan",
} }
if ex_metrics: if ex_metrics:
@@ -4051,13 +4113,15 @@ def api_price_snapshot():
if ex_metrics.get("notional") is not None: if ex_metrics.get("notional") is not None:
payload["exchange_notional"] = ex_metrics["notional"] payload["exchange_notional"] = ex_metrics["notional"]
if ex_metrics.get("mark_price") is not None: if ex_metrics.get("mark_price") is not None:
payload["exchange_mark_price"] = ex_metrics["mark_price"] mp = ex_metrics["mark_price"]
payload["exchange_mark_price"] = mp
payload["exchange_mark_price_display"] = format_price_for_symbol(ex_sym, mp)
if ex_metrics.get("unrealized_pnl") is not None: if ex_metrics.get("unrealized_pnl") is not None:
payload["float_pnl"] = round(float(ex_metrics["unrealized_pnl"]), 6) payload["float_pnl"] = round(float(ex_metrics["unrealized_pnl"]), FUNDS_DECIMALS)
payload["pnl_source"] = "exchange" payload["pnl_source"] = "exchange"
denom = ex_metrics.get("initial_margin") or margin denom = ex_metrics.get("initial_margin") or margin
payload["float_pct"] = ( payload["float_pct"] = (
round((payload["float_pnl"] / float(denom)) * 100, 4) if denom and float(denom) > 0 else pnl_pct round((payload["float_pnl"] / float(denom)) * 100, 2) if denom and float(denom) > 0 else pnl_pct
) )
order_prices.append(payload) order_prices.append(payload)
@@ -4109,7 +4173,7 @@ def api_order_defaults():
"exchange_symbol": exchange_symbol, "exchange_symbol": exchange_symbol,
"direction": direction, "direction": direction,
"leverage": leverage, "leverage": leverage,
"available_trading_usdt": round(available, 4) if available is not None else None "available_trading_usdt": round(available, FUNDS_DECIMALS) if available is not None else None
}) })
@@ -4122,7 +4186,7 @@ def order_focus():
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
_, trading_capital_live = get_exchange_capitals() _, trading_capital_live = get_exchange_capitals()
current_capital = round(trading_capital_live, 4) if trading_capital_live is not None else round(local_current_capital, 4) current_capital = round(trading_capital_live, FUNDS_DECIMALS) if trading_capital_live is not None else round(local_current_capital, FUNDS_DECIMALS)
raw_orders = conn.execute("SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC").fetchall() raw_orders = conn.execute("SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC").fetchall()
conn.close() conn.close()
orders = [enrich_order_item(row_to_dict(r), current_capital) for r in raw_orders] orders = [enrich_order_item(row_to_dict(r), current_capital) for r in raw_orders]
@@ -4161,7 +4225,7 @@ def api_order_kline():
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
_, trading_capital_live = get_exchange_capitals() _, trading_capital_live = get_exchange_capitals()
current_capital = round(trading_capital_live, 4) if trading_capital_live is not None else round(local_current_capital, 4) current_capital = round(trading_capital_live, FUNDS_DECIMALS) if trading_capital_live is not None else round(local_current_capital, FUNDS_DECIMALS)
row = conn.execute("SELECT * FROM order_monitors WHERE id=? AND status='active'", (order_id,)).fetchone() row = conn.execute("SELECT * FROM order_monitors WHERE id=? AND status='active'", (order_id,)).fetchone()
conn.close() conn.close()
if not row: if not row:
@@ -4194,7 +4258,7 @@ def api_order_kline():
leverage = float(order_item.get("leverage") or 0) leverage = float(order_item.get("leverage") or 0)
entry = float(order_item.get("trigger_price") or 0) entry = float(order_item.get("trigger_price") or 0)
float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0 float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0
float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0 float_pct = round((float_pnl / margin * 100), 2) if margin > 0 else 0
return jsonify({ return jsonify({
"ok": True, "ok": True,
@@ -4207,13 +4271,17 @@ def api_order_kline():
"trigger_price": order_item.get("trigger_price"), "trigger_price": order_item.get("trigger_price"),
"stop_loss": order_item.get("stop_loss"), "stop_loss": order_item.get("stop_loss"),
"take_profit": order_item.get("take_profit"), "take_profit": order_item.get("take_profit"),
"trigger_price_display": format_price_for_symbol(exchange_symbol, order_item.get("trigger_price")),
"stop_loss_display": format_price_for_symbol(exchange_symbol, order_item.get("stop_loss")),
"take_profit_display": format_price_for_symbol(exchange_symbol, order_item.get("take_profit")),
"margin_capital": order_item.get("margin_capital"), "margin_capital": order_item.get("margin_capital"),
"leverage": order_item.get("leverage"), "leverage": order_item.get("leverage"),
"position_ratio": order_item.get("position_ratio"), "position_ratio": order_item.get("position_ratio"),
"rr_ratio": order_item.get("rr_ratio"), "rr_ratio": order_item.get("rr_ratio"),
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)), "breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
"current_price": round(float(current_price), 8) if current_price else None, "current_price": round(float(current_price), 8) if current_price else None,
"float_pnl": round(float(float_pnl), 6), "current_price_display": format_price_for_symbol(exchange_symbol, current_price) if current_price else None,
"float_pnl": round(float(float_pnl), FUNDS_DECIMALS),
"float_pct": float_pct, "float_pct": float_pct,
}, },
"candles": candles, "candles": candles,
@@ -4311,6 +4379,8 @@ def api_key_kline():
"direction": key_row["direction"] or "long", "direction": key_row["direction"] or "long",
"upper": upper, "upper": upper,
"lower": lower, "lower": lower,
"upper_display": format_price_for_symbol(exchange_symbol, upper) if upper is not None else None,
"lower_display": format_price_for_symbol(exchange_symbol, lower) if lower is not None else None,
"notification_count": int(key_row["notification_count"] or 0), "notification_count": int(key_row["notification_count"] or 0),
"upper_diff": upper_diff, "upper_diff": upper_diff,
"upper_pct": upper_pct, "upper_pct": upper_pct,
@@ -4324,6 +4394,7 @@ def api_key_kline():
"timeframe": timeframe, "timeframe": timeframe,
"limit": limit, "limit": limit,
"current_price": round(float(current_price), 8) if current_price is not None else None, "current_price": round(float(current_price), 8) if current_price is not None else None,
"current_price_display": format_price_for_symbol(exchange_symbol, current_price) if current_price is not None else None,
"key_monitor": key_info, "key_monitor": key_info,
"candles": candles, "candles": candles,
"updated_at": app_now_str(), "updated_at": app_now_str(),
@@ -4466,18 +4537,18 @@ def add_order():
flash("止损方向不合法:请检查入场方向与止损价格关系") flash("止损方向不合法:请检查入场方向与止损价格关系")
return redirect("/") return redirect("/")
risk_percent = max(0.01, float(RISK_PERCENT)) risk_percent = max(0.01, float(RISK_PERCENT))
risk_amount = round(capital_base * risk_percent / 100.0, 4) risk_amount = round(capital_base * risk_percent / 100.0, FUNDS_DECIMALS)
notional_value = round(risk_amount / risk_fraction, 4) notional_value = round(risk_amount / risk_fraction, FUNDS_DECIMALS)
margin_capital = round(notional_value / leverage, 4) margin_capital = round(notional_value / leverage, FUNDS_DECIMALS)
if capital_base and margin_capital > capital_base: if capital_base and margin_capital > capital_base:
conn.close() conn.close()
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例") flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
return redirect("/") return redirect("/")
if available_usdt is not None: if available_usdt is not None:
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4) max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), FUNDS_DECIMALS)
if margin_capital > max_margin: if margin_capital > max_margin:
conn.close() conn.close()
flash(f"保证金不足:交易账户可用约 {round(available_usdt,4)}U,当前最多建议 {max_margin}U") flash(f"保证金不足:交易账户可用约 {round(available_usdt, FUNDS_DECIMALS)}U,当前最多建议 {max_margin}U")
return redirect("/") return redirect("/")
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0 position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
try: try:
@@ -4592,9 +4663,9 @@ def add_order():
_, trading_capital_after = get_exchange_capitals(force=True) _, trading_capital_after = get_exchange_capitals(force=True)
account_base_display = ( account_base_display = (
round(float(trading_capital_after), 4) round(float(trading_capital_after), FUNDS_DECIMALS)
if trading_capital_after is not None if trading_capital_after is not None
else round(float(capital_base), 4) else round(float(capital_base), FUNDS_DECIMALS)
) )
account_name = (os.getenv("BINANCE_ACCOUNT_LABEL") or "binance实盘账户").strip() account_name = (os.getenv("BINANCE_ACCOUNT_LABEL") or "binance实盘账户").strip()
dir_text = "多头(long" if direction == "long" else "空头(short" dir_text = "多头(long" if direction == "long" else "空头(short"
@@ -4620,7 +4691,7 @@ def add_order():
"🧾 订单基础信息", "🧾 订单基础信息",
f"🔖 交易所订单 ID{open_order_id}", f"🔖 交易所订单 ID{open_order_id}",
f"📈 交易风格:{style_zh}", f"📈 交易风格:{style_zh}",
f"⚠️ 单笔风控风险:{risk_percent}% ≈ {round(float(risk_amount_final), 4)} U", f"⚠️ 单笔风控风险:{risk_percent}% ≈ {round(float(risk_amount_final), FUNDS_DECIMALS)} U",
"📊 仓位配置详情", "📊 仓位配置详情",
f"账户基数:{account_base_display} USDT", f"账户基数:{account_base_display} USDT",
f"合约杠杆:{leverage}", f"合约杠杆:{leverage}",
@@ -5350,7 +5421,7 @@ def api_trade_record_review_update():
reviewed_closed_at, reviewed_closed_at,
reviewed_stop_loss, reviewed_stop_loss,
reviewed_take_profit, reviewed_take_profit,
round(reviewed_pnl_amount, 4), round(reviewed_pnl_amount, FUNDS_DECIMALS),
reviewed_result or None, reviewed_result or None,
reviewed_miss_reason or None, reviewed_miss_reason or None,
hold_seconds, hold_seconds,
+35 -27
View File
@@ -130,14 +130,14 @@
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div> <div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div> <div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ s.net_pnl_u }}</div></div> <div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ s.loss_sum_u }}</div></div> <div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ s.max_single_loss }}{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ s.max_single_profit }}{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ s.max_drawdown_u }}</div></div> <div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div> <div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div> <div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}{{ s.worst_day_pnl }}U{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}{{ funds_fmt(s.worst_day_pnl) }}U{% else %}-{% endif %}</div></div>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
@@ -165,9 +165,9 @@
<div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div> <div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div>
<div class="stat-item"><div class="label">错过次数</div><div class="value">{{ miss_count }}</div></div> <div class="stat-item"><div class="label">错过次数</div><div class="value">{{ miss_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div> <div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div>
<div class="stat-item"><div class="label">资金账户(USDT)</div><div class="value" id="total-capital">{% if funding_usdt is not none %}{{ funding_usdt }}U{% else %}—{% endif %}</div></div> <div class="stat-item"><div class="label">资金账户(USDT)</div><div class="value" id="total-capital">{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}</div></div>
<div class="stat-item"><div class="label">交易日</div><div class="value">{{ trading_day }}</div></div> <div class="stat-item"><div class="label">交易日</div><div class="value">{{ trading_day }}</div></div>
<div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ current_capital }}U</div></div> <div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ funds_fmt(current_capital) }}U</div></div>
</div> </div>
<div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div> <div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div>
@@ -300,14 +300,14 @@
<div class="list-item"> <div class="list-item">
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div> <div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
<div> <div>
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ o.risk_amount or '-' }}U 风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U
| {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ o.breakeven_price or '-' }}{% else %}移动保本:关{% endif %} | {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
<br> <br>
成交:{{ o.trigger_price }} 止损:{{ o.stop_loss }} 止盈:{{ o.take_profit }} 成交:{{ price_fmt(o.symbol, o.trigger_price) }} 止损:{{ price_fmt(o.symbol, o.stop_loss) }} 止盈:{{ price_fmt(o.symbol, o.take_profit) }}
| 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span> | 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span>
| 现价:<span id="order-price-{{ o.id }}">-</span> | 现价:<span id="order-price-{{ o.id }}">-</span>
| 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span> | 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span>
| 计划基数:{{ o.margin_capital }}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span> | 计划基数:{{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span>
| 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}% | 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}%
</div> </div>
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a> <a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
@@ -335,18 +335,18 @@
<td>{{ r.symbol }}</td> <td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}</td> <td>{{ r.monitor_type }}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td> <td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ r.trigger_price }}</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 %} {% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %} {% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td> <td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td> <td>{{ price_fmt(r.symbol, tp_show) }}</td>
<td>{{ r.margin_capital or '-' }}</td> <td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td> <td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td> <td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td> <td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_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 %} {% 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 '') }}">{{ 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></td>
<td> <td>
{% set effective_result = r.effective_result %} {% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span> {% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
@@ -1091,7 +1091,7 @@ setTimeout(() => {
let latestAvailableUsdt = null; let latestAvailableUsdt = null;
const lastPriceMap = {}; const lastPriceMap = {};
function formatSigned(v, digits=4){ function formatSigned(v, digits=2){
if(v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-"; if(v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-";
const n = Number(v); const n = Number(v);
const sign = n > 0 ? "+" : ""; const sign = n > 0 ? "+" : "";
@@ -1121,7 +1121,7 @@ function refreshPriceSnapshot(){
(data.key_prices || []).forEach(k=>{ (data.key_prices || []).forEach(k=>{
const pEl = document.getElementById(`key-price-${k.id}`); const pEl = document.getElementById(`key-price-${k.id}`);
if(pEl){ if(pEl){
pEl.innerText = Number(k.price).toFixed(6); pEl.innerText = k.price_display || (Number.isFinite(Number(k.price)) ? Number(k.price).toFixed(6) : "-");
paintPriceTrend(pEl, `k-${k.id}`, Number(k.price)); paintPriceTrend(pEl, `k-${k.id}`, Number(k.price));
} }
const upEl = document.getElementById(`key-up-diff-${k.id}`); const upEl = document.getElementById(`key-up-diff-${k.id}`);
@@ -1146,17 +1146,25 @@ function refreshPriceSnapshot(){
const pEl = document.getElementById(`order-price-${o.id}`); const pEl = document.getElementById(`order-price-${o.id}`);
if(pEl){ if(pEl){
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })(); const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
let disp = "";
if(hasMark && o.exchange_mark_price_display){
disp = o.exchange_mark_price_display;
} else if(o.price_display){
disp = o.price_display;
} else {
const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price); const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
const decimals = hasMark ? 8 : 6; disp = Number.isFinite(px) ? px.toFixed(6) : "-";
pEl.innerText = px.toFixed(decimals); }
paintPriceTrend(pEl, `o-${o.id}`, px); pEl.innerText = disp;
const pxNum = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
paintPriceTrend(pEl, `o-${o.id}`, Number.isFinite(pxNum) ? pxNum : px);
} }
const exM = document.getElementById(`order-ex-margin-${o.id}`); const exM = document.getElementById(`order-ex-margin-${o.id}`);
if(exM){ if(exM){
const mv = o.exchange_initial_margin; const mv = o.exchange_initial_margin;
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv); const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
if(!Number.isNaN(mn)){ if(!Number.isNaN(mn)){
exM.innerText = `${mn.toFixed(4)}U`; exM.innerText = `${mn.toFixed(2)}U`;
} else { } else {
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null; const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
exM.innerText = (prc === 0) ? "无仓数据" : "-"; exM.innerText = (prc === 0) ? "无仓数据" : "-";
@@ -1164,7 +1172,7 @@ function refreshPriceSnapshot(){
} }
const pnlEl = document.getElementById(`order-pnl-${o.id}`); const pnlEl = document.getElementById(`order-pnl-${o.id}`);
if(pnlEl){ if(pnlEl){
pnlEl.innerText = `${formatSigned(o.float_pnl, 4)}U (${formatSigned(o.float_pct, 2)}%)`; pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
pnlEl.classList.remove("price-up","price-down","price-flat"); pnlEl.classList.remove("price-up","price-down","price-flat");
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up"); if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down"); else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
@@ -1198,7 +1206,7 @@ function refreshOrderDefaults(){
const fullEl = document.getElementById("use-full-margin"); const fullEl = document.getElementById("use-full-margin");
const marginEl = document.getElementById("order-margin"); const marginEl = document.getElementById("order-margin");
if(fullEl && marginEl && fullEl.checked){ if(fullEl && marginEl && fullEl.checked){
const m = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(4); const m = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(2);
marginEl.value = m; marginEl.value = m;
} }
} }
@@ -1209,18 +1217,18 @@ function refreshAccountSnapshot(){
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{ fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
if (typeof data.funding_usdt !== "undefined") { if (typeof data.funding_usdt !== "undefined") {
const el = document.getElementById("total-capital"); const el = document.getElementById("total-capital");
if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${data.funding_usdt}U`; if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${Number(data.funding_usdt).toFixed(2)}U`;
} }
if (typeof data.current_capital !== "undefined") { if (typeof data.current_capital !== "undefined") {
const el = document.getElementById("current-capital"); const el = document.getElementById("current-capital");
if(el) el.innerText = `${data.current_capital}U`; if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`;
} }
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) { if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
latestAvailableUsdt = Number(data.available_trading_usdt); latestAvailableUsdt = Number(data.available_trading_usdt);
} }
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00"; const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00";
const tip = document.getElementById("order-rule-tip"); const tip = document.getElementById("order-rule-tip");
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt}U` : ""; const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt.toFixed(2)}U` : "";
if(tip){ if(tip){
tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`; tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`;
} }
@@ -1236,7 +1244,7 @@ if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){ fullMarginEl.addEventListener("change", function(){
const marginEl = document.getElementById("order-margin"); const marginEl = document.getElementById("order-margin");
if(marginEl && this.checked && latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)){ if(marginEl && this.checked && latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)){
marginEl.value = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(4); marginEl.value = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(2);
} }
}); });
} }
@@ -166,7 +166,7 @@ function addLine(price, title, color){
function paintMeta(data){ function paintMeta(data){
const key = data.key_monitor || null; const key = data.key_monitor || null;
document.getElementById("m-symbol").innerText = data.symbol || "-"; document.getElementById("m-symbol").innerText = data.symbol || "-";
document.getElementById("m-price").innerText = fmt(data.current_price,8); document.getElementById("m-price").innerText = data.current_price_display || fmt(data.current_price,8);
if(!key){ if(!key){
document.getElementById("m-type").innerText = "未匹配到关键位"; document.getElementById("m-type").innerText = "未匹配到关键位";
@@ -180,8 +180,8 @@ function paintMeta(data){
document.getElementById("m-type").innerText = key.monitor_type || "-"; document.getElementById("m-type").innerText = key.monitor_type || "-";
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多"; document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = fmt(key.upper,8); document.getElementById("m-upper").innerText = key.upper_display || fmt(key.upper,8);
document.getElementById("m-lower").innerText = fmt(key.lower,8); document.getElementById("m-lower").innerText = key.lower_display || fmt(key.lower,8);
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`; document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`; document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
} }
@@ -140,13 +140,13 @@ function addLine(price, title, color){
function paintOrder(order){ function paintOrder(order){
document.getElementById("m-symbol").innerText = order.symbol || "-"; document.getElementById("m-symbol").innerText = order.symbol || "-";
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多"; document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8); document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8); document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8); document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`; document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
document.getElementById("m-price").innerText = fmt(order.current_price, 8); document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
const pnlEl = document.getElementById("m-pnl"); const pnlEl = document.getElementById("m-pnl");
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`; pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff"); pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
} }
@@ -142,15 +142,15 @@ function addLine(price, title, color){
function paintOrder(order){ function paintOrder(order){
document.getElementById("m-symbol").innerText = order.symbol || "-"; document.getElementById("m-symbol").innerText = order.symbol || "-";
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多"; document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8); document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8); document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8); document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`; document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
document.getElementById("m-breakeven").innerText = document.getElementById("m-breakeven").innerText =
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启"; (order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
document.getElementById("m-price").innerText = fmt(order.current_price, 8); document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
const pnlEl = document.getElementById("m-pnl"); const pnlEl = document.getElementById("m-pnl");
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`; pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff"); pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
} }
+323 -72
View File
@@ -238,10 +238,10 @@ def _wechat_trading_capital_text(fallback=None):
except Exception: except Exception:
trading_capital = None trading_capital = None
if trading_capital is not None: if trading_capital is not None:
return f"{round(float(trading_capital), 4)}U" return f"{round(float(trading_capital), 2)}U"
if fallback is not None: if fallback is not None:
try: try:
return f"{round(float(fallback), 4)}U" return f"{round(float(fallback), 2)}U"
except Exception: except Exception:
pass pass
return "-" return "-"
@@ -265,12 +265,12 @@ def build_wechat_close_message(
ep = format_price_for_symbol(symbol, trigger_price) ep = format_price_for_symbol(symbol, trigger_price)
cp = format_price_for_symbol(symbol, current_price) cp = format_price_for_symbol(symbol, current_price)
tp = format_price_for_symbol(symbol, take_profit) tp = format_price_for_symbol(symbol, take_profit)
sl = format_price_for_symbol(symbol, stop_loss) sl = format_wechat_scalar_2dp(stop_loss)
cap_txt = _wechat_trading_capital_text(session_capital_fallback) cap_txt = _wechat_trading_capital_text(session_capital_fallback)
try: try:
if pnl_amount is not None: if pnl_amount is not None:
pv = float(pnl_amount) pv = float(pnl_amount)
pnl_disp = f"{'+' if pv > 0 else ''}{round(pv, 4)} U" pnl_disp = f"{'+' if pv > 0 else ''}{round(pv, 2)} U"
else: else:
pnl_disp = "-" pnl_disp = "-"
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -300,7 +300,7 @@ def build_wechat_close_message(
def build_wechat_breakeven_message(symbol, direction, arm_txt, now_rr, locked_r, new_sl): def build_wechat_breakeven_message(symbol, direction, arm_txt, now_rr, locked_r, new_sl):
sl_fmt = format_price_for_symbol(symbol, new_sl) sl_fmt = format_wechat_scalar_2dp(new_sl)
return "\n".join( return "\n".join(
[ [
f"# 🛡️ {symbol} 保护位更新", f"# 🛡️ {symbol} 保护位更新",
@@ -975,6 +975,7 @@ def init_db():
breakeven_armed INTEGER DEFAULT 0, breakeven_price REAL, breakeven_armed INTEGER DEFAULT 0, breakeven_price REAL,
notional_value REAL, position_ratio REAL, base_amount REAL, notional_value REAL, position_ratio REAL, base_amount REAL,
order_amount REAL, exchange_order_id TEXT, exchange_close_order_id TEXT, order_amount REAL, exchange_order_id TEXT, exchange_close_order_id TEXT,
exchange_margin_usdt REAL,
opened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, opened_at_ms INTEGER, session_date TEXT, opened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, opened_at_ms INTEGER, session_date TEXT,
status TEXT DEFAULT "active")''') status TEXT DEFAULT "active")''')
@@ -1088,6 +1089,10 @@ def init_db():
c.execute("ALTER TABLE order_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 1") c.execute("ALTER TABLE order_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 1")
except Exception: except Exception:
pass pass
try:
c.execute("ALTER TABLE order_monitors ADD COLUMN exchange_margin_usdt REAL")
except Exception:
pass
try: try:
c.execute("UPDATE order_monitors SET opened_at = datetime('now') WHERE opened_at IS NULL OR opened_at = ''") c.execute("UPDATE order_monitors SET opened_at = datetime('now') WHERE opened_at IS NULL OR opened_at = ''")
except: pass except: pass
@@ -1351,19 +1356,19 @@ def _compute_period_metrics(trades):
closed = len(trades) closed = len(trades)
wins = sum(1 for p, _, _ in trades if p > 0) wins = sum(1 for p, _, _ in trades if p > 0)
losses = sum(1 for p, _, _ in trades if p < 0) losses = sum(1 for p, _, _ in trades if p < 0)
net = round(sum(p for p, _, _ in trades), 4) net = round(sum(p for p, _, _ in trades), 2)
loss_sum_raw = sum(p for p, _, _ in trades if p < 0) loss_sum_raw = sum(p for p, _, _ in trades if p < 0)
loss_sum_u = round(abs(loss_sum_raw), 4) if loss_sum_raw < 0 else 0.0 loss_sum_u = round(abs(loss_sum_raw), 2) if loss_sum_raw < 0 else 0.0
neg_pnls = [p for p, _, _ in trades if p < 0] neg_pnls = [p for p, _, _ in trades if p < 0]
pos_pnls = [p for p, _, _ in trades if p > 0] pos_pnls = [p for p, _, _ in trades if p > 0]
max_single_loss = round(min(neg_pnls), 4) if neg_pnls else None max_single_loss = round(min(neg_pnls), 2) if neg_pnls else None
max_single_profit = round(max(pos_pnls), 4) if pos_pnls else None max_single_profit = round(max(pos_pnls), 2) if pos_pnls else None
cum = peak = max_dd = 0.0 cum = peak = max_dd = 0.0
for p, _, _ in trades: for p, _, _ in trades:
cum += p cum += p
peak = max(peak, cum) peak = max(peak, cum)
max_dd = max(max_dd, peak - cum) max_dd = max(max_dd, peak - cum)
max_dd = round(max_dd, 4) max_dd = round(max_dd, 2)
streak = 0 streak = 0
for p, _, _ in reversed(trades): for p, _, _ in reversed(trades):
if p < 0: if p < 0:
@@ -1387,7 +1392,7 @@ def _compute_period_metrics(trades):
else: else:
run = 0 run = 0
worst_day = min(daily.keys(), key=lambda x: daily[x]) worst_day = min(daily.keys(), key=lambda x: daily[x])
worst_day_pnl = round(daily[worst_day], 4) worst_day_pnl = round(daily[worst_day], 2)
win_rate_pct = round(wins / (wins + losses) * 100, 2) if (wins + losses) else None win_rate_pct = round(wins / (wins + losses) * 100, 2) if (wins + losses) else None
return { return {
"closed_count": closed, "closed_count": closed,
@@ -1640,9 +1645,8 @@ def to_effective_trade_dict(row):
return item return item
def format_price_for_symbol(symbol, value): def format_price_magnitude_fallback(value):
if value in (None, ""): """无 markets 或解析失败时的价格展示兜底(按量级)。"""
return "-"
try: try:
v = float(value) v = float(value)
except Exception: except Exception:
@@ -1650,7 +1654,6 @@ def format_price_for_symbol(symbol, value):
if v == 0: if v == 0:
return "0" return "0"
av = abs(v) av = abs(v)
# 根据币价量级动态精度:低价币保留更多小数,高价币减少噪音位数
if av >= 10000: if av >= 10000:
d = 2 d = 2
elif av >= 100: elif av >= 100:
@@ -1667,6 +1670,88 @@ def format_price_for_symbol(symbol, value):
return text.rstrip("0").rstrip(".") if "." in text else text return text.rstrip("0").rstrip(".") if "." in text else text
def resolve_ccxt_price_symbol(symbol):
"""将界面/库中的品种名转为 ccxt 永续合约 id(如 BTC/USDT -> BTC/USDT:USDT)。"""
s = (symbol or "").strip()
if not s:
return ""
if "/" not in s and ":" not in s:
s = f"{s.upper()}/USDT"
else:
s = s.upper()
return normalize_exchange_symbol(s)
def round_price_to_exchange(exchange_symbol, price):
"""与交易所 tick 对齐后的 float,供入库与计算;失败时退回 float(price)。"""
if price in (None, ""):
return None
try:
v = float(price)
except (TypeError, ValueError):
return None
if not exchange_symbol:
return v
try:
ensure_markets_loaded()
s = exchange.price_to_precision(exchange_symbol, v)
return float(s)
except Exception:
return v
def format_price_for_symbol(symbol, value):
"""价格展示:与交易所 price_to_precision 一致(与入库 round_price_to_exchange 对齐)。"""
if value in (None, ""):
return "-"
try:
v = float(value)
except Exception:
return str(value)
ex = resolve_ccxt_price_symbol(symbol)
if not ex:
return format_price_magnitude_fallback(v)
try:
ensure_markets_loaded()
return exchange.price_to_precision(ex, v)
except Exception:
return format_price_magnitude_fallback(v)
def format_usdt(value):
"""USDT 资金类展示:固定两位小数。"""
if value in (None, ""):
return "-"
try:
return f"{float(value):.2f}"
except (TypeError, ValueError):
return str(value)
def format_signed_usdt(value):
"""USDT 盈亏等可正可负:+1.23 / -0.50 / 0.00"""
if value in (None, ""):
return "-"
try:
v = float(value)
except (TypeError, ValueError):
return str(value)
if v == 0:
return "0.00"
sign = "+" if v > 0 else ""
return f"{sign}{v:.2f}"
def format_wechat_scalar_2dp(value):
"""企业微信推送:数值统一两位小数(与交易所 tick 无关)。"""
if value in (None, ""):
return "-"
try:
return f"{float(value):.2f}"
except (TypeError, ValueError):
return str(value)
def format_hold_minutes(minutes): def format_hold_minutes(minutes):
if not minutes: if not minutes:
return "0分钟" return "0分钟"
@@ -1866,7 +1951,7 @@ def enrich_order_item(raw_item, current_capital):
notional = item.get("notional_value") notional = item.get("notional_value")
ratio = item.get("position_ratio") ratio = item.get("position_ratio")
if notional is None: if notional is None:
notional = round(margin * lev, 4) if margin and lev else 0 notional = round(margin * lev, 2) if margin and lev else 0
if ratio is None: if ratio is None:
ratio = round(margin / current_capital * 100, 2) if current_capital else 0 ratio = round(margin / current_capital * 100, 2) if current_capital else 0
item["notional_value"] = notional item["notional_value"] = notional
@@ -2140,7 +2225,7 @@ def friendly_exchange_error(err, available_usdt=None):
or "margin" in low and ("not enough" in low or "不足" in msg) or "margin" in low and ("not enough" in low or "不足" in msg)
or "balance" in low and "insufficient" in low or "balance" in low and "insufficient" in low
): ):
tail = f"(当前交易账户可用约 {round(available_usdt, 4)}U" if available_usdt is not None else "" tail = f"(当前交易账户可用约 {round(available_usdt, 2)}U" if available_usdt is not None else ""
return f"交易所下单失败:保证金不足 {tail}。请降低保证金/杠杆,或先划转USDT到合约账户。" return f"交易所下单失败:保证金不足 {tail}。请降低保证金/杠杆,或先划转USDT到合约账户。"
clean = re.sub(r"\s+", " ", msg).strip() clean = re.sub(r"\s+", " ", msg).strip()
return f"交易所下单失败:{clean}" return f"交易所下单失败:{clean}"
@@ -2236,7 +2321,7 @@ def auto_transfer_once_per_day():
if needed <= 0: if needed <= 0:
conn.execute( conn.execute(
"INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)",
("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "skipped", f"{AUTO_TRANSFER_TO}账户已达到目标{target_amount}U") ("auto_daily", transfer_day, 0, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "skipped", f"{AUTO_TRANSFER_TO}账户已达到目标{round(float(target_amount), 2)}U")
) )
conn.commit() conn.commit()
conn.close() conn.close()
@@ -2244,12 +2329,12 @@ def auto_transfer_once_per_day():
if from_balance is not None and from_balance < needed: if from_balance is not None and from_balance < needed:
conn.execute( conn.execute(
"INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)", "INSERT INTO transfer_logs (transfer_type, transfer_day, amount, from_account, to_account, status, message) VALUES (?,?,?,?,?,?,?)",
("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{needed}U,当前{round(from_balance,4)}U") ("auto_daily", transfer_day, needed, AUTO_TRANSFER_FROM, AUTO_TRANSFER_TO, "failed", f"{AUTO_TRANSFER_FROM}账户USDT不足,需{round(needed, 2)}U,当前{round(from_balance, 2)}U")
) )
conn.commit() conn.commit()
conn.close() conn.close()
send_wechat_msg( send_wechat_msg(
f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{needed}U,当前{round(from_balance,4)}U\n" f"自动划转失败:{AUTO_TRANSFER_FROM}余额不足,需{round(needed, 2)}U,当前{round(from_balance, 2)}U\n"
f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}" f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}"
) )
return return
@@ -2263,13 +2348,13 @@ def auto_transfer_once_per_day():
conn.close() conn.close()
if ok: if ok:
send_wechat_msg( send_wechat_msg(
f"自动划转成功:补足到{target_amount}U,实际划转{needed}U " f"自动划转成功:补足到{round(float(target_amount), 2)}U,实际划转{round(needed, 2)}U "
f"{AUTO_TRANSFER_FROM}->{AUTO_TRANSFER_TO}\n" f"{AUTO_TRANSFER_FROM}->{AUTO_TRANSFER_TO}\n"
f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}" f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}"
) )
else: else:
send_wechat_msg( send_wechat_msg(
f"自动划转失败:计划补足到{target_amount}U,需划转{needed}U\n原因:{msg}\n" f"自动划转失败:计划补足到{round(float(target_amount), 2)}U,需划转{round(needed, 2)}U\n原因:{msg}\n"
f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}" f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}"
) )
@@ -2724,17 +2809,17 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice")) mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice"))
out = {} out = {}
if initial is not None and initial > 0: if initial is not None and initial > 0:
out["initial_margin"] = round(initial, 4) out["initial_margin"] = round(initial, 2)
if notional is not None and notional > 0: if notional is not None and notional > 0:
out["notional"] = round(notional, 4) out["notional"] = round(notional, 2)
if unrealized is not None: if unrealized is not None:
out["unrealized_pnl"] = round(unrealized, 6) out["unrealized_pnl"] = round(unrealized, 2)
if mark is not None and mark > 0: if mark is not None and mark > 0:
out["mark_price"] = round(mark, 8) out["mark_price"] = round(mark, 8)
return out or None return out or None
def get_live_position_exchange_metrics(exchange_symbol, direction): def get_live_position_exchange_metrics(exchange_symbol, direction, order_leverage=None):
ensure_markets_loaded() ensure_markets_loaded()
if not exchange_private_api_configured() or not exchange_symbol: if not exchange_private_api_configured() or not exchange_symbol:
return None return None
@@ -2746,7 +2831,72 @@ def get_live_position_exchange_metrics(exchange_symbol, direction):
except Exception: except Exception:
return None return None
p = _select_live_position_row(rows, exchange_symbol, direction) p = _select_live_position_row(rows, exchange_symbol, direction)
return parse_ccxt_position_metrics(p) return parse_ccxt_position_metrics(p, order_leverage=order_leverage)
def _order_row_exchange_margin_usdt(row):
if not row:
return None
try:
keys = row.keys()
except Exception:
return None
if "exchange_margin_usdt" not in keys:
return None
v = row["exchange_margin_usdt"]
if v is None:
return None
try:
x = float(v)
except (TypeError, ValueError):
return None
return x if x > 0 else None
def margin_capital_for_trade_record(order_row):
"""trade_records.基数:优先交易所持仓保证金快照,旧数据无快照时回退计划保证金。"""
ex = _order_row_exchange_margin_usdt(order_row)
if ex is not None:
return round(ex, 2)
if not order_row:
return None
try:
v = order_row["margin_capital"]
except (TypeError, KeyError, IndexError):
return None
if v is None:
return None
try:
return float(v)
except (TypeError, ValueError):
return None
def try_persist_exchange_margin_for_order(conn, order_id, exchange_symbol, direction, order_leverage=None, max_attempts=6, sleep_s=0.45):
"""开仓成功后持仓可见时拉取交易所保证金并写入 order_monitors(平仓后无法再取)。"""
if not conn or not order_id or not exchange_private_api_configured():
return False
direction = (direction or "long").lower()
ex_sym = (exchange_symbol or "").strip()
if not ex_sym:
return False
n = max(1, int(max_attempts))
delay = max(0.05, float(sleep_s))
for _ in range(n):
pm = get_live_position_exchange_metrics(ex_sym, direction, order_leverage=order_leverage)
if pm and pm.get("initial_margin") is not None:
try:
v = float(pm["initial_margin"])
except (TypeError, ValueError):
v = 0.0
if v > 0:
conn.execute(
"UPDATE order_monitors SET exchange_margin_usdt=? WHERE id=?",
(round(v, 4), int(order_id)),
)
return True
time.sleep(delay)
return False
def opened_at_str_to_ms(opened_at_str): def opened_at_str_to_ms(opened_at_str):
@@ -3055,7 +3205,7 @@ def reconcile_external_closes(conn, days=None):
stop_loss=r["stop_loss"], stop_loss=r["stop_loss"],
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"], initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
take_profit=r["take_profit"], take_profit=r["take_profit"],
margin_capital=r["margin_capital"], margin_capital=margin_capital_for_trade_record(r),
leverage=r["leverage"], leverage=r["leverage"],
pnl_amount=pnl_amount, pnl_amount=pnl_amount,
hold_seconds=hold_seconds, hold_seconds=hold_seconds,
@@ -3429,6 +3579,21 @@ def check_order_monitors():
pid, sym, direction, trigger_price, stop_loss, take_profit = r["id"], r["symbol"], r["direction"], r["trigger_price"], r["stop_loss"], r["take_profit"] pid, sym, direction, trigger_price, stop_loss, take_profit = r["id"], r["symbol"], r["direction"], r["trigger_price"], r["stop_loss"], r["take_profit"]
margin_capital = r["margin_capital"] or DAILY_START_CAPITAL margin_capital = r["margin_capital"] or DAILY_START_CAPITAL
leverage = r["leverage"] or infer_leverage(sym) leverage = r["leverage"] or infer_leverage(sym)
trade_basis_row = row_to_dict(r)
ex_sym = r["exchange_symbol"] or normalize_exchange_symbol(sym)
if _order_row_exchange_margin_usdt(r) is None and exchange_private_api_configured():
pm = get_live_position_exchange_metrics(ex_sym, direction, order_leverage=leverage)
if pm and pm.get("initial_margin") is not None:
try:
mv = float(pm["initial_margin"])
if mv > 0:
conn.execute(
"UPDATE order_monitors SET exchange_margin_usdt=? WHERE id=?",
(round(mv, 4), pid),
)
trade_basis_row["exchange_margin_usdt"] = round(mv, 4)
except (TypeError, ValueError):
pass
session_date = r["session_date"] or get_trading_day() session_date = r["session_date"] or get_trading_day()
p = get_price(sym) p = get_price(sym)
if not p: continue if not p: continue
@@ -3466,6 +3631,7 @@ def check_order_monitors():
direction == "long" and new_sl > float(stop_loss) direction == "long" and new_sl > float(stop_loss)
) )
if should_move: if should_move:
new_sl = round_price_to_exchange(resolve_monitor_exchange_symbol(r), new_sl)
conn.execute( conn.execute(
"UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?",
(new_sl, new_sl, pid), (new_sl, new_sl, pid),
@@ -3585,7 +3751,7 @@ def check_order_monitors():
stop_loss=stop_loss, stop_loss=stop_loss,
initial_stop_loss=r["initial_stop_loss"] or stop_loss, initial_stop_loss=r["initial_stop_loss"] or stop_loss,
take_profit=take_profit, take_profit=take_profit,
margin_capital=margin_capital, margin_capital=margin_capital_for_trade_record(trade_basis_row),
leverage=leverage, leverage=leverage,
pnl_amount=pnl_amount, pnl_amount=pnl_amount,
hold_seconds=hold_seconds, hold_seconds=hold_seconds,
@@ -3655,7 +3821,7 @@ def check_order_monitors():
stop_loss=stop_loss, stop_loss=stop_loss,
initial_stop_loss=r["initial_stop_loss"] or stop_loss, initial_stop_loss=r["initial_stop_loss"] or stop_loss,
take_profit=take_profit, take_profit=take_profit,
margin_capital=margin_capital, margin_capital=margin_capital_for_trade_record(trade_basis_row),
leverage=leverage, leverage=leverage,
pnl_amount=pnl_amount, pnl_amount=pnl_amount,
hold_seconds=hold_seconds, hold_seconds=hold_seconds,
@@ -3720,7 +3886,7 @@ def force_close_before_reset():
stop_loss=r["stop_loss"], stop_loss=r["stop_loss"],
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"], initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
take_profit=r["take_profit"], take_profit=r["take_profit"],
margin_capital=margin_capital, margin_capital=margin_capital_for_trade_record(r),
leverage=leverage, leverage=leverage,
pnl_amount=pnl_amount, pnl_amount=pnl_amount,
hold_seconds=hold_seconds, hold_seconds=hold_seconds,
@@ -3853,9 +4019,9 @@ def render_main_page(page="trade"):
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
funding_capital, trading_capital = get_exchange_capitals() funding_capital, trading_capital = get_exchange_capitals()
# 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。 # 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。
funding_usdt = round(funding_capital, 4) if funding_capital is not None else None funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4) current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
recommended_capital = get_recommended_capital(current_capital) recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
key_list = conn.execute("SELECT * FROM key_monitors").fetchall() key_list = conn.execute("SELECT * FROM key_monitors").fetchall()
key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall() key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall()
stats_bundle = compute_stats_bundle(conn, trading_day, now) stats_bundle = compute_stats_bundle(conn, trading_day, now)
@@ -3916,6 +4082,8 @@ def render_main_page(page="trade"):
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT, breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
occupied_miss_total=occupied_miss_total, occupied_miss_total=occupied_miss_total,
price_fmt=format_price_for_symbol, price_fmt=format_price_for_symbol,
usdt_fmt=format_usdt,
signed_usdt_fmt=format_signed_usdt,
entry_reason_options=list(ENTRY_REASON_OPTIONS), entry_reason_options=list(ENTRY_REASON_OPTIONS),
entry_reason_other_value=ENTRY_REASON_OTHER, entry_reason_other_value=ENTRY_REASON_OTHER,
exchange_display=EXCHANGE_DISPLAY_NAME, exchange_display=EXCHANGE_DISPLAY_NAME,
@@ -3955,9 +4123,9 @@ def api_account_snapshot():
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
funding_capital, trading_capital = get_exchange_capitals(force=True) funding_capital, trading_capital = get_exchange_capitals(force=True)
funding_usdt = round(funding_capital, 4) if funding_capital is not None else None funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4) current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
recommended_capital = get_recommended_capital(current_capital) recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0] active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
conn.close() conn.close()
can_trade = trading_day_reset_allows_new_open(now) and active_count == 0 can_trade = trading_day_reset_allows_new_open(now) and active_count == 0
@@ -3965,7 +4133,7 @@ def api_account_snapshot():
return jsonify({ return jsonify({
"funding_usdt": funding_usdt, "funding_usdt": funding_usdt,
"current_capital": current_capital, "current_capital": current_capital,
"available_trading_usdt": round(available_trading_usdt, 4) if available_trading_usdt is not None else None, "available_trading_usdt": round(available_trading_usdt, 2) if available_trading_usdt is not None else None,
"recommended_capital": recommended_capital, "recommended_capital": recommended_capital,
"active_count": active_count, "active_count": active_count,
"can_trade": can_trade, "can_trade": can_trade,
@@ -3983,6 +4151,11 @@ def api_price_snapshot():
).fetchall() ).fetchall()
conn.close() conn.close()
try:
ensure_markets_loaded()
except Exception:
pass
symbol_set = set() symbol_set = set()
for r in key_rows: for r in key_rows:
symbol_set.add(r["symbol"]) symbol_set.add(r["symbol"])
@@ -4044,10 +4217,16 @@ def api_price_snapshot():
) )
except Exception: except Exception:
gate_metrics = "" gate_metrics = ""
px_disp = format_price_for_symbol(r["symbol"], price)
try:
price_num = float(px_disp) if px_disp != "-" else float(price)
except Exception:
price_num = float(price)
key_prices.append({ key_prices.append({
"id": r["id"], "id": r["id"],
"symbol": r["symbol"], "symbol": r["symbol"],
"price": round(price, 6), "price": price_num,
"price_display": px_disp,
"upper_diff": upper_diff, "upper_diff": upper_diff,
"upper_pct": upper_pct, "upper_pct": upper_pct,
"lower_diff": lower_diff, "lower_diff": lower_diff,
@@ -4075,11 +4254,10 @@ def api_price_snapshot():
payload = { payload = {
"id": r["id"], "id": r["id"],
"symbol": r["symbol"], "symbol": r["symbol"],
"price": round(price, 6), "float_pnl": round(pnl, 2),
"float_pnl": round(pnl, 6),
"float_pct": pnl_pct, "float_pct": pnl_pct,
"rr_ratio": rr_ratio, "rr_ratio": rr_ratio,
"plan_margin": round(margin, 4) if margin else None, "plan_margin": round(margin, 2) if margin else None,
"exchange_initial_margin": None, "exchange_initial_margin": None,
"exchange_notional": None, "exchange_notional": None,
"exchange_mark_price": None, "exchange_mark_price": None,
@@ -4093,12 +4271,24 @@ def api_price_snapshot():
if ex_metrics.get("mark_price") is not None: if ex_metrics.get("mark_price") is not None:
payload["exchange_mark_price"] = ex_metrics["mark_price"] payload["exchange_mark_price"] = ex_metrics["mark_price"]
if ex_metrics.get("unrealized_pnl") is not None: if ex_metrics.get("unrealized_pnl") is not None:
payload["float_pnl"] = round(float(ex_metrics["unrealized_pnl"]), 6) payload["float_pnl"] = round(float(ex_metrics["unrealized_pnl"]), 2)
payload["pnl_source"] = "exchange" payload["pnl_source"] = "exchange"
denom = ex_metrics.get("initial_margin") or margin denom = ex_metrics.get("initial_margin") or margin
payload["float_pct"] = ( payload["float_pct"] = (
round((payload["float_pnl"] / float(denom)) * 100, 4) if denom and float(denom) > 0 else pnl_pct round((payload["float_pnl"] / float(denom)) * 100, 4) if denom and float(denom) > 0 else pnl_pct
) )
px_for_fmt = float(price)
if ex_metrics and ex_metrics.get("mark_price") is not None:
try:
px_for_fmt = float(ex_metrics["mark_price"])
except (TypeError, ValueError):
pass
px_disp = format_price_for_symbol(r["symbol"], px_for_fmt)
try:
payload["price"] = float(px_disp) if px_disp != "-" else px_for_fmt
except Exception:
payload["price"] = px_for_fmt
payload["price_display"] = px_disp
order_prices.append(payload) order_prices.append(payload)
return jsonify({ return jsonify({
@@ -4149,7 +4339,7 @@ def api_order_defaults():
"exchange_symbol": exchange_symbol, "exchange_symbol": exchange_symbol,
"direction": direction, "direction": direction,
"leverage": leverage, "leverage": leverage,
"available_trading_usdt": round(available, 4) if available is not None else None "available_trading_usdt": round(available, 2) if available is not None else None
}) })
@@ -4162,7 +4352,7 @@ def order_focus():
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
_, trading_capital_live = get_exchange_capitals() _, trading_capital_live = get_exchange_capitals()
current_capital = round(trading_capital_live, 4) if trading_capital_live is not None else round(local_current_capital, 4) current_capital = round(trading_capital_live, 2) if trading_capital_live is not None else round(local_current_capital, 2)
raw_orders = conn.execute("SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC").fetchall() raw_orders = conn.execute("SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC").fetchall()
conn.close() conn.close()
orders = [enrich_order_item(row_to_dict(r), current_capital) for r in raw_orders] orders = [enrich_order_item(row_to_dict(r), current_capital) for r in raw_orders]
@@ -4201,7 +4391,7 @@ def api_order_kline():
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
_, trading_capital_live = get_exchange_capitals() _, trading_capital_live = get_exchange_capitals()
current_capital = round(trading_capital_live, 4) if trading_capital_live is not None else round(local_current_capital, 4) current_capital = round(trading_capital_live, 2) if trading_capital_live is not None else round(local_current_capital, 2)
row = conn.execute("SELECT * FROM order_monitors WHERE id=? AND status='active'", (order_id,)).fetchone() row = conn.execute("SELECT * FROM order_monitors WHERE id=? AND status='active'", (order_id,)).fetchone()
conn.close() conn.close()
if not row: if not row:
@@ -4236,24 +4426,29 @@ def api_order_kline():
float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0 float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0
float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0 float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0
sym = order_item["symbol"]
return jsonify({ return jsonify({
"ok": True, "ok": True,
"timeframe": timeframe, "timeframe": timeframe,
"limit": limit, "limit": limit,
"order": { "order": {
"id": order_item["id"], "id": order_item["id"],
"symbol": order_item["symbol"], "symbol": sym,
"direction": order_item.get("direction") or "long", "direction": order_item.get("direction") or "long",
"trigger_price": order_item.get("trigger_price"), "trigger_price": order_item.get("trigger_price"),
"stop_loss": order_item.get("stop_loss"), "stop_loss": order_item.get("stop_loss"),
"take_profit": order_item.get("take_profit"), "take_profit": order_item.get("take_profit"),
"trigger_price_display": format_price_for_symbol(sym, order_item.get("trigger_price")),
"stop_loss_display": format_price_for_symbol(sym, order_item.get("stop_loss")),
"take_profit_display": format_price_for_symbol(sym, order_item.get("take_profit")),
"margin_capital": order_item.get("margin_capital"), "margin_capital": order_item.get("margin_capital"),
"leverage": order_item.get("leverage"), "leverage": order_item.get("leverage"),
"position_ratio": order_item.get("position_ratio"), "position_ratio": order_item.get("position_ratio"),
"rr_ratio": order_item.get("rr_ratio"), "rr_ratio": order_item.get("rr_ratio"),
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)), "breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
"current_price": round(float(current_price), 8) if current_price else None, "current_price": round(float(current_price), 8) if current_price else None,
"float_pnl": round(float(float_pnl), 6), "current_price_display": format_price_for_symbol(sym, current_price) if current_price else None,
"float_pnl": round(float(float_pnl), 2),
"float_pct": float_pct, "float_pct": float_pct,
}, },
"candles": candles, "candles": candles,
@@ -4386,8 +4581,15 @@ def add_key():
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位") flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位")
return redirect("/") return redirect("/")
conn = get_db() conn = get_db()
ex_sym_key = normalize_exchange_symbol(symbol)
try:
ensure_markets_loaded()
except Exception:
pass
upper_px = round_price_to_exchange(ex_sym_key, float(d["upper"]))
lower_px = round_price_to_exchange(ex_sym_key, float(d["lower"]))
conn.execute("INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)", conn.execute("INSERT INTO key_monitors (symbol,monitor_type,direction,upper,lower) VALUES (?,?,?,?,?)",
(symbol, d["type"], d.get("direction", "long"), d["upper"], d["lower"])) (symbol, d["type"], d.get("direction", "long"), upper_px, lower_px))
conn.commit() conn.commit()
conn.close() conn.close()
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}") flash(f"添加成功({symbol} 日成交量排名 {rank}/{total}")
@@ -4414,14 +4616,19 @@ def add_order():
tgt_raw = parse_positive_float(d.get("tgt")) tgt_raw = parse_positive_float(d.get("tgt"))
except Exception: except Exception:
tp_raw = sl_raw = tgt_raw = None tp_raw = sl_raw = tgt_raw = None
ex_miss = normalize_exchange_symbol(symbol)
try:
ensure_markets_loaded()
except Exception:
pass
insert_trade_record( insert_trade_record(
conn, conn,
symbol=symbol, symbol=symbol,
monitor_type="下单监控", monitor_type="下单监控",
direction=direction if direction in ("long", "short") else "long", direction=direction if direction in ("long", "short") else "long",
trigger_price=tp_raw or 0, trigger_price=round_price_to_exchange(ex_miss, tp_raw) if tp_raw else 0,
stop_loss=sl_raw or 0, stop_loss=round_price_to_exchange(ex_miss, sl_raw) if sl_raw else 0,
take_profit=tgt_raw or 0, take_profit=round_price_to_exchange(ex_miss, tgt_raw) if tgt_raw else 0,
result="错过", result="错过",
miss_reason="持仓占用:一次只能持有一个仓位", miss_reason="持仓占用:一次只能持有一个仓位",
opened_at=app_now_str(), opened_at=app_now_str(),
@@ -4467,6 +4674,13 @@ def add_order():
conn.close() conn.close()
flash("获取交易所实时价格失败,请稍后重试") flash("获取交易所实时价格失败,请稍后重试")
return redirect("/") return redirect("/")
try:
ensure_markets_loaded()
except Exception:
pass
lp_r = round_price_to_exchange(exchange_symbol, live_price)
if lp_r is not None:
live_price = lp_r
sltp_mode = (d.get("sltp_mode") or "price").strip().lower() sltp_mode = (d.get("sltp_mode") or "price").strip().lower()
if sltp_mode not in ("price", "pct"): if sltp_mode not in ("price", "pct"):
sltp_mode = "price" sltp_mode = "price"
@@ -4500,6 +4714,12 @@ def add_order():
conn.close() conn.close()
flash("价格参数必须大于0") flash("价格参数必须大于0")
return redirect("/") return redirect("/")
sl_adj = round_price_to_exchange(exchange_symbol, stop_loss)
tp_adj = round_price_to_exchange(exchange_symbol, take_profit)
if sl_adj is not None:
stop_loss = sl_adj
if tp_adj is not None:
take_profit = tp_adj
risk_fraction = calc_risk_fraction(direction, live_price, stop_loss) risk_fraction = calc_risk_fraction(direction, live_price, stop_loss)
if risk_fraction is None: if risk_fraction is None:
conn.close() conn.close()
@@ -4517,7 +4737,7 @@ def add_order():
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4) max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
if margin_capital > max_margin: if margin_capital > max_margin:
conn.close() conn.close()
flash(f"保证金不足:交易账户可用约 {round(available_usdt,4)}U,当前最多建议 {max_margin}U") flash(f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U")
return redirect("/") return redirect("/")
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0 position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
try: try:
@@ -4533,6 +4753,10 @@ def add_order():
flash(friendly_exchange_error(e, available_usdt=available_usdt)) flash(friendly_exchange_error(e, available_usdt=available_usdt))
return redirect("/") return redirect("/")
trigger_price = round_price_to_exchange(exchange_symbol, trigger_price)
stop_loss = round_price_to_exchange(exchange_symbol, stop_loss)
take_profit = round_price_to_exchange(exchange_symbol, take_profit)
make_order_chart = d.get("order_chart", "").lower() in ("1", "true", "on", "yes") make_order_chart = d.get("order_chart", "").lower() in ("1", "true", "on", "yes")
opened_at_bj = app_now_str() opened_at_bj = app_now_str()
opened_at_ms = _to_ms_with_fallback(None, opened_at_bj) opened_at_ms = _to_ms_with_fallback(None, opened_at_bj)
@@ -4542,9 +4766,10 @@ def add_order():
breakeven_step_r = float(BREAKEVEN_STEP_R) if float(BREAKEVEN_STEP_R) > 0 else 1.0 breakeven_step_r = float(BREAKEVEN_STEP_R) if float(BREAKEVEN_STEP_R) > 0 else 1.0
risk_amount_final = calc_risk_amount_from_plan(direction, trigger_price, stop_loss, margin_capital, leverage) or risk_amount risk_amount_final = calc_risk_amount_from_plan(direction, trigger_price, stop_loss, margin_capital, leverage) or risk_amount
if direction == "short": if direction == "short":
breakeven_price = round(float(trigger_price) * (1 - breakeven_offset_pct / 100.0), 8) breakeven_raw = float(trigger_price) * (1 - breakeven_offset_pct / 100.0)
else: else:
breakeven_price = round(float(trigger_price) * (1 + breakeven_offset_pct / 100.0), 8) breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
breakeven_enabled = 1 if (d.get("breakeven_enabled") or "").strip() in ("1", "true", "on", "yes") else 0 breakeven_enabled = 1 if (d.get("breakeven_enabled") or "").strip() in ("1", "true", "on", "yes") else 0
conn.execute( conn.execute(
"INSERT INTO order_monitors (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) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "INSERT INTO order_monitors (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) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
@@ -4557,6 +4782,8 @@ def add_order():
) )
conn.commit() conn.commit()
new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0]) new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
try_persist_exchange_margin_for_order(conn, new_order_id, exchange_symbol, direction, order_leverage=leverage)
conn.commit()
opens_today_after = conn.execute( opens_today_after = conn.execute(
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?", "SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
(trading_day,), (trading_day,),
@@ -4632,9 +4859,9 @@ def add_order():
_, trading_capital_after = get_exchange_capitals(force=True) _, trading_capital_after = get_exchange_capitals(force=True)
account_base_display = ( account_base_display = (
round(float(trading_capital_after), 4) round(float(trading_capital_after), 2)
if trading_capital_after is not None if trading_capital_after is not None
else round(float(capital_base), 4) else round(float(capital_base), 2)
) )
account_name = (os.getenv("GATE_ACCOUNT_LABEL") or "gate实盘账户").strip() account_name = (os.getenv("GATE_ACCOUNT_LABEL") or "gate实盘账户").strip()
dir_text = "多头(long" if direction == "long" else "空头(short" dir_text = "多头(long" if direction == "long" else "空头(short"
@@ -4645,12 +4872,12 @@ def add_order():
) )
rr_show = planned_rr if planned_rr is not None else "-" rr_show = planned_rr if planned_rr is not None else "-"
try: try:
rr_show_fmt = round(float(planned_rr), 4) if planned_rr is not None else None rr_show_fmt = f"{float(planned_rr):.2f}" if planned_rr is not None else None
except (TypeError, ValueError): except (TypeError, ValueError):
rr_show_fmt = None rr_show_fmt = None
rr_line = f"RR {rr_show_fmt} : 1" if rr_show_fmt is not None else f"RR {rr_show} : 1" rr_line = f"RR {rr_show_fmt} : 1" if rr_show_fmt is not None else f"RR {rr_show} : 1"
ep_wx = format_price_for_symbol(symbol, trigger_price) ep_wx = format_price_for_symbol(symbol, trigger_price)
sl_wx = format_price_for_symbol(symbol, stop_loss) sl_wx = format_wechat_scalar_2dp(stop_loss)
tp_wx = format_price_for_symbol(symbol, take_profit) tp_wx = format_price_for_symbol(symbol, take_profit)
be_wx = format_price_for_symbol(symbol, breakeven_price) be_wx = format_price_for_symbol(symbol, breakeven_price)
style_zh = "Swing 波段" if trade_style == "swing" else "Trend 趋势" style_zh = "Swing 波段" if trade_style == "swing" else "Trend 趋势"
@@ -4660,13 +4887,13 @@ def add_order():
"🧾 订单基础信息", "🧾 订单基础信息",
f"🔖 交易所订单 ID{open_order_id}", f"🔖 交易所订单 ID{open_order_id}",
f"📈 交易风格:{style_zh}", f"📈 交易风格:{style_zh}",
f"⚠️ 单笔风控风险:{risk_percent}% ≈ {round(float(risk_amount_final), 4)} U", f"⚠️ 单笔风控风险:{risk_percent}% ≈ {round(float(risk_amount_final), 2)} U",
"📊 仓位配置详情", "📊 仓位配置详情",
f"账户基数:{account_base_display} USDT", f"账户基数:{account_base_display} USDT",
f"合约杠杆:{leverage}", f"合约杠杆:{leverage}",
f"名义仓位:{notional_value} USDT", f"名义仓位:{format_wechat_scalar_2dp(notional_value)} USDT",
f"仓位占比:{position_ratio}%", f"仓位占比:{position_ratio}%",
f"合约张数:{amount}", f"合约张数:{format_wechat_scalar_2dp(amount)}",
f"折算标的:{base_amount} {journal_coin_from_symbol(symbol)}", f"折算标的:{base_amount} {journal_coin_from_symbol(symbol)}",
"🎯 价位 & 盈亏比", "🎯 价位 & 盈亏比",
f"开仓成交价:{ep_wx}", f"开仓成交价:{ep_wx}",
@@ -4683,8 +4910,8 @@ def add_order():
send_wechat_msg("\n".join(wx_lines)) send_wechat_msg("\n".join(wx_lines))
flash_lines = [ flash_lines = [
f"实盘开单成功:风格 {trade_style};风险 {risk_percent}%≈{risk_amount_final}U;基数 {margin_capital}U,杠杆 {leverage}x,名义仓位 {notional_value}U,仓位占比 {position_ratio}%,合约张数 {amount}(折算标的 {base_amount})," f"实盘开单成功:风格 {trade_style};风险 {risk_percent}%≈{round(float(risk_amount_final), 2)}U;基数 {round(float(margin_capital), 2)}U,杠杆 {leverage}x,名义仓位 {format_wechat_scalar_2dp(notional_value)}U,仓位占比 {position_ratio}%,合约张数 {format_wechat_scalar_2dp(amount)}(折算标的 {base_amount}),"
f"计划RR {planned_rr if planned_rr is not None else '-'};已在交易所挂条件止盈/止损委托(非仓位绑定型)", f"计划RR {format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else '-'};已在交易所挂条件止盈/止损委托(非仓位绑定型)",
f"本交易日累计开仓:{opens_today_after}", f"本交易日累计开仓:{opens_today_after}",
] ]
if chart_url: if chart_url:
@@ -4694,7 +4921,7 @@ def add_order():
if opens_today_before < DAILY_OPEN_ALERT_THRESHOLD <= opens_today_after: if opens_today_before < DAILY_OPEN_ALERT_THRESHOLD <= opens_today_after:
advice = ai_short_advice( advice = ai_short_advice(
f"用户在北京时间交易日 {trading_day} 已累计开仓 {opens_today_after} 次(阈值 {DAILY_OPEN_ALERT_THRESHOLD})。" f"用户在北京时间交易日 {trading_day} 已累计开仓 {opens_today_after} 次(阈值 {DAILY_OPEN_ALERT_THRESHOLD})。"
f"最新一笔:{symbol} {direction},杠杆{leverage}x,基数{margin_capital}U。" f"最新一笔:{symbol} {direction},杠杆{leverage}x,基数{round(float(margin_capital), 2)}U。"
f"用户自述“上头了”。请给克制提醒。" f"用户自述“上头了”。请给克制提醒。"
) )
if advice: if advice:
@@ -4931,6 +5158,7 @@ def del_order(id):
cancel_gate_swap_trigger_orders(row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"])) cancel_gate_swap_trigger_orders(row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"]))
session_date = row["session_date"] or get_trading_day() session_date = row["session_date"] or get_trading_day()
session_capital = update_session_capital(conn, session_date, pnl_amount) session_capital = update_session_capital(conn, session_date, pnl_amount)
row_snap = conn.execute("SELECT * FROM order_monitors WHERE id=?", (id,)).fetchone() or row
insert_trade_record( insert_trade_record(
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
@@ -4940,7 +5168,7 @@ def del_order(id):
stop_loss=row["stop_loss"], stop_loss=row["stop_loss"],
initial_stop_loss=row["initial_stop_loss"] or row["stop_loss"], initial_stop_loss=row["initial_stop_loss"] or row["stop_loss"],
take_profit=row["take_profit"], take_profit=row["take_profit"],
margin_capital=row["margin_capital"], margin_capital=margin_capital_for_trade_record(row_snap),
leverage=row["leverage"], leverage=row["leverage"],
pnl_amount=pnl_amount, pnl_amount=pnl_amount,
hold_seconds=hold_seconds, hold_seconds=hold_seconds,
@@ -4985,6 +5213,7 @@ def del_order(id):
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt) hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
session_date = row["session_date"] or get_trading_day(closed_at_dt) session_date = row["session_date"] or get_trading_day(closed_at_dt)
update_session_capital(conn, session_date, pnl_amount) update_session_capital(conn, session_date, pnl_amount)
row_snap = conn.execute("SELECT * FROM order_monitors WHERE id=?", (id,)).fetchone() or row
insert_trade_record( insert_trade_record(
conn, conn,
symbol=row["symbol"], symbol=row["symbol"],
@@ -4994,7 +5223,7 @@ def del_order(id):
stop_loss=row["stop_loss"], stop_loss=row["stop_loss"],
initial_stop_loss=row["initial_stop_loss"] or row["stop_loss"], initial_stop_loss=row["initial_stop_loss"] or row["stop_loss"],
take_profit=row["take_profit"], take_profit=row["take_profit"],
margin_capital=row["margin_capital"], margin_capital=margin_capital_for_trade_record(row_snap),
leverage=row["leverage"], leverage=row["leverage"],
pnl_amount=pnl_amount, pnl_amount=pnl_amount,
hold_seconds=hold_seconds, hold_seconds=hold_seconds,
@@ -5025,15 +5254,28 @@ def del_order(id):
def add_miss(): def add_miss():
d = request.form d = request.form
direction = d.get("direction", "long") direction = d.get("direction", "long")
sym_in = normalize_symbol_input(d.get("symbol"))
ex_sym = normalize_exchange_symbol(sym_in)
try:
ensure_markets_loaded()
except Exception:
pass
try:
tp_px = round_price_to_exchange(ex_sym, float(d["tp"]))
sl_px = round_price_to_exchange(ex_sym, float(d["sl"]))
tgt_px = round_price_to_exchange(ex_sym, float(d["tgt"]))
except Exception:
flash("价格格式错误")
return redirect("/records")
conn = get_db() conn = get_db()
insert_trade_record( insert_trade_record(
conn, conn,
symbol=d["symbol"], symbol=sym_in,
monitor_type=d["type"], monitor_type=d["type"],
direction=direction, direction=direction,
trigger_price=d["tp"], trigger_price=tp_px,
stop_loss=d["sl"], stop_loss=sl_px,
take_profit=d["tgt"], take_profit=tgt_px,
result="错过", result="错过",
miss_reason=d["reason"], miss_reason=d["reason"],
opened_at=app_now_str(), opened_at=app_now_str(),
@@ -5379,11 +5621,20 @@ def api_trade_record_review_update():
reviewed_entry_reason_update = s or None reviewed_entry_reason_update = s or None
conn = get_db() conn = get_db()
row = conn.execute("SELECT risk_amount FROM trade_records WHERE id=?", (rec_id,)).fetchone() row = conn.execute("SELECT risk_amount, symbol FROM trade_records WHERE id=?", (rec_id,)).fetchone()
if not row: if not row:
conn.close() conn.close()
return jsonify({"ok": False, "msg": "记录不存在"}), 404 return jsonify({"ok": False, "msg": "记录不存在"}), 404
risk_amount = row["risk_amount"] risk_amount = row["risk_amount"]
ex_review = resolve_ccxt_price_symbol(row["symbol"])
try:
ensure_markets_loaded()
except Exception:
pass
if reviewed_stop_loss is not None:
reviewed_stop_loss = round_price_to_exchange(ex_review, reviewed_stop_loss)
if reviewed_take_profit is not None:
reviewed_take_profit = round_price_to_exchange(ex_review, reviewed_take_profit)
actual_rr = calc_actual_rr(reviewed_pnl_amount, risk_amount) actual_rr = calc_actual_rr(reviewed_pnl_amount, risk_amount)
base_params = [ base_params = [
reviewed_opened_at, reviewed_opened_at,
+58 -33
View File
@@ -130,14 +130,14 @@
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div> <div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div> <div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ s.net_pnl_u }}</div></div> <div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ signed_usdt_fmt(s.net_pnl_u) }}</div></div>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ s.loss_sum_u }}</div></div> <div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ usdt_fmt(s.loss_sum_u) }}</div></div>
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ s.max_single_loss }}{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ signed_usdt_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ s.max_single_profit }}{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ usdt_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ s.max_drawdown_u }}</div></div> <div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ usdt_fmt(s.max_drawdown_u) }}</div></div>
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div> <div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div> <div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}{{ s.worst_day_pnl }}U{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}{{ signed_usdt_fmt(s.worst_day_pnl) }}U{% else %}-{% endif %}</div></div>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
@@ -165,9 +165,9 @@
<div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div> <div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div>
<div class="stat-item"><div class="label">错过次数</div><div class="value">{{ miss_count }}</div></div> <div class="stat-item"><div class="label">错过次数</div><div class="value">{{ miss_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div> <div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div>
<div class="stat-item"><div class="label">资金账户(USDT)</div><div class="value" id="total-capital">{% if funding_usdt is not none %}{{ funding_usdt }}U{% else %}—{% endif %}</div></div> <div class="stat-item"><div class="label">资金账户(USDT)</div><div class="value" id="total-capital">{% if funding_usdt is not none %}{{ usdt_fmt(funding_usdt) }}U{% else %}—{% endif %}</div></div>
<div class="stat-item"><div class="label">交易日</div><div class="value">{{ trading_day }}</div></div> <div class="stat-item"><div class="label">交易日</div><div class="value">{{ trading_day }}</div></div>
<div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ current_capital }}U</div></div> <div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ usdt_fmt(current_capital) }}U</div></div>
</div> </div>
<div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div> <div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div>
@@ -202,7 +202,7 @@
<div class="list-item" id="key-row-{{ k.id }}"> <div class="list-item" id="key-row-{{ k.id }}">
<div><strong>{{ k.symbol }}</strong> | {{ k.monitor_type }} | <span class="badge direction">{{ '做多' if k.direction == 'long' else '做空' }}</span></div> <div><strong>{{ k.symbol }}</strong> | {{ k.monitor_type }} | <span class="badge direction">{{ '做多' if k.direction == 'long' else '做空' }}</span></div>
<div> <div>
上:{{ k.upper }} 下:{{ k.lower }} 上:{{ price_fmt(k.symbol, k.upper) }} 下:{{ price_fmt(k.symbol, k.lower) }}
| 已提醒:{{ k.notification_count or 0 }}/{{ k.max_notify or 3 }} | 已提醒:{{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
| 现价:<span id="key-price-{{ k.id }}">-</span> | 现价:<span id="key-price-{{ k.id }}">-</span>
| 距上沿:<span id="key-up-diff-{{ k.id }}">-</span> | 距上沿:<span id="key-up-diff-{{ k.id }}">-</span>
@@ -224,7 +224,7 @@
<strong>{{ h.symbol }}</strong> | {{ h.monitor_type }} | {{ '做多' if h.direction == 'long' else '做空' }} | {{ h.close_reason }} <strong>{{ h.symbol }}</strong> | {{ h.monitor_type }} | {{ '做多' if h.direction == 'long' else '做空' }} | {{ h.close_reason }}
<button type="button" class="table-del" style="margin-left:8px" onclick="deleteKeyHistory({{ h.id }})">删除</button> <button type="button" class="table-del" style="margin-left:8px" onclick="deleteKeyHistory({{ h.id }})">删除</button>
</div> </div>
<div>上:{{ h.upper }} 下:{{ h.lower }} | 提醒次数:{{ h.notification_count }} | {{ (h.closed_at or '-')[:16] }}</div> <div>上:{{ price_fmt(h.symbol, h.upper) }} 下:{{ price_fmt(h.symbol, h.lower) }} | 提醒次数:{{ h.notification_count }} | {{ (h.closed_at or '-')[:16] }}</div>
{% if h.last_alert_message %}<div style="font-size:.78rem;color:#aab;margin-top:4px;white-space:pre-wrap">{{ h.last_alert_message[:200] }}{% if h.last_alert_message|length > 200 %}…{% endif %}</div>{% endif %} {% if h.last_alert_message %}<div style="font-size:.78rem;color:#aab;margin-top:4px;white-space:pre-wrap">{{ h.last_alert_message[:200] }}{% if h.last_alert_message|length > 200 %}…{% endif %}</div>{% endif %}
</div> </div>
{% else %} {% else %}
@@ -252,7 +252,7 @@
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}% 以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div> </div>
<div class="rule-tip"> <div class="rule-tip">
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ auto_transfer_amount }}U,来自 {{ auto_transfer_from }} 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ usdt_fmt(auto_transfer_amount) }}U,来自 {{ auto_transfer_from }}
</div> </div>
<form action="/manual_transfer" method="post" class="form-row"> <form action="/manual_transfer" method="post" class="form-row">
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required> <input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
@@ -300,14 +300,14 @@
<div class="list-item"> <div class="list-item">
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div> <div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
<div> <div>
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ o.risk_amount or '-' }}U 风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{% if o.risk_amount is not none %}{{ usdt_fmt(o.risk_amount) }}{% else %}-{% endif %}U
| {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ o.breakeven_price or '-' }}{% else %}移动保本:关{% endif %} | {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
<br> <br>
成交:{{ o.trigger_price }} 止损:{{ o.stop_loss }} 止盈:{{ o.take_profit }} 成交:{{ price_fmt(o.symbol, o.trigger_price) }} 止损:{{ price_fmt(o.symbol, o.stop_loss) }} 止盈:{{ price_fmt(o.symbol, o.take_profit) }}
| 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span> | 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span>
| 现价:<span id="order-price-{{ o.id }}">-</span> | 现价:<span id="order-price-{{ o.id }}">-</span>
| 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span> | 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span>
| 计划基数:{{ o.margin_capital }}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span> | 计划基数:{% if o.margin_capital is not none %}{{ usdt_fmt(o.margin_capital) }}{% else %}-{% endif %}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span>
| 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}% | 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}%
</div> </div>
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a> <a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
@@ -335,18 +335,18 @@
<td>{{ r.symbol }}</td> <td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}</td> <td>{{ r.monitor_type }}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td> <td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ r.trigger_price }}</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 %} {% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %} {% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td> <td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td> <td>{{ price_fmt(r.symbol, tp_show) }}</td>
<td>{{ r.margin_capital or '-' }}</td> <td>{% if r.margin_capital is not none %}{{ usdt_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td> <td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td> <td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td> <td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_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 %} {% 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 '') }}">{{ 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></td>
<td> <td>
{% set effective_result = r.effective_result %} {% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span> {% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
@@ -602,7 +602,7 @@ function openJournalDetail(id){
`开仓时间:${o.open_datetime || "-"}`, `开仓时间:${o.open_datetime || "-"}`,
`平仓时间:${o.close_datetime || "-"}`, `平仓时间:${o.close_datetime || "-"}`,
`持仓时长:${o.hold_duration || "-"}`, `持仓时长:${o.hold_duration || "-"}`,
`盈亏:${o.pnl || "-"}U`, `盈亏:${formatJournalPnlUi(o.pnl)}U`,
`开仓类型:${o.entry_reason || "无"}`, `开仓类型:${o.entry_reason || "无"}`,
`平仓/离场:${formatJournalExitOneLine(o)}`, `平仓/离场:${formatJournalExitOneLine(o)}`,
`预期RR${o.expect_rr || "-"}`, `预期RR${o.expect_rr || "-"}`,
@@ -685,7 +685,7 @@ function editTradeRecordReview(t){
if(stopLoss === null) return; if(stopLoss === null) return;
const takeProfit = prompt("止盈价格(核对后用于统计)", String(t.take_profit ?? "")); const takeProfit = prompt("止盈价格(核对后用于统计)", String(t.take_profit ?? ""));
if(takeProfit === null) return; if(takeProfit === null) return;
const pnl = prompt("最终盈亏(可手工核对后填写)", String(t.pnl_amount ?? "")); const pnl = prompt("最终盈亏(可手工核对后填写)", (t.pnl_amount === null || typeof t.pnl_amount === "undefined") ? "" : (Number.isFinite(Number(t.pnl_amount)) ? Number(t.pnl_amount).toFixed(2) : String(t.pnl_amount)));
if(pnl === null) return; if(pnl === null) return;
const result = prompt("结果(止盈/止损/保本止盈/移动止盈/手动平仓)", String(t.result || "")); const result = prompt("结果(止盈/止损/保本止盈/移动止盈/手动平仓)", String(t.result || ""));
if(result === null) return; if(result === null) return;
@@ -751,7 +751,7 @@ function loadJournals(){
journalCache[o.id] = o; journalCache[o.id] = o;
const moodTags = (o.mood_issues || []).join(",") || "无"; const moodTags = (o.mood_issues || []).join(",") || "无";
html += `<div class="entry"> html += `<div class="entry">
<div><strong>${o.coin||"-"} ${o.tf||"-"}</strong> | 盈亏:${o.pnl||"-"}U</div> <div><strong>${o.coin||"-"} ${o.tf||"-"}</strong> | 盈亏:${formatJournalPnlUi(o.pnl)}U</div>
<div>开:${o.open_datetime||"-"} 平:${o.close_datetime||"-"} 持仓:${o.hold_duration||"-"}</div> <div>开:${o.open_datetime||"-"} 平:${o.close_datetime||"-"} 持仓:${o.hold_duration||"-"}</div>
<div>心态标签:${moodTags}</div> <div>心态标签:${moodTags}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px"> <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px">
@@ -908,7 +908,7 @@ function fillJournalFromTrade(t){
setJournalField("close_datetime", toDatetimeLocalFromBeijing(t.closed_at)); setJournalField("close_datetime", toDatetimeLocalFromBeijing(t.closed_at));
setJournalField("coin", coinFromSymbol(t.symbol)); setJournalField("coin", coinFromSymbol(t.symbol));
setJournalField("tf", "5m"); setJournalField("tf", "5m");
setJournalField("pnl", (t.pnl_amount === null || typeof t.pnl_amount === "undefined") ? "" : String(t.pnl_amount)); setJournalField("pnl", (t.pnl_amount === null || typeof t.pnl_amount === "undefined") ? "" : (Number.isFinite(Number(t.pnl_amount)) ? Number(t.pnl_amount).toFixed(2) : String(t.pnl_amount)));
const rr = calcExpectedRrFromTrade(t); const rr = calcExpectedRrFromTrade(t);
setJournalField("expect_rr", rr); setJournalField("expect_rr", rr);
let realRr = rr; let realRr = rr;
@@ -919,7 +919,7 @@ function fillJournalFromTrade(t){
} }
setJournalField("real_rr", realRr); setJournalField("real_rr", realRr);
const riskHint = document.getElementById("risk-amount-hint"); const riskHint = document.getElementById("risk-amount-hint");
if(riskHint){ riskHint.value = (Number.isFinite(riskAmount) && riskAmount > 0) ? String(riskAmount) : ""; } if(riskHint){ riskHint.value = (Number.isFinite(riskAmount) && riskAmount > 0) ? riskAmount.toFixed(2) : ""; }
const entryHint = document.getElementById("entry-price-hint"); const entryHint = document.getElementById("entry-price-hint");
if(entryHint){ entryHint.value = t.trigger_price || ""; } if(entryHint){ entryHint.value = t.trigger_price || ""; }
const stopHint = document.getElementById("stop-loss-hint"); const stopHint = document.getElementById("stop-loss-hint");
@@ -1098,6 +1098,30 @@ function formatSigned(v, digits=4){
return `${sign}${n.toFixed(digits)}`; return `${sign}${n.toFixed(digits)}`;
} }
function formatUsdt2(v){
if(v === null || typeof v === "undefined" || v === "") return "-";
const n = Number(v);
if(Number.isNaN(n)) return "-";
return n.toFixed(2);
}
function formatSignedUsdt2(v){
if(v === null || typeof v === "undefined" || v === "" || Number.isNaN(Number(v))) return "-";
const n = Number(v);
if(n === 0) return "0.00";
const sign = n > 0 ? "+" : "";
return `${sign}${n.toFixed(2)}`;
}
function formatJournalPnlUi(v){
if(v === null || typeof v === "undefined" || v === "") return "-";
const raw = String(v).trim();
if(!raw) return "-";
const n = Number(raw.replace(/,/g, ""));
if(Number.isFinite(n)) return formatSignedUsdt2(n);
return raw;
}
function paintPriceTrend(el, key, value){ function paintPriceTrend(el, key, value){
if(!el) return; if(!el) return;
const prev = lastPriceMap[key]; const prev = lastPriceMap[key];
@@ -1121,7 +1145,7 @@ function refreshPriceSnapshot(){
(data.key_prices || []).forEach(k=>{ (data.key_prices || []).forEach(k=>{
const pEl = document.getElementById(`key-price-${k.id}`); const pEl = document.getElementById(`key-price-${k.id}`);
if(pEl){ if(pEl){
pEl.innerText = Number(k.price).toFixed(6); pEl.innerText = (k.price_display && k.price_display !== "-") ? k.price_display : Number(k.price).toFixed(6);
paintPriceTrend(pEl, `k-${k.id}`, Number(k.price)); paintPriceTrend(pEl, `k-${k.id}`, Number(k.price));
} }
const upEl = document.getElementById(`key-up-diff-${k.id}`); const upEl = document.getElementById(`key-up-diff-${k.id}`);
@@ -1145,18 +1169,19 @@ function refreshPriceSnapshot(){
(data.order_prices || []).forEach(o=>{ (data.order_prices || []).forEach(o=>{
const pEl = document.getElementById(`order-price-${o.id}`); const pEl = document.getElementById(`order-price-${o.id}`);
if(pEl){ if(pEl){
const pxd = (o.price_display && o.price_display !== "-") ? o.price_display : null;
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })(); const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price); const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
const decimals = hasMark ? 8 : 6; const decimals = hasMark ? 8 : 6;
pEl.innerText = px.toFixed(decimals); pEl.innerText = pxd !== null ? pxd : px.toFixed(decimals);
paintPriceTrend(pEl, `o-${o.id}`, px); paintPriceTrend(pEl, `o-${o.id}`, pxd !== null ? Number(pxd) : px);
} }
const exM = document.getElementById(`order-ex-margin-${o.id}`); const exM = document.getElementById(`order-ex-margin-${o.id}`);
if(exM){ if(exM){
const mv = o.exchange_initial_margin; const mv = o.exchange_initial_margin;
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv); const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
if(!Number.isNaN(mn)){ if(!Number.isNaN(mn)){
exM.innerText = `${mn.toFixed(4)}U`; exM.innerText = `${formatUsdt2(mn)}U`;
} else { } else {
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null; const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
exM.innerText = (prc === 0) ? "无仓数据" : "-"; exM.innerText = (prc === 0) ? "无仓数据" : "-";
@@ -1164,7 +1189,7 @@ function refreshPriceSnapshot(){
} }
const pnlEl = document.getElementById(`order-pnl-${o.id}`); const pnlEl = document.getElementById(`order-pnl-${o.id}`);
if(pnlEl){ if(pnlEl){
pnlEl.innerText = `${formatSigned(o.float_pnl, 4)}U (${formatSigned(o.float_pct, 2)}%)`; pnlEl.innerText = `${formatSignedUsdt2(o.float_pnl)}U (${formatSigned(o.float_pct, 2)}%)`;
pnlEl.classList.remove("price-up","price-down","price-flat"); pnlEl.classList.remove("price-up","price-down","price-flat");
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up"); if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down"); else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
@@ -1198,7 +1223,7 @@ function refreshOrderDefaults(){
const fullEl = document.getElementById("use-full-margin"); const fullEl = document.getElementById("use-full-margin");
const marginEl = document.getElementById("order-margin"); const marginEl = document.getElementById("order-margin");
if(fullEl && marginEl && fullEl.checked){ if(fullEl && marginEl && fullEl.checked){
const m = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(4); const m = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(2);
marginEl.value = m; marginEl.value = m;
} }
} }
@@ -1209,18 +1234,18 @@ function refreshAccountSnapshot(){
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{ fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
if (typeof data.funding_usdt !== "undefined") { if (typeof data.funding_usdt !== "undefined") {
const el = document.getElementById("total-capital"); const el = document.getElementById("total-capital");
if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${data.funding_usdt}U`; if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${formatUsdt2(data.funding_usdt)}U`;
} }
if (typeof data.current_capital !== "undefined") { if (typeof data.current_capital !== "undefined") {
const el = document.getElementById("current-capital"); const el = document.getElementById("current-capital");
if(el) el.innerText = `${data.current_capital}U`; if(el) el.innerText = `${formatUsdt2(data.current_capital)}U`;
} }
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) { if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
latestAvailableUsdt = Number(data.available_trading_usdt); latestAvailableUsdt = Number(data.available_trading_usdt);
} }
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00"; const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00";
const tip = document.getElementById("order-rule-tip"); const tip = document.getElementById("order-rule-tip");
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt}U` : ""; const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约 ${formatUsdt2(latestAvailableUsdt)}U` : "";
if(tip){ if(tip){
tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`; tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`;
} }
@@ -1236,7 +1261,7 @@ if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){ fullMarginEl.addEventListener("change", function(){
const marginEl = document.getElementById("order-margin"); const marginEl = document.getElementById("order-margin");
if(marginEl && this.checked && latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)){ if(marginEl && this.checked && latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)){
marginEl.value = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(4); marginEl.value = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(2);
} }
}); });
} }
@@ -140,13 +140,13 @@ function addLine(price, title, color){
function paintOrder(order){ function paintOrder(order){
document.getElementById("m-symbol").innerText = order.symbol || "-"; document.getElementById("m-symbol").innerText = order.symbol || "-";
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多"; document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8); document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8); document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8); document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`; document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
document.getElementById("m-price").innerText = fmt(order.current_price, 8); document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
const pnlEl = document.getElementById("m-pnl"); const pnlEl = document.getElementById("m-pnl");
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`; pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff"); pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
} }
@@ -142,15 +142,15 @@ function addLine(price, title, color){
function paintOrder(order){ function paintOrder(order){
document.getElementById("m-symbol").innerText = order.symbol || "-"; document.getElementById("m-symbol").innerText = order.symbol || "-";
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多"; document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8); document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8); document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8); document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`; document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
document.getElementById("m-breakeven").innerText = document.getElementById("m-breakeven").innerText =
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启"; (order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
document.getElementById("m-price").innerText = fmt(order.current_price, 8); document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
const pnlEl = document.getElementById("m-pnl"); const pnlEl = document.getElementById("m-pnl");
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`; pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff"); pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
} }
+3 -2
View File
@@ -37,8 +37,9 @@ BTC_LEVERAGE=10
ALT_LEVERAGE=5 ALT_LEVERAGE=5
# 交易日重置小时(北京时间) # 交易日重置小时(北京时间)
TRADING_DAY_RESET_HOUR=8 TRADING_DAY_RESET_HOUR=8
# 整点前禁止新开仓:true=启用(默认),false=关闭(仍可保留 8 点作为交易日划分) # Gate 平仓历史:同步「趋势回调」交易记录与交易所已实现盈亏(北京日期 00:00 起,与 APP_TIMEZONE 一致);留空则从近 90 天拉取
TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true # EXCHANGE_POSITION_SYNC_FROM_BJ=2026-05-14
# EXCHANGE_POSITION_HISTORY_LIMIT=200
# 是否开启 Gate 实盘下单(false=只做本地流程,true=真实下单) # 是否开启 Gate 实盘下单(false=只做本地流程,true=真实下单)
LIVE_TRADING_ENABLED=true LIVE_TRADING_ENABLED=true
+514 -15
View File
@@ -93,6 +93,11 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED = os.getenv(
"TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "true" "TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "true"
).lower() in ("1", "true", "yes", "on") ).lower() in ("1", "true", "yes", "on")
APP_TIMEZONE = os.getenv("APP_TIMEZONE", "Asia/Shanghai") APP_TIMEZONE = os.getenv("APP_TIMEZONE", "Asia/Shanghai")
# 交易所「平仓历史」同步:自北京日期 00:00 起(与 APP_TIMEZONE 一致);空则取最近 90 天
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_POSITION_HISTORY_SYNC_AT = 0.0
def _resolve_app_tz(): def _resolve_app_tz():
@@ -1182,6 +1187,26 @@ def init_db():
try: try:
c.execute("ALTER TABLE trade_records ADD COLUMN reviewed_entry_reason TEXT") c.execute("ALTER TABLE trade_records ADD COLUMN reviewed_entry_reason TEXT")
except: pass except: pass
try:
c.execute("ALTER TABLE trade_records ADD COLUMN trend_plan_id INTEGER")
except Exception:
pass
try:
c.execute("ALTER TABLE trade_records ADD COLUMN exchange_realized_pnl REAL")
except Exception:
pass
try:
c.execute("ALTER TABLE trade_records ADD COLUMN exchange_opened_at TEXT")
except Exception:
pass
try:
c.execute("ALTER TABLE trade_records ADD COLUMN exchange_closed_at TEXT")
except Exception:
pass
try:
c.execute("ALTER TABLE trade_records ADD COLUMN exchange_sync_key TEXT")
except Exception:
pass
try: try:
c.execute("ALTER TABLE journal_entries ADD COLUMN mood_ai_score INTEGER") c.execute("ALTER TABLE journal_entries ADD COLUMN mood_ai_score INTEGER")
except: pass except: pass
@@ -1286,6 +1311,36 @@ def init_db():
)""" )"""
) )
c.execute(
"""CREATE TABLE IF NOT EXISTS trend_pullback_preview_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
preview_id TEXT NOT NULL UNIQUE,
symbol TEXT NOT NULL,
exchange_symbol TEXT NOT NULL,
direction TEXT NOT NULL,
leverage INTEGER NOT NULL,
stop_loss REAL NOT NULL,
add_upper REAL NOT NULL,
take_profit REAL NOT NULL,
risk_percent REAL NOT NULL,
snapshot_available_usdt REAL NOT NULL,
snapshot_at TEXT,
live_price_ref REAL,
plan_margin_capital REAL,
target_order_amount REAL,
first_order_amount REAL,
remainder_total REAL,
dca_legs INTEGER,
per_leg_amount REAL,
grid_prices_json TEXT,
leg_amounts_json TEXT,
expires_at_ms INTEGER NOT NULL,
preview_created_at TEXT,
outcome TEXT DEFAULT 'open',
executed_plan_id INTEGER
)"""
)
conn.commit() conn.commit()
conn.close() conn.close()
@@ -1710,9 +1765,57 @@ def to_effective_trade_dict(row):
item["effective_hold_seconds"] = get_effective_trade_field(row, "reviewed_hold_seconds", "hold_seconds", item.get("hold_seconds")) 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")) 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 "" item["effective_entry_reason"] = (str(er_eff).strip() if er_eff is not None else "") or ""
mt = (item.get("monitor_type") or "").strip()
ex_pnl = item.get("exchange_realized_pnl")
ex_open = item.get("exchange_opened_at")
ex_close = item.get("exchange_closed_at")
if mt == MONITOR_TYPE_TREND and ex_pnl is not None and str(ex_pnl).strip() != "":
try:
item["display_pnl_amount"] = float(ex_pnl)
except (TypeError, ValueError):
item["display_pnl_amount"] = float(item.get("effective_pnl_amount") or 0)
item["display_pnl_source"] = "exchange"
eo = (str(ex_open).strip() if ex_open else "") or item.get("effective_opened_at") or ""
ec = (str(ex_close).strip() if ex_close else "") or item.get("effective_closed_at") or ""
item["display_opened_at"] = eo[:16] if eo else "-"
item["display_closed_at"] = ec[:16] if ec else "-"
else:
try:
item["display_pnl_amount"] = float(item.get("effective_pnl_amount") or 0)
except (TypeError, ValueError):
item["display_pnl_amount"] = 0.0
item["display_pnl_source"] = "local"
eo = item.get("effective_opened_at") or ""
ec = item.get("effective_closed_at") or ""
item["display_opened_at"] = (eo[:16] if eo else "-")
item["display_closed_at"] = (ec[:16] if ec else "-")
return item return item
def format_money_usdt(value):
"""资金类展示:固定两位小数(USDT)。"""
if value is None or value == "":
return ""
try:
return f"{round(float(value), 2):.2f}"
except (TypeError, ValueError):
return ""
def _exchange_unified_symbol_for_format(symbol_str):
if not symbol_str:
return None
s = str(symbol_str).strip()
if not s:
return None
try:
if ":" in s or "/" in s:
return normalize_exchange_symbol(s)
return normalize_exchange_symbol(f"{s}/USDT")
except Exception:
return None
def format_price_for_symbol(symbol, value): def format_price_for_symbol(symbol, value):
if value in (None, ""): if value in (None, ""):
return "-" return "-"
@@ -1722,8 +1825,14 @@ def format_price_for_symbol(symbol, value):
return str(value) return str(value)
if v == 0: if v == 0:
return "0" return "0"
sym = _exchange_unified_symbol_for_format(symbol)
if sym and exchange_private_api_configured():
try:
ensure_markets_loaded()
return str(exchange.price_to_precision(sym, v))
except Exception:
pass
av = abs(v) av = abs(v)
# 根据币价量级动态精度:低价币保留更多小数,高价币减少噪音位数
if av >= 10000: if av >= 10000:
d = 2 d = 2
elif av >= 100: elif av >= 100:
@@ -1740,6 +1849,70 @@ def format_price_for_symbol(symbol, value):
return text.rstrip("0").rstrip(".") if "." in text else text return text.rstrip("0").rstrip(".") if "." in text else text
def format_amount_for_symbol(symbol, value):
"""合约张数等:尽量与交易所 amount 精度一致。"""
if value in (None, ""):
return "-"
try:
v = float(value)
except Exception:
return str(value)
sym = _exchange_unified_symbol_for_format(symbol)
if sym and exchange_private_api_configured():
try:
ensure_markets_loaded()
return str(exchange.amount_to_precision(sym, v))
except Exception:
pass
text = f"{v:.8f}"
return text.rstrip("0").rstrip(".") if "." in text else text
def insert_trend_preview_snapshot(conn, preview_id, created, exp_ms, pl):
"""生成预览成功后归档一条快照(与 trend_pullback_previews 同参)。"""
conn.execute(
"""INSERT INTO trend_pullback_preview_snapshots (
preview_id,symbol,exchange_symbol,direction,leverage,stop_loss,add_upper,take_profit,risk_percent,
snapshot_available_usdt,snapshot_at,live_price_ref,plan_margin_capital,target_order_amount,first_order_amount,remainder_total,
dca_legs,per_leg_amount,grid_prices_json,leg_amounts_json,expires_at_ms,preview_created_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
preview_id,
pl["symbol"],
pl["exchange_symbol"],
pl["direction"],
pl["leverage"],
pl["stop_loss"],
pl["add_upper"],
pl["take_profit"],
pl["risk_percent"],
pl["snapshot_available_usdt"],
pl["snapshot_at"],
pl["live_price_ref"],
pl["plan_margin_capital"],
pl["target_order_amount"],
pl["first_order_amount"],
pl["remainder_total"],
pl["dca_legs"],
pl["per_leg_amount"],
pl["grid_prices_json"],
pl["leg_amounts_json"],
exp_ms,
created,
),
)
def preview_snapshot_outcome_label(outcome):
o = (outcome or "").strip().lower()
return {
"open": "待确认",
"executed": "已执行",
"cancelled": "已取消",
"expired": "已过期",
}.get(o, outcome or "-")
def format_hold_minutes(minutes): def format_hold_minutes(minutes):
if not minutes: if not minutes:
return "0分钟" return "0分钟"
@@ -1887,6 +2060,7 @@ def insert_trade_record(
closed_at=None, closed_at=None,
closed_at_ms=None, closed_at_ms=None,
exchange_trade_id=None, exchange_trade_id=None,
trend_plan_id=None,
): ):
hold_minutes = calc_hold_minutes(hold_seconds) hold_minutes = calc_hold_minutes(hold_seconds)
open_ts = opened_at or app_now_str() open_ts = opened_at or app_now_str()
@@ -1894,12 +2068,12 @@ def insert_trade_record(
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts) open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts) close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
conn.execute( 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,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,trend_plan_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
( (
symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
margin_capital, leverage, pnl_amount, hold_seconds, margin_capital, leverage, pnl_amount, hold_seconds,
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes, 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 open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id, trend_plan_id
) )
) )
@@ -2395,6 +2569,15 @@ def precheck_trend_pullback_start(conn):
def _trend_cleanup_stale_previews(conn): def _trend_cleanup_stale_previews(conn):
ms = int(time.time() * 1000) ms = int(time.time() * 1000)
stale = conn.execute("SELECT id FROM trend_pullback_previews WHERE expires_at_ms < ?", (ms,)).fetchall()
for row in stale:
try:
conn.execute(
"UPDATE trend_pullback_preview_snapshots SET outcome='expired' WHERE preview_id=? AND outcome='open'",
(row["id"],),
)
except Exception:
pass
conn.execute("DELETE FROM trend_pullback_previews WHERE expires_at_ms < ?", (ms,)) conn.execute("DELETE FROM trend_pullback_previews WHERE expires_at_ms < ?", (ms,))
@@ -3030,6 +3213,236 @@ def get_live_position_exchange_metrics(exchange_symbol, direction):
return parse_ccxt_position_metrics(p) return parse_ccxt_position_metrics(p)
def _unified_symbol_for_match(symbol_str):
"""统一 BTC/USDT:USDT 与 BTC/USDT 便于与 trade_records.symbol 比对。"""
x = (symbol_str or "").strip().upper()
if ":" in x:
x = x.split(":")[0]
return x
def exchange_position_sync_since_ms():
"""Gate fetch_positions_history 的 since(毫秒,含当日 0 点)。"""
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 _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 > 1e10:
return int(v)
return int(v * 1000.0)
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_trend_trade_records_from_exchange(conn):
global _LAST_POSITION_HISTORY_SYNC_AT
if not exchange_private_api_configured():
return
now = time.time()
if now - _LAST_POSITION_HISTORY_SYNC_AT < 25.0:
return
try:
hist = fetch_gate_positions_close_history()
except Exception:
return
if not hist:
_LAST_POSITION_HISTORY_SYNC_AT = now
return
candidates = conn.execute(
"""
SELECT id, symbol, direction, closed_at, opened_at, trend_plan_id, exchange_sync_key
FROM trade_records
WHERE monitor_type = ? AND (exchange_sync_key IS NULL OR TRIM(exchange_sync_key) = '')
ORDER BY id DESC
LIMIT 120
""",
(MONITOR_TYPE_TREND,),
).fetchall()
if not candidates:
_LAST_POSITION_HISTORY_SYNC_AT = now
return
used = set()
for tr in candidates:
tid = None
if "trend_plan_id" in tr.keys() and tr["trend_plan_id"]:
try:
tid = int(tr["trend_plan_id"])
except (TypeError, ValueError):
tid = None
plan_open_ms = None
if tid:
prow = conn.execute("SELECT opened_at FROM trend_pullback_plans WHERE id=?", (tid,)).fetchone()
if prow and prow["opened_at"]:
plan_open_ms = opened_at_str_to_ms(prow["opened_at"])
close_ms_trade = opened_at_str_to_ms(tr["closed_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 plan_open_ms is not None:
if cm < plan_open_ms - 15 * 60 * 1000:
continue
if cm > plan_open_ms + 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_POSITION_HISTORY_SYNC_AT = now
conn.commit()
def trend_plan_history_status_label(status):
s = (status or "").strip().lower()
return {
"stopped_tp": "止盈结束",
"stopped_sl": "止损结束",
"stopped_manual": "手动结束",
}.get(s, status or "-")
def enrich_active_trend_plan_row(row):
d = row_to_dict(row)
ex_sym = d.get("exchange_symbol") or normalize_exchange_symbol(d.get("symbol") or "")
direction = (d.get("direction") or "long").lower()
m = get_live_position_exchange_metrics(ex_sym, direction)
if m and m.get("unrealized_pnl") is not None:
d["floating_pnl"] = float(m["unrealized_pnl"])
else:
d["floating_pnl"] = None
if m and m.get("mark_price") is not None:
d["floating_mark"] = float(m["mark_price"])
else:
d["floating_mark"] = None
return d
def opened_at_str_to_ms(opened_at_str): def opened_at_str_to_ms(opened_at_str):
if not opened_at_str: if not opened_at_str:
return None return None
@@ -3795,6 +4208,7 @@ def _trend_finalize_plan(conn, row, result_label, exit_price, closed_at=None):
result=res, result=res,
opened_at=opened_at, opened_at=opened_at,
closed_at=closed_at, closed_at=closed_at,
trend_plan_id=int(row["id"]),
) )
st = "stopped_tp" if result_label == "止盈" else ("stopped_sl" if result_label == "止损" else "stopped_manual") st = "stopped_tp" if result_label == "止盈" else ("stopped_sl" if result_label == "止损" else "stopped_manual")
conn.execute( conn.execute(
@@ -4417,9 +4831,9 @@ def render_main_page(page="trade"):
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
funding_capital, trading_capital = get_exchange_capitals() funding_capital, trading_capital = get_exchange_capitals()
# 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。 # 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。
funding_usdt = round(funding_capital, 4) if funding_capital is not None else None funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4) current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
recommended_capital = get_recommended_capital(current_capital) recommended_capital = round(get_recommended_capital(current_capital), 2)
key_list = conn.execute("SELECT * FROM key_monitors").fetchall() key_list = conn.execute("SELECT * FROM key_monitors").fetchall()
key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall() key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall()
stats_bundle = compute_stats_bundle(conn, trading_day, now) stats_bundle = compute_stats_bundle(conn, trading_day, now)
@@ -4427,6 +4841,11 @@ def render_main_page(page="trade"):
order_list = [] order_list = []
for o in raw_order_list: for o in raw_order_list:
order_list.append(enrich_order_item(row_to_dict(o), current_capital)) order_list.append(enrich_order_item(row_to_dict(o), current_capital))
if page in ("trade", "records", "plan_history"):
try:
sync_trend_trade_records_from_exchange(conn)
except Exception:
pass
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall() raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall()
records = [to_effective_trade_dict(r) for r in raw_records] records = [to_effective_trade_dict(r) for r in raw_records]
total = len(records) total = len(records)
@@ -4443,9 +4862,27 @@ def render_main_page(page="trade"):
trend_active = conn.execute( trend_active = conn.execute(
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'" "SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
).fetchone()[0] ).fetchone()[0]
trend_plans = conn.execute( trend_plans_raw = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC" "SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC"
).fetchall() ).fetchall()
trend_plans = [enrich_active_trend_plan_row(r) for r in trend_plans_raw]
plan_history = []
preview_snapshots = []
if page == "plan_history":
plan_history_raw = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE status != 'active' ORDER BY id DESC LIMIT 100"
).fetchall()
for pr in plan_history_raw:
pd = row_to_dict(pr)
pd["status_label"] = trend_plan_history_status_label(pd.get("status"))
plan_history.append(pd)
snap_rows = conn.execute(
"SELECT * FROM trend_pullback_preview_snapshots ORDER BY id DESC LIMIT 150"
).fetchall()
for sr in snap_rows:
sd = row_to_dict(sr)
sd["outcome_label"] = preview_snapshot_outcome_label(sd.get("outcome"))
preview_snapshots.append(sd)
can_trade = ( can_trade = (
trading_day_reset_allows_new_open(now) trading_day_reset_allows_new_open(now)
and active_count == 0 and active_count == 0
@@ -4508,6 +4945,9 @@ def render_main_page(page="trade"):
active_count=active_count, active_count=active_count,
can_trade=can_trade, can_trade=can_trade,
trend_plans=trend_plans, trend_plans=trend_plans,
plan_history=plan_history,
preview_snapshots=preview_snapshots,
exchange_sync_from_label=(EXCHANGE_POSITION_SYNC_FROM_BJ or "最近90天"),
trend_pullback_dca_legs=TREND_PULLBACK_DCA_LEGS, trend_pullback_dca_legs=TREND_PULLBACK_DCA_LEGS,
trend_pullback_preview_ttl=TREND_PULLBACK_PREVIEW_TTL_SECONDS, trend_pullback_preview_ttl=TREND_PULLBACK_PREVIEW_TTL_SECONDS,
trend_preview=trend_preview, trend_preview=trend_preview,
@@ -4518,13 +4958,15 @@ def render_main_page(page="trade"):
trend_preview_max_drift_pct=TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT, trend_preview_max_drift_pct=TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT,
focus_key_id=(key_list[0]["id"] if key_list else None), focus_key_id=(key_list[0]["id"] if key_list else None),
focus_order_id=(order_list[0]["id"] if order_list else None), focus_order_id=(order_list[0]["id"] if order_list else None),
data_export_version=2, data_export_version=3,
key_alert_max_times=KEY_ALERT_MAX_TIMES, key_alert_max_times=KEY_ALERT_MAX_TIMES,
risk_percent=RISK_PERCENT, risk_percent=RISK_PERCENT,
breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER, breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER,
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT, breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
occupied_miss_total=occupied_miss_total, occupied_miss_total=occupied_miss_total,
price_fmt=format_price_for_symbol, price_fmt=format_price_for_symbol,
amt_fmt=format_amount_for_symbol,
money_fmt=format_money_usdt,
entry_reason_options=list(ENTRY_REASON_OPTIONS), entry_reason_options=list(ENTRY_REASON_OPTIONS),
entry_reason_other_value=ENTRY_REASON_OTHER, entry_reason_other_value=ENTRY_REASON_OTHER,
exchange_display=EXCHANGE_DISPLAY_NAME, exchange_display=EXCHANGE_DISPLAY_NAME,
@@ -4555,6 +4997,25 @@ def stats_page():
return render_main_page("stats") return render_main_page("stats")
@app.route("/plan_history")
@login_required
def plan_history_page():
return render_main_page("plan_history")
@app.route("/api/preview_snapshot/<int:sid>")
@login_required
def api_preview_snapshot(sid):
conn = get_db()
row = conn.execute("SELECT * FROM trend_pullback_preview_snapshots WHERE id=?", (sid,)).fetchone()
conn.close()
if not row:
return jsonify({"ok": False, "msg": "not_found"}), 404
d = row_to_dict(row)
d["outcome_label"] = preview_snapshot_outcome_label(d.get("outcome"))
return jsonify({"ok": True, "snapshot": d})
@app.route("/api/account_snapshot") @app.route("/api/account_snapshot")
@login_required @login_required
def api_account_snapshot(): def api_account_snapshot():
@@ -4564,9 +5025,9 @@ def api_account_snapshot():
session_row = ensure_session(conn, trading_day) session_row = ensure_session(conn, trading_day)
local_current_capital = float(session_row["current_capital"]) local_current_capital = float(session_row["current_capital"])
funding_capital, trading_capital = get_exchange_capitals(force=True) funding_capital, trading_capital = get_exchange_capitals(force=True)
funding_usdt = round(funding_capital, 4) if funding_capital is not None else None funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
current_capital = round(trading_capital, 4) if trading_capital is not None else round(local_current_capital, 4) current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
recommended_capital = get_recommended_capital(current_capital) recommended_capital = round(get_recommended_capital(current_capital), 2)
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0] active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
conn.close() conn.close()
can_trade = trading_day_reset_allows_new_open(now) and active_count == 0 can_trade = trading_day_reset_allows_new_open(now) and active_count == 0
@@ -4574,7 +5035,7 @@ def api_account_snapshot():
return jsonify({ return jsonify({
"funding_usdt": funding_usdt, "funding_usdt": funding_usdt,
"current_capital": current_capital, "current_capital": current_capital,
"available_trading_usdt": round(available_trading_usdt, 4) if available_trading_usdt is not None else None, "available_trading_usdt": round(available_trading_usdt, 2) if available_trading_usdt is not None else None,
"recommended_capital": recommended_capital, "recommended_capital": recommended_capital,
"active_count": active_count, "active_count": active_count,
"can_trade": can_trade, "can_trade": can_trade,
@@ -5366,6 +5827,7 @@ def preview_trend_pullback():
created, created,
), ),
) )
insert_trend_preview_snapshot(conn, pid, created, exp_ms, payload)
conn.commit() conn.commit()
conn.close() conn.close()
flash(f"预览已生成,有效期 {TREND_PULLBACK_PREVIEW_TTL_SECONDS} 秒,请核对后点击「确认执行」。") flash(f"预览已生成,有效期 {TREND_PULLBACK_PREVIEW_TTL_SECONDS} 秒,请核对后点击「确认执行」。")
@@ -5444,7 +5906,7 @@ def execute_trend_pullback():
trading_day = get_trading_day(now) trading_day = get_trading_day(now)
opened_at = app_now_str() opened_at = app_now_str()
opened_ms = _to_ms_with_fallback(None, opened_at) opened_ms = _to_ms_with_fallback(None, opened_at)
conn.execute( cur = conn.execute(
"""INSERT INTO trend_pullback_plans ( """INSERT INTO trend_pullback_plans (
status,symbol,exchange_symbol,direction,leverage,stop_loss,add_upper,take_profit,risk_percent, status,symbol,exchange_symbol,direction,leverage,stop_loss,add_upper,take_profit,risk_percent,
snapshot_available_usdt,snapshot_at,plan_margin_capital,target_order_amount,first_order_amount,remainder_total, snapshot_available_usdt,snapshot_at,plan_margin_capital,target_order_amount,first_order_amount,remainder_total,
@@ -5481,11 +5943,16 @@ def execute_trend_pullback():
f"预览ID:{pid[:8]}", f"预览ID:{pid[:8]}",
), ),
) )
new_plan_id = int(cur.lastrowid)
conn.execute(
"UPDATE trend_pullback_preview_snapshots SET outcome='executed', executed_plan_id=? WHERE preview_id=?",
(new_plan_id, pid),
)
conn.execute("DELETE FROM trend_pullback_previews WHERE id=?", (pid,)) conn.execute("DELETE FROM trend_pullback_previews WHERE id=?", (pid,))
conn.commit() conn.commit()
conn.close() conn.close()
flash( flash(
f"趋势回调已执行:可用余额(执行时){round(snap, 4)}U;计划保证金约 {round(margin_plan, 4)}U" f"趋势回调已执行:可用余额(执行时){round(snap, 2)}U;计划保证金约 {round(margin_plan, 2)}U"
f"总张数约 {target_amt},首仓 {first_amt},补仓 {n_legs} 档;已挂交易所止损,止盈由程序监控。" f"总张数约 {target_amt},首仓 {first_amt},补仓 {n_legs} 档;已挂交易所止损,止盈由程序监控。"
) )
return redirect(url_for("trade_page")) return redirect(url_for("trade_page"))
@@ -5497,6 +5964,10 @@ def cancel_trend_pullback_preview():
pid = (request.form.get("preview_id") or "").strip() pid = (request.form.get("preview_id") or "").strip()
conn = get_db() conn = get_db()
if pid: if pid:
conn.execute(
"UPDATE trend_pullback_preview_snapshots SET outcome='cancelled' WHERE preview_id=? AND outcome='open'",
(pid,),
)
conn.execute("DELETE FROM trend_pullback_previews WHERE id=?", (pid,)) conn.execute("DELETE FROM trend_pullback_previews WHERE id=?", (pid,))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -5546,6 +6017,28 @@ def stop_trend_pullback(pid):
return redirect("/trade") return redirect("/trade")
@app.route("/delete_trend_plan_history/<int:pid>", methods=["POST"])
@login_required
def delete_trend_plan_history(pid):
conn = get_db()
row = conn.execute("SELECT id, status FROM trend_pullback_plans WHERE id=?", (pid,)).fetchone()
if not row:
conn.close()
flash("计划不存在")
return redirect(request.referrer or url_for("plan_history_page"))
if (row["status"] or "").strip() == "active":
conn.close()
flash("运行中的计划请使用「结束计划」,不可从历史中删除")
return redirect(request.referrer or url_for("plan_history_page"))
conn.execute("DELETE FROM trade_records WHERE trend_plan_id=?", (pid,))
conn.execute("DELETE FROM trend_pullback_preview_snapshots WHERE executed_plan_id=?", (pid,))
conn.execute("DELETE FROM trend_pullback_plans WHERE id=?", (pid,))
conn.commit()
conn.close()
flash("已删除该计划历史及关联趋势交易记录(若有)")
return redirect(request.referrer or url_for("plan_history_page"))
@app.route("/delete_key_monitor/<int:kid>", methods=["POST"]) @app.route("/delete_key_monitor/<int:kid>", methods=["POST"])
@login_required @login_required
def delete_key_monitor(kid): def delete_key_monitor(kid):
@@ -5622,7 +6115,8 @@ def export_trade_records():
rows = conn.execute( rows = conn.execute(
"SELECT id,symbol,monitor_type,direction,trigger_price,stop_loss,take_profit,margin_capital,leverage," "SELECT id,symbol,monitor_type,direction,trigger_price,stop_loss,take_profit,margin_capital,leverage,"
"pnl_amount,hold_seconds,hold_minutes,opened_at,closed_at,result,miss_reason," "pnl_amount,hold_seconds,hold_minutes,opened_at,closed_at,result,miss_reason,"
"entry_reason,reviewed_entry_reason,created_at FROM trade_records ORDER BY id ASC" "entry_reason,reviewed_entry_reason,created_at,trend_plan_id,exchange_realized_pnl,"
"exchange_opened_at,exchange_closed_at,exchange_sync_key FROM trade_records ORDER BY id ASC"
).fetchall() ).fetchall()
conn.close() conn.close()
head_base = [ head_base = [
@@ -5645,6 +6139,11 @@ def export_trade_records():
"entry_reason", "entry_reason",
"reviewed_entry_reason", "reviewed_entry_reason",
"created_at", "created_at",
"trend_plan_id",
"exchange_realized_pnl",
"exchange_opened_at",
"exchange_closed_at",
"exchange_sync_key",
] ]
head = head_base + ["开仓类型"] head = head_base + ["开仓类型"]
data = [] data = []
+130 -36
View File
@@ -130,14 +130,14 @@
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div> <div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div> <div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ s.net_pnl_u }}</div></div> <div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ money_fmt(s.net_pnl_u) }}</div></div>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ s.loss_sum_u }}</div></div> <div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ money_fmt(s.loss_sum_u) }}</div></div>
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ s.max_single_loss }}{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ money_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ s.max_single_profit }}{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ money_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ s.max_drawdown_u }}</div></div> <div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ money_fmt(s.max_drawdown_u) }}</div></div>
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div> <div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div> <div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}{{ s.worst_day_pnl }}U{% else %}-{% endif %}</div></div> <div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}{{ money_fmt(s.worst_day_pnl) }}U{% else %}-{% endif %}</div></div>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
@@ -149,12 +149,13 @@
<div class="top-nav"> <div class="top-nav">
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a> <a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录</a> <a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录</a>
<a href="/plan_history" class="{% if page == 'plan_history' %}active{% endif %}">计划历史</a>
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a> <a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
</div> </div>
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %} {% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
<div class="export-bar"> <div class="export-bar">
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSVUTF-8;交易记录含开仓类型列):</span> <span style="color:#9aa">数据导出(v{{ data_export_version }} CSVUTF-8;交易记录含开仓类型列及交易所对齐字段):</span>
<a href="/export/trade_records">交易记录</a> <a href="/export/trade_records">交易记录</a>
</div> </div>
<div class="stat-box"> <div class="stat-box">
@@ -162,9 +163,9 @@
<div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div> <div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div>
<div class="stat-item"><div class="label">错过次数</div><div class="value">{{ miss_count }}</div></div> <div class="stat-item"><div class="label">错过次数</div><div class="value">{{ miss_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div> <div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div>
<div class="stat-item"><div class="label">资金账户(USDT)</div><div class="value" id="total-capital">{% if funding_usdt is not none %}{{ funding_usdt }}U{% else %}—{% endif %}</div></div> <div class="stat-item"><div class="label">资金账户(USDT)</div><div class="value" id="total-capital">{% if funding_usdt is not none %}{{ money_fmt(funding_usdt) }}U{% else %}—{% endif %}</div></div>
<div class="stat-item"><div class="label">交易日</div><div class="value">{{ trading_day }}</div></div> <div class="stat-item"><div class="label">交易日</div><div class="value">{{ trading_day }}</div></div>
<div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ current_capital }}U</div></div> <div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ money_fmt(current_capital) }}U</div></div>
</div> </div>
<div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div> <div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div>
@@ -188,7 +189,7 @@
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}% 以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div> </div>
<div class="rule-tip"> <div class="rule-tip">
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ auto_transfer_amount }}U,来自 {{ auto_transfer_from }} 划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ money_fmt(auto_transfer_amount) }}U,来自 {{ auto_transfer_from }}
</div> </div>
<form action="/manual_transfer" method="post" class="form-row"> <form action="/manual_transfer" method="post" class="form-row">
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required> <input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
@@ -236,14 +237,14 @@
<div class="list-item"> <div class="list-item">
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div> <div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
<div> <div>
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ o.risk_amount or '-' }}U 风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ money_fmt(o.risk_amount) }}U
| {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ o.breakeven_price or '-' }}{% else %}移动保本:关{% endif %} | {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
<br> <br>
成交:{{ o.trigger_price }} 止损:{{ o.stop_loss }} 止盈:{{ o.take_profit }} 成交:{{ price_fmt(o.symbol, o.trigger_price) }} 止损:{{ price_fmt(o.symbol, o.stop_loss) }} 止盈:{{ price_fmt(o.symbol, o.take_profit) }}
| 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span> | 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span>
| 现价:<span id="order-price-{{ o.id }}">-</span> | 现价:<span id="order-price-{{ o.id }}">-</span>
| 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span> | 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span>
| 计划基数:{{ o.margin_capital }}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span> | 计划基数:{{ money_fmt(o.margin_capital) }}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span>
| 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}% | 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}%
</div> </div>
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a> <a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
@@ -282,15 +283,15 @@
</div> </div>
<div style="font-size:.82rem;color:#cfd3ef;line-height:1.55;margin-bottom:10px"> <div style="font-size:.82rem;color:#cfd3ef;line-height:1.55;margin-bottom:10px">
{{ trend_preview.symbol }} {{ '做多' if trend_preview.direction == 'long' else '做空' }} {{ trend_preview.leverage }}x {{ trend_preview.symbol }} {{ '做多' if trend_preview.direction == 'long' else '做空' }} {{ trend_preview.leverage }}x
预览可用快照 <strong>{{ trend_preview.snapshot_available_usdt }}</strong> U 参考价 {{ trend_preview.live_price_ref }} 预览可用快照 <strong>{{ money_fmt(trend_preview.snapshot_available_usdt) }}</strong> U 参考价 {{ price_fmt(trend_preview.symbol, trend_preview.live_price_ref) }}
计划保证金≈{{ trend_preview.plan_margin_capital }} U 总张≈{{ trend_preview.target_order_amount }}(首仓 {{ trend_preview.first_order_amount }} + 补仓 {{ trend_preview.remainder_total }}<br> 计划保证金≈{{ money_fmt(trend_preview.plan_margin_capital) }} U 总张≈{{ amt_fmt(trend_preview.symbol, trend_preview.target_order_amount) }}(首仓 {{ amt_fmt(trend_preview.symbol, trend_preview.first_order_amount) }} + 补仓 {{ amt_fmt(trend_preview.symbol, trend_preview.remainder_total) }}<br>
止损 {{ trend_preview.stop_loss }} 补仓上沿 {{ trend_preview.add_upper }} 止盈 {{ trend_preview.take_profit }} 风险比例 {{ trend_preview.risk_percent }}% 止损 {{ price_fmt(trend_preview.symbol, trend_preview.stop_loss) }} 补仓上沿 {{ price_fmt(trend_preview.symbol, trend_preview.add_upper) }} 止盈 {{ price_fmt(trend_preview.symbol, trend_preview.take_profit) }} 风险比例 {{ trend_preview.risk_percent }}%
</div> </div>
<div class="table-wrap" style="margin-bottom:10px"> <div class="table-wrap" style="margin-bottom:10px">
<table> <table>
<tr><th>#</th><th>补仓触发价</th><th>该档张数</th></tr> <tr><th>#</th><th>补仓触发价</th><th>该档张数</th></tr>
{% for row in trend_preview_levels %} {% for row in trend_preview_levels %}
<tr><td>{{ row.i }}</td><td>{{ row.price }}</td><td>{{ row.contracts }}</td></tr> <tr><td>{{ row.i }}</td><td>{{ price_fmt(trend_preview.symbol, row.price) }}</td><td>{{ amt_fmt(trend_preview.symbol, row.contracts) }}</td></tr>
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
@@ -331,9 +332,10 @@
<div class="list-item"> <div class="list-item">
<div><strong>#{{ t.id }} {{ t.symbol }}</strong> | {{ '做多' if t.direction == 'long' else '做空' }} | {{ t.leverage }}x</div> <div><strong>#{{ t.id }} {{ t.symbol }}</strong> | {{ '做多' if t.direction == 'long' else '做空' }} | {{ t.leverage }}x</div>
<div style="font-size:.82rem;color:#cfd3ef"> <div style="font-size:.82rem;color:#cfd3ef">
可用快照:{{ t.snapshot_available_usdt }}U | 计划保证金≈{{ t.plan_margin_capital }}U | 总张≈{{ t.target_order_amount }} 首仓{{ t.first_order_amount }} 补仓档{{ t.dca_legs }} 可用快照:{{ money_fmt(t.snapshot_available_usdt) }}U | 计划保证金≈{{ money_fmt(t.plan_margin_capital) }}U | 总张≈{{ amt_fmt(t.symbol, t.target_order_amount) }} 首仓{{ amt_fmt(t.symbol, t.first_order_amount) }} 补仓档{{ t.dca_legs }}
<br>止损:{{ t.stop_loss }} 补仓上沿:{{ t.add_upper }} 止盈:{{ t.take_profit }} <br>止损:{{ price_fmt(t.symbol, t.stop_loss) }} 补仓上沿:{{ price_fmt(t.symbol, t.add_upper) }} 止盈:{{ price_fmt(t.symbol, t.take_profit) }}
<br>均价:{{ t.avg_entry_price }} 已补仓:{{ t.legs_done }}/{{ t.dca_legs }} <br>均价:{{ price_fmt(t.symbol, t.avg_entry_price) }} 已补仓:{{ t.legs_done }}/{{ t.dca_legs }}
<br>浮盈亏(交易所): {% if t.floating_pnl is not none %}<span class="{{ 'pnl-profit' if t.floating_pnl > 0 else ('pnl-loss' if t.floating_pnl < 0 else '') }}">{{ money_fmt(t.floating_pnl) }} U</span>{% else %}—{% endif %}{% if t.floating_mark is not none %} 标记价: {{ price_fmt(t.symbol, t.floating_mark) }}{% endif %}
</div> </div>
<a href="/stop_trend_pullback/{{ t.id }}" class="btn-del" onclick="return confirm('结束计划:市价平仓并撤掉该合约全部挂单,确定?')">结束计划</a> <a href="/stop_trend_pullback/{{ t.id }}" class="btn-del" onclick="return confirm('结束计划:市价平仓并撤掉该合约全部挂单,确定?')">结束计划</a>
</div> </div>
@@ -349,25 +351,24 @@
<h2>交易记录</h2> <h2>交易记录</h2>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr> <tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓(展示)</th><th>平仓(展示)</th><th>盈亏U(展示)</th><th>结果</th><th>操作</th></tr>
{% for r in record %} {% for r in record %}
<tr id="trade-row-{{ r.id }}"> <tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td> <td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}</td> <td>{{ r.monitor_type }}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td> <td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ r.trigger_price }}</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 %} {% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %} {% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td> <td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td> <td>{{ price_fmt(r.symbol, tp_show) }}</td>
<td>{{ r.margin_capital or '-' }}</td> <td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ money_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td> <td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td> <td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td> <td>{{ r.display_opened_at }}</td>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td> <td>{{ r.display_closed_at }}</td>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %} {% set pnl_val = (r.display_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ 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 '') }}">{{ money_fmt(r.display_pnl_amount) }}</span>{% if r.monitor_type == '趋势回调' and r.display_pnl_source == 'local' %}<span style="font-size:.68rem;color:#8892b0"></span>{% elif r.monitor_type == '趋势回调' and r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></span>{% endif %}</td>
<td> <td>
{% set effective_result = r.effective_result %} {% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span> {% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
@@ -407,6 +408,64 @@
{% endif %} {% endif %}
</div> </div>
{% if page == 'plan_history' %}
<div class="card full" style="margin-bottom:12px">
<h2 style="margin-bottom:6px">已结束的趋势回调计划</h2>
<div class="rule-tip" style="margin-bottom:8px">删除将同时移除 <code>trend_plan_id</code> 关联的「趋势回调」交易记录及该计划对应的预览快照归档。交易所平仓同步起点(北京日期):<strong>{{ exchange_sync_from_label }}</strong><code>EXCHANGE_POSITION_SYNC_FROM_BJ</code>)。</div>
{% if plan_history and plan_history|length > 0 %}
<div class="table-wrap">
<table>
<tr><th>ID</th><th>品种</th><th>方向</th><th>杠杆</th><th>状态</th><th>结束</th><th>开仓时间</th><th>计划保证金≈</th><th>操作</th></tr>
{% for p in plan_history %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.symbol }}</td>
<td><span class="badge {{ 'direction-long' if p.direction == 'long' else 'direction-short' }}">{{ '做多' if p.direction == 'long' else '做空' }}</span></td>
<td>{{ p.leverage }}x</td>
<td>{{ p.status_label }}</td>
<td>{{ p.message or '-' }}</td>
<td>{{ (p.opened_at or '-')[:16] }}</td>
<td>{% if p.plan_margin_capital is not none %}{{ money_fmt(p.plan_margin_capital) }}{% else %}-{% endif %}</td>
<td>
<form action="{{ url_for('delete_trend_plan_history', pid=p.id) }}" method="post" style="display:inline" onsubmit="return confirm('确定删除该计划历史及关联趋势交易记录?');">
<button type="submit" class="table-del">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="rule-tip" style="color:#8892b0">暂无已结束的计划</div>
{% endif %}
</div>
<div class="card full" style="margin-bottom:12px">
<h2 style="margin-bottom:6px">预览快照(自本版本起留存)</h2>
<div class="rule-tip" style="margin-bottom:8px">每次「生成预览」自动归档;取消、过期或执行后仍可点开查看当时参数。执行后状态为「已执行」并带关联计划 ID。</div>
{% if preview_snapshots and preview_snapshots|length > 0 %}
<div class="table-wrap">
<table>
<tr><th>ID</th><th>时间</th><th>品种</th><th>方向</th><th>杠杆</th><th>状态</th><th>快照余额U</th><th>操作</th></tr>
{% for s in preview_snapshots %}
<tr>
<td>{{ s.id }}</td>
<td>{{ (s.preview_created_at or '-')[:16] }}</td>
<td>{{ s.symbol }}</td>
<td>{{ '多' if s.direction == 'long' else '空' }}</td>
<td>{{ s.leverage }}x</td>
<td>{{ s.outcome_label }}{% if s.executed_plan_id %} #{{ s.executed_plan_id }}{% endif %}</td>
<td>{{ money_fmt(s.snapshot_available_usdt) }}</td>
<td><button type="button" class="table-del" style="background:#1f3a5a;color:#8fc8ff" onclick="openPreviewSnapshotDetail({{ s.id }})">查看</button></td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="rule-tip" style="color:#8892b0">暂无预览快照(新版本生成预览后将出现在此)</div>
{% endif %}
</div>
{% endif %}
{% if page == 'stats' %} {% if page == 'stats' %}
<div class="card stats-card full" id="stats-card"> <div class="card stats-card full" id="stats-card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap"> <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
@@ -484,6 +543,41 @@ function validateJournalEntryReason(){
function showImage(src){document.getElementById("bigImg").src=src;document.getElementById("imgModal").style.display="flex";} function showImage(src){document.getElementById("bigImg").src=src;document.getElementById("imgModal").style.display="flex";}
function closeModal(){document.getElementById("imgModal").style.display="none";} function closeModal(){document.getElementById("imgModal").style.display="none";}
function forceCloseDetailModal(){document.getElementById("detailModal").style.display="none";} function forceCloseDetailModal(){document.getElementById("detailModal").style.display="none";}
function fmtU2(n){
if(n === null || n === undefined || n === "") return "-";
const x = Number(n);
if(Number.isNaN(x)) return String(n);
return x.toFixed(2);
}
function openPreviewSnapshotDetail(id){
fetch(`/api/preview_snapshot/${id}`).then(r=>r.json()).then(data=>{
if(!data.ok){ alert((data && data.msg) || "加载失败"); return; }
const s = data.snapshot;
const lines = [
`预览ID${s.preview_id || "-"}`,
`归档状态:${s.outcome_label || "-"}`,
`关联计划ID${s.executed_plan_id != null ? s.executed_plan_id : "-"}`,
"",
`${s.symbol || "-"} ${s.direction === "long" ? "做多" : "做空"} ${s.leverage || "-"}x`,
`可用快照U${fmtU2(s.snapshot_available_usdt)}`,
`参考价:${s.live_price_ref != null ? s.live_price_ref : "-"}`,
`计划保证金≈U${fmtU2(s.plan_margin_capital)}`,
`总张数:${s.target_order_amount != null ? s.target_order_amount : "-"}`,
`首仓/补仓余:${s.first_order_amount != null ? s.first_order_amount : "-"} / ${s.remainder_total != null ? s.remainder_total : "-"}`,
`补仓档数:${s.dca_legs != null ? s.dca_legs : "-"}`,
`止损 / 补仓上沿 / 止盈:${s.stop_loss} / ${s.add_upper} / ${s.take_profit}`,
`风险%${s.risk_percent != null ? s.risk_percent : "-"}`,
`网格价 JSON${s.grid_prices_json || "[]"}`,
`分档张数 JSON${s.leg_amounts_json || "[]"}`,
`创建时间:${s.preview_created_at || "-"}`,
`预览过期(ms)${s.expires_at_ms != null ? s.expires_at_ms : "-"}`,
].join("\n");
document.getElementById("detailTitle").innerText = `预览快照 #${id}`;
document.getElementById("detailBody").innerText = lines;
document.getElementById("detailImage").style.display = "none";
document.getElementById("detailModal").style.display = "flex";
}).catch(()=>{ alert("网络错误"); });
}
function closeDetailModal(e){if(e.target && e.target.id==="detailModal"){forceCloseDetailModal();}} function closeDetailModal(e){if(e.target && e.target.id==="detailModal"){forceCloseDetailModal();}}
const journalCache = {}; const journalCache = {};
@@ -1060,7 +1154,7 @@ function refreshPriceSnapshot(){
const mv = o.exchange_initial_margin; const mv = o.exchange_initial_margin;
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv); const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
if(!Number.isNaN(mn)){ if(!Number.isNaN(mn)){
exM.innerText = `${mn.toFixed(4)}U`; exM.innerText = `${mn.toFixed(2)}U`;
} else { } else {
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null; const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
exM.innerText = (prc === 0) ? "无仓数据" : "-"; exM.innerText = (prc === 0) ? "无仓数据" : "-";
@@ -1068,7 +1162,7 @@ function refreshPriceSnapshot(){
} }
const pnlEl = document.getElementById(`order-pnl-${o.id}`); const pnlEl = document.getElementById(`order-pnl-${o.id}`);
if(pnlEl){ if(pnlEl){
pnlEl.innerText = `${formatSigned(o.float_pnl, 4)}U (${formatSigned(o.float_pct, 2)}%)`; pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
pnlEl.classList.remove("price-up","price-down","price-flat"); pnlEl.classList.remove("price-up","price-down","price-flat");
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up"); if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down"); else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
@@ -1102,7 +1196,7 @@ function refreshOrderDefaults(){
const fullEl = document.getElementById("use-full-margin"); const fullEl = document.getElementById("use-full-margin");
const marginEl = document.getElementById("order-margin"); const marginEl = document.getElementById("order-margin");
if(fullEl && marginEl && fullEl.checked){ if(fullEl && marginEl && fullEl.checked){
const m = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(4); const m = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(2);
marginEl.value = m; marginEl.value = m;
} }
} }
@@ -1113,18 +1207,18 @@ function refreshAccountSnapshot(){
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{ fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
if (typeof data.funding_usdt !== "undefined") { if (typeof data.funding_usdt !== "undefined") {
const el = document.getElementById("total-capital"); const el = document.getElementById("total-capital");
if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${data.funding_usdt}U`; if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${Number(data.funding_usdt).toFixed(2)}U`;
} }
if (typeof data.current_capital !== "undefined") { if (typeof data.current_capital !== "undefined") {
const el = document.getElementById("current-capital"); const el = document.getElementById("current-capital");
if(el) el.innerText = `${data.current_capital}U`; if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`;
} }
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) { if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
latestAvailableUsdt = Number(data.available_trading_usdt); latestAvailableUsdt = Number(data.available_trading_usdt);
} }
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00"; const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00";
const tip = document.getElementById("order-rule-tip"); const tip = document.getElementById("order-rule-tip");
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt}U` : ""; const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt.toFixed(2)}U` : "";
if(tip){ if(tip){
tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`; tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`;
} }
@@ -1140,7 +1234,7 @@ if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){ fullMarginEl.addEventListener("change", function(){
const marginEl = document.getElementById("order-margin"); const marginEl = document.getElementById("order-margin");
if(marginEl && this.checked && latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)){ if(marginEl && this.checked && latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)){
marginEl.value = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(4); marginEl.value = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(2);
} }
}); });
} }
@@ -50,6 +50,24 @@
用户可「取消预览」删除 `trend_pullback_previews` 中对应记录;过期记录会在新预览或页面加载时清理。 用户可「取消预览」删除 `trend_pullback_previews` 中对应记录;过期记录会在新预览或页面加载时清理。
### 3.4 界面:计划历史与运行中浮动盈亏
- **计划历史(页顶卡片)**
- 仅展示 **`trend_pullback_plans` 中已结束的计划**`status != 'active'`,如止盈结束、止损结束、手动结束)。
- **不包含**仅存在于 `trend_pullback_previews`、从未「确认执行」的预览。
- 每行提供 **删除**:删除该计划行,并删除 `trade_records`**`trend_plan_id` 与之相同** 且类型为「趋势回调」的记录(用于与计划一一对应的新数据;历史旧行若无 `trend_plan_id` 则不会随删)。
- **运行中的计划(交易执行页)**
- 在计划摘要下方展示 **浮盈亏(交易所)**:来自 Gate 当前持仓接口的 **未实现盈亏**(及标记价,若可得);与本地按均价估算可能略有差异,以交易所为准便于对照。
### 3.5 交易记录与交易所「已实现盈亏」对齐
- 平仓时仍会写入一条 **`trade_records`**`monitor_type=趋势回调`),其中的 **`pnl_amount` 等为本地估算**`calc_pnl`,不含手续费、资金费等完整账单口径)。
- 打开 **「交易执行」或「交易记录」** 页面时,若已配置 **`GATE_API_KEY` / `GATE_API_SECRET`**(不要求 `LIVE_TRADING_ENABLED=true`,只读即可),应用会按节流策略(同进程约 **25 秒**内最多一次)调用 Gate **`fetch_positions_history`(平仓历史)**,为尚未写入 `exchange_sync_key` 的趋势回调记录 **匹配一条平仓记录**,并回填:
- **`exchange_realized_pnl`**:交易所口径已实现盈亏(与 App「历史仓位」更接近);
- **`exchange_opened_at` / `exchange_closed_at`**:换算为应用时区(默认北京)下的开、平时间字符串。
- **交易记录表**展示列「开仓(展示) / 平仓(展示) / 盈亏U(展示)」:对「趋势回调」行,若已同步则优先显示交易所字段(界面小字 **「所」**);未同步前仍显示本地复盘字段(小字 **「估」**)。
- 匹配规则概要:同品种、同方向、平仓时间与本地 `closed_at` 接近,并结合 **`trend_plan_id`** 对应计划的 `opened_at` 收窄时间窗;极端情况下若短时间多笔同向同品种,仍存在错配可能,可对照 `exchange_sync_key` 与交易所记录。
--- ---
## 4. 与「机器人下单监控」的差异 ## 4. 与「机器人下单监控」的差异
@@ -82,10 +100,16 @@
| `MONITOR_POLL_SECONDS` | 监控轮询间隔(秒) | `3` | | `MONITOR_POLL_SECONDS` | 监控轮询间隔(秒) | `3` |
| `LIVE_TRADING_ENABLED` | 是否允许真实下单 | `false` | | `LIVE_TRADING_ENABLED` | 是否允许真实下单 | `false` |
| `FULL_MARGIN_BUFFER_RATIO` | 计划保证金相对可用余额上限比例 | `0.98` | | `FULL_MARGIN_BUFFER_RATIO` | 计划保证金相对可用余额上限比例 | `0.98` |
| `APP_TIMEZONE` | 应用墙钟与「北京日期」同步起点时区(如 `Asia/Shanghai` | `Asia/Shanghai` |
| `EXCHANGE_POSITION_SYNC_FROM_BJ` | 拉取 Gate **平仓历史** 的最早日期(`YYYY-MM-DD`,按 `APP_TIMEZONE` 当日 **00:00** 起算)。**留空**则从近 **90 天** 起拉取 | 空 |
| `EXCHANGE_POSITION_HISTORY_LIMIT` | 单次拉取平仓历史条数上限(50–1000) | `200` |
--- ---
## 7. 数据库 ## 7. 数据库
- **`trend_pullback_previews`**:未执行的预览行(含 `expires_at_ms`),执行成功或取消后删除;过期可被清理。 - **`trend_pullback_previews`**:未执行的预览行(含 `expires_at_ms`),执行成功或取消后删除;过期可被清理。
- **`trend_pullback_plans`**已执行且运行中的计划;字段含快照可用余额、计划保证金、总张数、首仓张数、补仓 JSON、网格价 JSON、已补仓档数、均价、状态等。平仓结果写入 `trade_records``monitor_type`**`趋势回调`**。 - **`trend_pullback_plans`**趋势回调计划。执行后写入一行,`status='active'` 表示运行中;止盈 / 止损 / 手动结束后变为 **`stopped_tp` / `stopped_sl` / `stopped_manual`** 等非 `active` 状态,并出现在页顶 **计划历史**字段含快照可用余额、计划保证金、总张数、首仓张数、补仓 JSON、网格价 JSON、已补仓档数、均价、`opened_at``message`(结束说明)等。
- **`trade_records`**`monitor_type=趋势回调`):每次计划结束插入一行;含本地估算盈亏等。新写入行带 **`trend_plan_id`** 指向 `trend_pullback_plans.id`。另含 **`exchange_realized_pnl``exchange_opened_at``exchange_closed_at``exchange_sync_key`**,由页面触发的交易所平仓历史同步填充(见 3.5)。
**CSV 导出**:交易记录导出为 **v3**,包含上述交易所对齐字段及 `trend_plan_id`
+18
View File
@@ -150,6 +150,24 @@ TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT=5
- **生成预览**与**确认执行**时都会读取 **Gate 永续账户 USDT 可用余额**;请尽量使用 **单独子账户** 承载策略资金。 - **生成预览**与**确认执行**时都会读取 **Gate 永续账户 USDT 可用余额**;请尽量使用 **单独子账户** 承载策略资金。
**界面与对账(与策略说明 3.4–3.5 节一致)**
- 页顶 **计划历史**:仅 **已结束** 的趋势计划(不含未执行预览);可 **删除** 计划行,并删除 `trend_plan_id` 关联的「趋势回调」`trade_records`(新数据;旧行无 `trend_plan_id` 不级联)。
- **运行中计划**展示交易所 **未实现盈亏**(浮盈亏)。
- **交易记录**:趋势单在配置 API Key 后,打开「交易执行 / 交易记录」页会按节流(约 **25 秒**内同进程最多一次)拉取 Gate **平仓历史**,回填 **`exchange_realized_pnl`** 等;列表展示优先用交易所口径(见策略说明)。
**与交易所对齐的可选环境变量**
```env
# 平仓历史同步起点:北京日期 YYYY-MM-DD 的 0 点(与 APP_TIMEZONE 一致);留空则从近 90 天拉取
# EXCHANGE_POSITION_SYNC_FROM_BJ=2026-05-14
# EXCHANGE_POSITION_HISTORY_LIMIT=200
```
说明:同步 **只读** 交易所接口,**不要求** `LIVE_TRADING_ENABLED=true`;无 Key 时不拉取,界面仍可用(浮盈亏可能为「—」、交易记录仍为本地「估」)。
**交易记录 CSV**:导出为 **v3**,含 `trend_plan_id` 与交易所对齐列(详见策略说明数据库一节)。
--- ---
## 6. 手工启动 Flask(验证) ## 6. 手工启动 Flask(验证)