修改币种精度
This commit is contained in:
+124
-53
@@ -239,10 +239,10 @@ def _wechat_trading_capital_text(fallback=None):
|
||||
except Exception:
|
||||
trading_capital = 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:
|
||||
try:
|
||||
return f"{round(float(fallback), 4)}U"
|
||||
return f"{round(float(fallback), FUNDS_DECIMALS)}U"
|
||||
except Exception:
|
||||
pass
|
||||
return "-"
|
||||
@@ -271,7 +271,7 @@ def build_wechat_close_message(
|
||||
try:
|
||||
if pnl_amount is not None:
|
||||
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:
|
||||
pnl_disp = "-"
|
||||
except (TypeError, ValueError):
|
||||
@@ -1352,19 +1352,19 @@ def _compute_period_metrics(trades):
|
||||
closed = len(trades)
|
||||
wins = 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_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]
|
||||
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_profit = round(max(pos_pnls), 4) if pos_pnls else None
|
||||
max_single_loss = round(min(neg_pnls), FUNDS_DECIMALS) if neg_pnls else None
|
||||
max_single_profit = round(max(pos_pnls), FUNDS_DECIMALS) if pos_pnls else None
|
||||
cum = peak = max_dd = 0.0
|
||||
for p, _, _ in trades:
|
||||
cum += p
|
||||
peak = max(peak, cum)
|
||||
max_dd = max(max_dd, peak - cum)
|
||||
max_dd = round(max_dd, 4)
|
||||
max_dd = round(max_dd, FUNDS_DECIMALS)
|
||||
streak = 0
|
||||
for p, _, _ in reversed(trades):
|
||||
if p < 0:
|
||||
@@ -1388,7 +1388,7 @@ def _compute_period_metrics(trades):
|
||||
else:
|
||||
run = 0
|
||||
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
|
||||
return {
|
||||
"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)
|
||||
conn.execute(
|
||||
"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()
|
||||
return round(new_capital, 4)
|
||||
return round(new_capital, FUNDS_DECIMALS)
|
||||
|
||||
|
||||
def calc_hold_seconds(opened_at_str, closed_at_dt):
|
||||
@@ -1684,17 +1684,66 @@ def to_effective_trade_dict(row):
|
||||
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):
|
||||
if value in (None, ""):
|
||||
return "-"
|
||||
try:
|
||||
v = float(value)
|
||||
except Exception:
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
if v == 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)
|
||||
# 根据币价量级动态精度:低价币保留更多小数,高价币减少噪音位数
|
||||
# 无法加载市场或无该合约时:按价格量级回退(尽量不阻断页面)
|
||||
if av >= 10000:
|
||||
d = 2
|
||||
elif av >= 100:
|
||||
@@ -1734,7 +1783,7 @@ def calc_pnl(direction, trigger_price, exit_price, margin_capital, leverage):
|
||||
pnl_ratio = (trigger - exit_p) / trigger
|
||||
else:
|
||||
pnl_ratio = (exit_p - trigger) / trigger
|
||||
return round(margin * lev * pnl_ratio, 4)
|
||||
return round(margin * lev * pnl_ratio, FUNDS_DECIMALS)
|
||||
except Exception:
|
||||
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)
|
||||
if notional <= 0:
|
||||
return None
|
||||
return round(notional * rf, 6)
|
||||
return round(notional * rf, FUNDS_DECIMALS)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -1910,7 +1959,7 @@ def enrich_order_item(raw_item, current_capital):
|
||||
notional = item.get("notional_value")
|
||||
ratio = item.get("position_ratio")
|
||||
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:
|
||||
ratio = round(margin / current_capital * 100, 2) if current_capital else 0
|
||||
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 "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到合约账户。"
|
||||
clean = re.sub(r"\s+", " ", msg).strip()
|
||||
return f"交易所下单失败:{clean}"
|
||||
@@ -2173,7 +2222,7 @@ def auto_transfer_once_per_day():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
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:
|
||||
conn.execute(
|
||||
"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:
|
||||
conn.execute(
|
||||
"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.close()
|
||||
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()}"
|
||||
)
|
||||
return
|
||||
@@ -2688,13 +2737,21 @@ 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"))
|
||||
out = {}
|
||||
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:
|
||||
out["notional"] = round(notional, 4)
|
||||
out["notional"] = round(notional, FUNDS_DECIMALS)
|
||||
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:
|
||||
out["mark_price"] = round(mark, 8)
|
||||
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)
|
||||
return out or None
|
||||
|
||||
|
||||
@@ -3817,8 +3874,8 @@ def render_main_page(page="trade"):
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals()
|
||||
# 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。
|
||||
funding_usdt = round(funding_capital, 4) 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)
|
||||
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
||||
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)
|
||||
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()
|
||||
@@ -3880,6 +3937,7 @@ def render_main_page(page="trade"):
|
||||
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
||||
occupied_miss_total=occupied_miss_total,
|
||||
price_fmt=format_price_for_symbol,
|
||||
funds_fmt=format_funds_u,
|
||||
entry_reason_options=list(ENTRY_REASON_OPTIONS),
|
||||
entry_reason_other_value=ENTRY_REASON_OTHER,
|
||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
@@ -3919,8 +3977,8 @@ def api_account_snapshot():
|
||||
session_row = ensure_session(conn, trading_day)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals(force=True)
|
||||
funding_usdt = round(funding_capital, 4) 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)
|
||||
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
|
||||
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)
|
||||
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
|
||||
conn.close()
|
||||
@@ -3929,7 +3987,7 @@ def api_account_snapshot():
|
||||
return jsonify({
|
||||
"funding_usdt": funding_usdt,
|
||||
"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,
|
||||
"active_count": active_count,
|
||||
"can_trade": can_trade,
|
||||
@@ -3995,19 +4053,21 @@ def api_price_snapshot():
|
||||
vol_now = round(float(gate.get("vol_break") or 0), 4)
|
||||
vol_avg = round(float(gate.get("avg20") 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)
|
||||
edge = round(float(gate.get("edge_price") or 0), 8)
|
||||
cfm_close = float(gate.get("confirm_close") or 0)
|
||||
edge = float(gate.get("edge_price") or 0)
|
||||
gate_metrics = (
|
||||
f"量值:{vol_now}/{vol_avg} "
|
||||
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:
|
||||
gate_metrics = ""
|
||||
sym_k = r["symbol"]
|
||||
key_prices.append({
|
||||
"id": r["id"],
|
||||
"symbol": r["symbol"],
|
||||
"symbol": sym_k,
|
||||
"price": round(price, 6),
|
||||
"price_display": format_price_for_symbol(sym_k, price),
|
||||
"upper_diff": upper_diff,
|
||||
"upper_pct": upper_pct,
|
||||
"lower_diff": lower_diff,
|
||||
@@ -4026,7 +4086,7 @@ def api_price_snapshot():
|
||||
leverage = float(r["leverage"] or 0)
|
||||
entry = float(r["trigger_price"] or 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"])
|
||||
ex_sym = resolve_monitor_exchange_symbol(r)
|
||||
prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"])
|
||||
@@ -4036,13 +4096,15 @@ def api_price_snapshot():
|
||||
"id": r["id"],
|
||||
"symbol": r["symbol"],
|
||||
"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,
|
||||
"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_notional": None,
|
||||
"exchange_mark_price": None,
|
||||
"exchange_mark_price_display": None,
|
||||
"pnl_source": "plan",
|
||||
}
|
||||
if ex_metrics:
|
||||
@@ -4051,13 +4113,15 @@ def api_price_snapshot():
|
||||
if ex_metrics.get("notional") is not None:
|
||||
payload["exchange_notional"] = ex_metrics["notional"]
|
||||
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:
|
||||
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"
|
||||
denom = ex_metrics.get("initial_margin") or margin
|
||||
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)
|
||||
|
||||
@@ -4109,7 +4173,7 @@ def api_order_defaults():
|
||||
"exchange_symbol": exchange_symbol,
|
||||
"direction": direction,
|
||||
"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)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
_, 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()
|
||||
conn.close()
|
||||
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)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
_, 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()
|
||||
conn.close()
|
||||
if not row:
|
||||
@@ -4194,7 +4258,7 @@ def api_order_kline():
|
||||
leverage = float(order_item.get("leverage") 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_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({
|
||||
"ok": True,
|
||||
@@ -4207,13 +4271,17 @@ def api_order_kline():
|
||||
"trigger_price": order_item.get("trigger_price"),
|
||||
"stop_loss": order_item.get("stop_loss"),
|
||||
"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"),
|
||||
"leverage": order_item.get("leverage"),
|
||||
"position_ratio": order_item.get("position_ratio"),
|
||||
"rr_ratio": order_item.get("rr_ratio"),
|
||||
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
|
||||
"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,
|
||||
},
|
||||
"candles": candles,
|
||||
@@ -4311,6 +4379,8 @@ def api_key_kline():
|
||||
"direction": key_row["direction"] or "long",
|
||||
"upper": upper,
|
||||
"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),
|
||||
"upper_diff": upper_diff,
|
||||
"upper_pct": upper_pct,
|
||||
@@ -4324,6 +4394,7 @@ def api_key_kline():
|
||||
"timeframe": timeframe,
|
||||
"limit": limit,
|
||||
"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,
|
||||
"candles": candles,
|
||||
"updated_at": app_now_str(),
|
||||
@@ -4466,18 +4537,18 @@ def add_order():
|
||||
flash("止损方向不合法:请检查入场方向与止损价格关系")
|
||||
return redirect("/")
|
||||
risk_percent = max(0.01, float(RISK_PERCENT))
|
||||
risk_amount = round(capital_base * risk_percent / 100.0, 4)
|
||||
notional_value = round(risk_amount / risk_fraction, 4)
|
||||
margin_capital = round(notional_value / leverage, 4)
|
||||
risk_amount = round(capital_base * risk_percent / 100.0, FUNDS_DECIMALS)
|
||||
notional_value = round(risk_amount / risk_fraction, FUNDS_DECIMALS)
|
||||
margin_capital = round(notional_value / leverage, FUNDS_DECIMALS)
|
||||
if capital_base and margin_capital > capital_base:
|
||||
conn.close()
|
||||
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
|
||||
return redirect("/")
|
||||
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:
|
||||
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("/")
|
||||
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
|
||||
try:
|
||||
@@ -4592,9 +4663,9 @@ def add_order():
|
||||
|
||||
_, trading_capital_after = get_exchange_capitals(force=True)
|
||||
account_base_display = (
|
||||
round(float(trading_capital_after), 4)
|
||||
round(float(trading_capital_after), FUNDS_DECIMALS)
|
||||
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()
|
||||
dir_text = "多头(long)" if direction == "long" else "空头(short)"
|
||||
@@ -4620,7 +4691,7 @@ def add_order():
|
||||
"🧾 订单基础信息",
|
||||
f"🔖 交易所订单 ID:{open_order_id}",
|
||||
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"合约杠杆:{leverage} 倍",
|
||||
@@ -5350,7 +5421,7 @@ def api_trade_record_review_update():
|
||||
reviewed_closed_at,
|
||||
reviewed_stop_loss,
|
||||
reviewed_take_profit,
|
||||
round(reviewed_pnl_amount, 4),
|
||||
round(reviewed_pnl_amount, FUNDS_DECIMALS),
|
||||
reviewed_result or None,
|
||||
reviewed_miss_reason or None,
|
||||
hold_seconds,
|
||||
|
||||
@@ -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.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">净盈亏(U)</div><div class="value">{{ 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">{% 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_profit is not none %}{{ 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.net_pnl_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 %}{{ 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 %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</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.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>
|
||||
{% 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">{{ 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">资金账户(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" 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 class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8)</div>
|
||||
|
||||
@@ -300,14 +300,14 @@
|
||||
<div class="list-item">
|
||||
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
|
||||
<div>
|
||||
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ o.risk_amount or '-' }}U
|
||||
| {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ o.breakeven_price or '-' }}{% else %}移动保本:关{% endif %}
|
||||
风格:{{ 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→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||
<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-price-{{ 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 }}%
|
||||
</div>
|
||||
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||
@@ -335,18 +335,18 @@
|
||||
<td>{{ r.symbol }}</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>{{ 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 tp_show = r.effective_take_profit or r.take_profit %}
|
||||
<td>{{ price_fmt(r.symbol, stop_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.effective_hold_minutes or 0 }}</td>
|
||||
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
|
||||
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
|
||||
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
|
||||
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ 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>
|
||||
{% set effective_result = r.effective_result %}
|
||||
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
|
||||
@@ -1091,7 +1091,7 @@ setTimeout(() => {
|
||||
let latestAvailableUsdt = null;
|
||||
const lastPriceMap = {};
|
||||
|
||||
function formatSigned(v, digits=4){
|
||||
function formatSigned(v, digits=2){
|
||||
if(v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
const sign = n > 0 ? "+" : "";
|
||||
@@ -1121,7 +1121,7 @@ function refreshPriceSnapshot(){
|
||||
(data.key_prices || []).forEach(k=>{
|
||||
const pEl = document.getElementById(`key-price-${k.id}`);
|
||||
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));
|
||||
}
|
||||
const upEl = document.getElementById(`key-up-diff-${k.id}`);
|
||||
@@ -1146,17 +1146,25 @@ function refreshPriceSnapshot(){
|
||||
const pEl = document.getElementById(`order-price-${o.id}`);
|
||||
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 px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
|
||||
const decimals = hasMark ? 8 : 6;
|
||||
pEl.innerText = px.toFixed(decimals);
|
||||
paintPriceTrend(pEl, `o-${o.id}`, px);
|
||||
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);
|
||||
disp = Number.isFinite(px) ? px.toFixed(6) : "-";
|
||||
}
|
||||
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}`);
|
||||
if(exM){
|
||||
const mv = o.exchange_initial_margin;
|
||||
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
|
||||
if(!Number.isNaN(mn)){
|
||||
exM.innerText = `${mn.toFixed(4)}U`;
|
||||
exM.innerText = `${mn.toFixed(2)}U`;
|
||||
} else {
|
||||
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
|
||||
exM.innerText = (prc === 0) ? "无仓数据" : "-";
|
||||
@@ -1164,7 +1172,7 @@ function refreshPriceSnapshot(){
|
||||
}
|
||||
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
|
||||
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");
|
||||
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
|
||||
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 marginEl = document.getElementById("order-margin");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1209,18 +1217,18 @@ function refreshAccountSnapshot(){
|
||||
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
|
||||
if (typeof data.funding_usdt !== "undefined") {
|
||||
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") {
|
||||
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) {
|
||||
latestAvailableUsdt = Number(data.available_trading_usdt);
|
||||
}
|
||||
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00)";
|
||||
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){
|
||||
tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${canTradeText}${avail}`;
|
||||
}
|
||||
@@ -1236,7 +1244,7 @@ if(fullMarginEl){
|
||||
fullMarginEl.addEventListener("change", function(){
|
||||
const marginEl = document.getElementById("order-margin");
|
||||
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){
|
||||
const key = data.key_monitor || null;
|
||||
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){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
@@ -180,8 +180,8 @@ function paintMeta(data){
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = fmt(key.lower,8);
|
||||
document.getElementById("m-upper").innerText = key.upper_display || fmt(key.upper,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-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
@@ -140,13 +140,13 @@ function addLine(price, title, color){
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
||||
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 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-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");
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@@ -142,15 +142,15 @@ function addLine(price, title, color){
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
||||
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 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-breakeven").innerText =
|
||||
(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");
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
+323
-72
@@ -238,10 +238,10 @@ def _wechat_trading_capital_text(fallback=None):
|
||||
except Exception:
|
||||
trading_capital = 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:
|
||||
try:
|
||||
return f"{round(float(fallback), 4)}U"
|
||||
return f"{round(float(fallback), 2)}U"
|
||||
except Exception:
|
||||
pass
|
||||
return "-"
|
||||
@@ -265,12 +265,12 @@ def build_wechat_close_message(
|
||||
ep = format_price_for_symbol(symbol, trigger_price)
|
||||
cp = format_price_for_symbol(symbol, current_price)
|
||||
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)
|
||||
try:
|
||||
if pnl_amount is not None:
|
||||
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:
|
||||
pnl_disp = "-"
|
||||
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):
|
||||
sl_fmt = format_price_for_symbol(symbol, new_sl)
|
||||
sl_fmt = format_wechat_scalar_2dp(new_sl)
|
||||
return "\n".join(
|
||||
[
|
||||
f"# 🛡️ {symbol} 保护位更新",
|
||||
@@ -975,6 +975,7 @@ def init_db():
|
||||
breakeven_armed INTEGER DEFAULT 0, breakeven_price REAL,
|
||||
notional_value REAL, position_ratio REAL, base_amount REAL,
|
||||
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,
|
||||
status TEXT DEFAULT "active")''')
|
||||
|
||||
@@ -1088,6 +1089,10 @@ def init_db():
|
||||
c.execute("ALTER TABLE order_monitors ADD COLUMN breakeven_enabled INTEGER DEFAULT 1")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute("ALTER TABLE order_monitors ADD COLUMN exchange_margin_usdt REAL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute("UPDATE order_monitors SET opened_at = datetime('now') WHERE opened_at IS NULL OR opened_at = ''")
|
||||
except: pass
|
||||
@@ -1351,19 +1356,19 @@ def _compute_period_metrics(trades):
|
||||
closed = len(trades)
|
||||
wins = 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_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]
|
||||
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_profit = round(max(pos_pnls), 4) if pos_pnls else None
|
||||
max_single_loss = round(min(neg_pnls), 2) if neg_pnls else None
|
||||
max_single_profit = round(max(pos_pnls), 2) if pos_pnls else None
|
||||
cum = peak = max_dd = 0.0
|
||||
for p, _, _ in trades:
|
||||
cum += p
|
||||
peak = max(peak, cum)
|
||||
max_dd = max(max_dd, peak - cum)
|
||||
max_dd = round(max_dd, 4)
|
||||
max_dd = round(max_dd, 2)
|
||||
streak = 0
|
||||
for p, _, _ in reversed(trades):
|
||||
if p < 0:
|
||||
@@ -1387,7 +1392,7 @@ def _compute_period_metrics(trades):
|
||||
else:
|
||||
run = 0
|
||||
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
|
||||
return {
|
||||
"closed_count": closed,
|
||||
@@ -1640,9 +1645,8 @@ def to_effective_trade_dict(row):
|
||||
return item
|
||||
|
||||
|
||||
def format_price_for_symbol(symbol, value):
|
||||
if value in (None, ""):
|
||||
return "-"
|
||||
def format_price_magnitude_fallback(value):
|
||||
"""无 markets 或解析失败时的价格展示兜底(按量级)。"""
|
||||
try:
|
||||
v = float(value)
|
||||
except Exception:
|
||||
@@ -1650,7 +1654,6 @@ def format_price_for_symbol(symbol, value):
|
||||
if v == 0:
|
||||
return "0"
|
||||
av = abs(v)
|
||||
# 根据币价量级动态精度:低价币保留更多小数,高价币减少噪音位数
|
||||
if av >= 10000:
|
||||
d = 2
|
||||
elif av >= 100:
|
||||
@@ -1667,6 +1670,88 @@ def format_price_for_symbol(symbol, value):
|
||||
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):
|
||||
if not minutes:
|
||||
return "0分钟"
|
||||
@@ -1866,7 +1951,7 @@ def enrich_order_item(raw_item, current_capital):
|
||||
notional = item.get("notional_value")
|
||||
ratio = item.get("position_ratio")
|
||||
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:
|
||||
ratio = round(margin / current_capital * 100, 2) if current_capital else 0
|
||||
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 "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到合约账户。"
|
||||
clean = re.sub(r"\s+", " ", msg).strip()
|
||||
return f"交易所下单失败:{clean}"
|
||||
@@ -2236,7 +2321,7 @@ def auto_transfer_once_per_day():
|
||||
if needed <= 0:
|
||||
conn.execute(
|
||||
"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.close()
|
||||
@@ -2244,12 +2329,12 @@ def auto_transfer_once_per_day():
|
||||
if from_balance is not None and from_balance < needed:
|
||||
conn.execute(
|
||||
"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.close()
|
||||
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()}"
|
||||
)
|
||||
return
|
||||
@@ -2263,13 +2348,13 @@ def auto_transfer_once_per_day():
|
||||
conn.close()
|
||||
if ok:
|
||||
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"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
|
||||
)
|
||||
else:
|
||||
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()}"
|
||||
)
|
||||
|
||||
@@ -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"))
|
||||
out = {}
|
||||
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:
|
||||
out["notional"] = round(notional, 4)
|
||||
out["notional"] = round(notional, 2)
|
||||
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:
|
||||
out["mark_price"] = round(mark, 8)
|
||||
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()
|
||||
if not exchange_private_api_configured() or not exchange_symbol:
|
||||
return None
|
||||
@@ -2746,7 +2831,72 @@ def get_live_position_exchange_metrics(exchange_symbol, direction):
|
||||
except Exception:
|
||||
return None
|
||||
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):
|
||||
@@ -3055,7 +3205,7 @@ def reconcile_external_closes(conn, days=None):
|
||||
stop_loss=r["stop_loss"],
|
||||
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
|
||||
take_profit=r["take_profit"],
|
||||
margin_capital=r["margin_capital"],
|
||||
margin_capital=margin_capital_for_trade_record(r),
|
||||
leverage=r["leverage"],
|
||||
pnl_amount=pnl_amount,
|
||||
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"]
|
||||
margin_capital = r["margin_capital"] or DAILY_START_CAPITAL
|
||||
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()
|
||||
p = get_price(sym)
|
||||
if not p: continue
|
||||
@@ -3466,6 +3631,7 @@ def check_order_monitors():
|
||||
direction == "long" and new_sl > float(stop_loss)
|
||||
)
|
||||
if should_move:
|
||||
new_sl = round_price_to_exchange(resolve_monitor_exchange_symbol(r), new_sl)
|
||||
conn.execute(
|
||||
"UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?",
|
||||
(new_sl, new_sl, pid),
|
||||
@@ -3585,7 +3751,7 @@ def check_order_monitors():
|
||||
stop_loss=stop_loss,
|
||||
initial_stop_loss=r["initial_stop_loss"] or stop_loss,
|
||||
take_profit=take_profit,
|
||||
margin_capital=margin_capital,
|
||||
margin_capital=margin_capital_for_trade_record(trade_basis_row),
|
||||
leverage=leverage,
|
||||
pnl_amount=pnl_amount,
|
||||
hold_seconds=hold_seconds,
|
||||
@@ -3655,7 +3821,7 @@ def check_order_monitors():
|
||||
stop_loss=stop_loss,
|
||||
initial_stop_loss=r["initial_stop_loss"] or stop_loss,
|
||||
take_profit=take_profit,
|
||||
margin_capital=margin_capital,
|
||||
margin_capital=margin_capital_for_trade_record(trade_basis_row),
|
||||
leverage=leverage,
|
||||
pnl_amount=pnl_amount,
|
||||
hold_seconds=hold_seconds,
|
||||
@@ -3720,7 +3886,7 @@ def force_close_before_reset():
|
||||
stop_loss=r["stop_loss"],
|
||||
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
|
||||
take_profit=r["take_profit"],
|
||||
margin_capital=margin_capital,
|
||||
margin_capital=margin_capital_for_trade_record(r),
|
||||
leverage=leverage,
|
||||
pnl_amount=pnl_amount,
|
||||
hold_seconds=hold_seconds,
|
||||
@@ -3853,9 +4019,9 @@ def render_main_page(page="trade"):
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals()
|
||||
# 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。
|
||||
funding_usdt = round(funding_capital, 4) 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)
|
||||
recommended_capital = get_recommended_capital(current_capital)
|
||||
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
|
||||
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
||||
recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
|
||||
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()
|
||||
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,
|
||||
occupied_miss_total=occupied_miss_total,
|
||||
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_other_value=ENTRY_REASON_OTHER,
|
||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
@@ -3955,9 +4123,9 @@ def api_account_snapshot():
|
||||
session_row = ensure_session(conn, trading_day)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals(force=True)
|
||||
funding_usdt = round(funding_capital, 4) 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)
|
||||
recommended_capital = get_recommended_capital(current_capital)
|
||||
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
|
||||
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
||||
recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
|
||||
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
|
||||
conn.close()
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count == 0
|
||||
@@ -3965,7 +4133,7 @@ def api_account_snapshot():
|
||||
return jsonify({
|
||||
"funding_usdt": funding_usdt,
|
||||
"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,
|
||||
"active_count": active_count,
|
||||
"can_trade": can_trade,
|
||||
@@ -3983,6 +4151,11 @@ def api_price_snapshot():
|
||||
).fetchall()
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
symbol_set = set()
|
||||
for r in key_rows:
|
||||
symbol_set.add(r["symbol"])
|
||||
@@ -4044,10 +4217,16 @@ def api_price_snapshot():
|
||||
)
|
||||
except Exception:
|
||||
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({
|
||||
"id": r["id"],
|
||||
"symbol": r["symbol"],
|
||||
"price": round(price, 6),
|
||||
"price": price_num,
|
||||
"price_display": px_disp,
|
||||
"upper_diff": upper_diff,
|
||||
"upper_pct": upper_pct,
|
||||
"lower_diff": lower_diff,
|
||||
@@ -4075,11 +4254,10 @@ def api_price_snapshot():
|
||||
payload = {
|
||||
"id": r["id"],
|
||||
"symbol": r["symbol"],
|
||||
"price": round(price, 6),
|
||||
"float_pnl": round(pnl, 6),
|
||||
"float_pnl": round(pnl, 2),
|
||||
"float_pct": pnl_pct,
|
||||
"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_notional": None,
|
||||
"exchange_mark_price": None,
|
||||
@@ -4093,12 +4271,24 @@ def api_price_snapshot():
|
||||
if ex_metrics.get("mark_price") is not None:
|
||||
payload["exchange_mark_price"] = ex_metrics["mark_price"]
|
||||
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"
|
||||
denom = ex_metrics.get("initial_margin") or margin
|
||||
payload["float_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)
|
||||
|
||||
return jsonify({
|
||||
@@ -4149,7 +4339,7 @@ def api_order_defaults():
|
||||
"exchange_symbol": exchange_symbol,
|
||||
"direction": direction,
|
||||
"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)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
_, 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()
|
||||
conn.close()
|
||||
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)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
_, 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()
|
||||
conn.close()
|
||||
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_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0
|
||||
|
||||
sym = order_item["symbol"]
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"timeframe": timeframe,
|
||||
"limit": limit,
|
||||
"order": {
|
||||
"id": order_item["id"],
|
||||
"symbol": order_item["symbol"],
|
||||
"symbol": sym,
|
||||
"direction": order_item.get("direction") or "long",
|
||||
"trigger_price": order_item.get("trigger_price"),
|
||||
"stop_loss": order_item.get("stop_loss"),
|
||||
"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"),
|
||||
"leverage": order_item.get("leverage"),
|
||||
"position_ratio": order_item.get("position_ratio"),
|
||||
"rr_ratio": order_item.get("rr_ratio"),
|
||||
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
|
||||
"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,
|
||||
},
|
||||
"candles": candles,
|
||||
@@ -4386,8 +4581,15 @@ def add_key():
|
||||
flash(f"{symbol} 当前日成交量排名为 {rank}/{total},不在前30,已拒绝添加关键位")
|
||||
return redirect("/")
|
||||
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 (?,?,?,?,?)",
|
||||
(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.close()
|
||||
flash(f"添加成功({symbol} 日成交量排名 {rank}/{total})")
|
||||
@@ -4414,14 +4616,19 @@ def add_order():
|
||||
tgt_raw = parse_positive_float(d.get("tgt"))
|
||||
except Exception:
|
||||
tp_raw = sl_raw = tgt_raw = None
|
||||
ex_miss = normalize_exchange_symbol(symbol)
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
except Exception:
|
||||
pass
|
||||
insert_trade_record(
|
||||
conn,
|
||||
symbol=symbol,
|
||||
monitor_type="下单监控",
|
||||
direction=direction if direction in ("long", "short") else "long",
|
||||
trigger_price=tp_raw or 0,
|
||||
stop_loss=sl_raw or 0,
|
||||
take_profit=tgt_raw or 0,
|
||||
trigger_price=round_price_to_exchange(ex_miss, tp_raw) if tp_raw else 0,
|
||||
stop_loss=round_price_to_exchange(ex_miss, sl_raw) if sl_raw else 0,
|
||||
take_profit=round_price_to_exchange(ex_miss, tgt_raw) if tgt_raw else 0,
|
||||
result="错过",
|
||||
miss_reason="持仓占用:一次只能持有一个仓位",
|
||||
opened_at=app_now_str(),
|
||||
@@ -4467,6 +4674,13 @@ def add_order():
|
||||
conn.close()
|
||||
flash("获取交易所实时价格失败,请稍后重试")
|
||||
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()
|
||||
if sltp_mode not in ("price", "pct"):
|
||||
sltp_mode = "price"
|
||||
@@ -4500,6 +4714,12 @@ def add_order():
|
||||
conn.close()
|
||||
flash("价格参数必须大于0")
|
||||
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)
|
||||
if risk_fraction is None:
|
||||
conn.close()
|
||||
@@ -4517,7 +4737,7 @@ def add_order():
|
||||
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
|
||||
if margin_capital > max_margin:
|
||||
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("/")
|
||||
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
|
||||
try:
|
||||
@@ -4533,6 +4753,10 @@ def add_order():
|
||||
flash(friendly_exchange_error(e, available_usdt=available_usdt))
|
||||
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")
|
||||
opened_at_bj = app_now_str()
|
||||
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
|
||||
risk_amount_final = calc_risk_amount_from_plan(direction, trigger_price, stop_loss, margin_capital, leverage) or risk_amount
|
||||
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:
|
||||
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
|
||||
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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
@@ -4557,6 +4782,8 @@ def add_order():
|
||||
)
|
||||
conn.commit()
|
||||
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(
|
||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
||||
(trading_day,),
|
||||
@@ -4632,9 +4859,9 @@ def add_order():
|
||||
|
||||
_, trading_capital_after = get_exchange_capitals(force=True)
|
||||
account_base_display = (
|
||||
round(float(trading_capital_after), 4)
|
||||
round(float(trading_capital_after), 2)
|
||||
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()
|
||||
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 "-"
|
||||
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):
|
||||
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"
|
||||
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)
|
||||
be_wx = format_price_for_symbol(symbol, breakeven_price)
|
||||
style_zh = "Swing 波段" if trade_style == "swing" else "Trend 趋势"
|
||||
@@ -4660,13 +4887,13 @@ def add_order():
|
||||
"🧾 订单基础信息",
|
||||
f"🔖 交易所订单 ID:{open_order_id}",
|
||||
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"合约杠杆:{leverage} 倍",
|
||||
f"名义仓位:{notional_value} USDT",
|
||||
f"名义仓位:{format_wechat_scalar_2dp(notional_value)} USDT",
|
||||
f"仓位占比:{position_ratio}%",
|
||||
f"合约张数:{amount} 张",
|
||||
f"合约张数:{format_wechat_scalar_2dp(amount)} 张",
|
||||
f"折算标的:{base_amount} {journal_coin_from_symbol(symbol)}",
|
||||
"🎯 价位 & 盈亏比",
|
||||
f"开仓成交价:{ep_wx}",
|
||||
@@ -4683,8 +4910,8 @@ def add_order():
|
||||
send_wechat_msg("\n".join(wx_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"计划RR {planned_rr if planned_rr is not None else '-'};已在交易所挂条件止盈/止损委托(非仓位绑定型)",
|
||||
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 {format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else '-'};已在交易所挂条件止盈/止损委托(非仓位绑定型)",
|
||||
f"本交易日累计开仓:{opens_today_after}",
|
||||
]
|
||||
if chart_url:
|
||||
@@ -4694,7 +4921,7 @@ def add_order():
|
||||
if opens_today_before < DAILY_OPEN_ALERT_THRESHOLD <= opens_today_after:
|
||||
advice = ai_short_advice(
|
||||
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"用户自述“上头了”。请给克制提醒。"
|
||||
)
|
||||
if advice:
|
||||
@@ -4931,6 +5158,7 @@ def del_order(id):
|
||||
cancel_gate_swap_trigger_orders(row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"]))
|
||||
session_date = row["session_date"] or get_trading_day()
|
||||
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(
|
||||
conn,
|
||||
symbol=row["symbol"],
|
||||
@@ -4940,7 +5168,7 @@ def del_order(id):
|
||||
stop_loss=row["stop_loss"],
|
||||
initial_stop_loss=row["initial_stop_loss"] or row["stop_loss"],
|
||||
take_profit=row["take_profit"],
|
||||
margin_capital=row["margin_capital"],
|
||||
margin_capital=margin_capital_for_trade_record(row_snap),
|
||||
leverage=row["leverage"],
|
||||
pnl_amount=pnl_amount,
|
||||
hold_seconds=hold_seconds,
|
||||
@@ -4985,6 +5213,7 @@ def del_order(id):
|
||||
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
|
||||
session_date = row["session_date"] or get_trading_day(closed_at_dt)
|
||||
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(
|
||||
conn,
|
||||
symbol=row["symbol"],
|
||||
@@ -4994,7 +5223,7 @@ def del_order(id):
|
||||
stop_loss=row["stop_loss"],
|
||||
initial_stop_loss=row["initial_stop_loss"] or row["stop_loss"],
|
||||
take_profit=row["take_profit"],
|
||||
margin_capital=row["margin_capital"],
|
||||
margin_capital=margin_capital_for_trade_record(row_snap),
|
||||
leverage=row["leverage"],
|
||||
pnl_amount=pnl_amount,
|
||||
hold_seconds=hold_seconds,
|
||||
@@ -5025,15 +5254,28 @@ def del_order(id):
|
||||
def add_miss():
|
||||
d = request.form
|
||||
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()
|
||||
insert_trade_record(
|
||||
conn,
|
||||
symbol=d["symbol"],
|
||||
symbol=sym_in,
|
||||
monitor_type=d["type"],
|
||||
direction=direction,
|
||||
trigger_price=d["tp"],
|
||||
stop_loss=d["sl"],
|
||||
take_profit=d["tgt"],
|
||||
trigger_price=tp_px,
|
||||
stop_loss=sl_px,
|
||||
take_profit=tgt_px,
|
||||
result="错过",
|
||||
miss_reason=d["reason"],
|
||||
opened_at=app_now_str(),
|
||||
@@ -5379,11 +5621,20 @@ def api_trade_record_review_update():
|
||||
reviewed_entry_reason_update = s or None
|
||||
|
||||
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:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": "记录不存在"}), 404
|
||||
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)
|
||||
base_params = [
|
||||
reviewed_opened_at,
|
||||
|
||||
@@ -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.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">净盈亏(U)</div><div class="value">{{ 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">{% 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_profit is not none %}{{ 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">{{ signed_usdt_fmt(s.net_pnl_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 %}{{ 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 %}{{ usdt_fmt(s.max_single_profit) }}{% else %}-{% endif %}</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.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>
|
||||
{% 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">{{ 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">资金账户(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" 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 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><strong>{{ k.symbol }}</strong> | {{ k.monitor_type }} | <span class="badge direction">{{ '做多' if k.direction == 'long' else '做空' }}</span></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 }}
|
||||
| 现价:<span id="key-price-{{ 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 }}
|
||||
<button type="button" class="table-del" style="margin-left:8px" onclick="deleteKeyHistory({{ h.id }})">删除</button>
|
||||
</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 %}
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -252,7 +252,7 @@
|
||||
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
||||
</div>
|
||||
<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>
|
||||
<form action="/manual_transfer" method="post" class="form-row">
|
||||
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
|
||||
@@ -300,14 +300,14 @@
|
||||
<div class="list-item">
|
||||
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
|
||||
<div>
|
||||
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ o.risk_amount or '-' }}U
|
||||
| {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ o.breakeven_price or '-' }}{% else %}移动保本:关{% endif %}
|
||||
风格:{{ 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→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||
<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-price-{{ 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 }}%
|
||||
</div>
|
||||
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||
@@ -335,18 +335,18 @@
|
||||
<td>{{ r.symbol }}</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>{{ 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 tp_show = r.effective_take_profit or r.take_profit %}
|
||||
<td>{{ price_fmt(r.symbol, stop_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.effective_hold_minutes or 0 }}</td>
|
||||
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
|
||||
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
|
||||
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
|
||||
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ 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>
|
||||
{% set effective_result = r.effective_result %}
|
||||
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
|
||||
@@ -602,7 +602,7 @@ function openJournalDetail(id){
|
||||
`开仓时间:${o.open_datetime || "-"}`,
|
||||
`平仓时间:${o.close_datetime || "-"}`,
|
||||
`持仓时长:${o.hold_duration || "-"}`,
|
||||
`盈亏:${o.pnl || "-"}U`,
|
||||
`盈亏:${formatJournalPnlUi(o.pnl)}U`,
|
||||
`开仓类型:${o.entry_reason || "无"}`,
|
||||
`平仓/离场:${formatJournalExitOneLine(o)}`,
|
||||
`预期RR:${o.expect_rr || "-"}`,
|
||||
@@ -685,7 +685,7 @@ function editTradeRecordReview(t){
|
||||
if(stopLoss === null) return;
|
||||
const takeProfit = prompt("止盈价格(核对后用于统计)", String(t.take_profit ?? ""));
|
||||
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;
|
||||
const result = prompt("结果(止盈/止损/保本止盈/移动止盈/手动平仓)", String(t.result || ""));
|
||||
if(result === null) return;
|
||||
@@ -751,7 +751,7 @@ function loadJournals(){
|
||||
journalCache[o.id] = o;
|
||||
const moodTags = (o.mood_issues || []).join(",") || "无";
|
||||
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>心态标签:${moodTags}</div>
|
||||
<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("coin", coinFromSymbol(t.symbol));
|
||||
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);
|
||||
setJournalField("expect_rr", rr);
|
||||
let realRr = rr;
|
||||
@@ -919,7 +919,7 @@ function fillJournalFromTrade(t){
|
||||
}
|
||||
setJournalField("real_rr", realRr);
|
||||
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");
|
||||
if(entryHint){ entryHint.value = t.trigger_price || ""; }
|
||||
const stopHint = document.getElementById("stop-loss-hint");
|
||||
@@ -1098,6 +1098,30 @@ function formatSigned(v, digits=4){
|
||||
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){
|
||||
if(!el) return;
|
||||
const prev = lastPriceMap[key];
|
||||
@@ -1121,7 +1145,7 @@ function refreshPriceSnapshot(){
|
||||
(data.key_prices || []).forEach(k=>{
|
||||
const pEl = document.getElementById(`key-price-${k.id}`);
|
||||
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));
|
||||
}
|
||||
const upEl = document.getElementById(`key-up-diff-${k.id}`);
|
||||
@@ -1145,18 +1169,19 @@ function refreshPriceSnapshot(){
|
||||
(data.order_prices || []).forEach(o=>{
|
||||
const pEl = document.getElementById(`order-price-${o.id}`);
|
||||
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 px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
|
||||
const decimals = hasMark ? 8 : 6;
|
||||
pEl.innerText = px.toFixed(decimals);
|
||||
paintPriceTrend(pEl, `o-${o.id}`, px);
|
||||
pEl.innerText = pxd !== null ? pxd : px.toFixed(decimals);
|
||||
paintPriceTrend(pEl, `o-${o.id}`, pxd !== null ? Number(pxd) : px);
|
||||
}
|
||||
const exM = document.getElementById(`order-ex-margin-${o.id}`);
|
||||
if(exM){
|
||||
const mv = o.exchange_initial_margin;
|
||||
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
|
||||
if(!Number.isNaN(mn)){
|
||||
exM.innerText = `${mn.toFixed(4)}U`;
|
||||
exM.innerText = `${formatUsdt2(mn)}U`;
|
||||
} else {
|
||||
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
|
||||
exM.innerText = (prc === 0) ? "无仓数据" : "-";
|
||||
@@ -1164,7 +1189,7 @@ function refreshPriceSnapshot(){
|
||||
}
|
||||
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
|
||||
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");
|
||||
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
|
||||
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 marginEl = document.getElementById("order-margin");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1209,18 +1234,18 @@ function refreshAccountSnapshot(){
|
||||
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
|
||||
if (typeof data.funding_usdt !== "undefined") {
|
||||
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") {
|
||||
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) {
|
||||
latestAvailableUsdt = Number(data.available_trading_usdt);
|
||||
}
|
||||
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00)";
|
||||
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){
|
||||
tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${canTradeText}${avail}`;
|
||||
}
|
||||
@@ -1236,7 +1261,7 @@ if(fullMarginEl){
|
||||
fullMarginEl.addEventListener("change", function(){
|
||||
const marginEl = document.getElementById("order-margin");
|
||||
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){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
||||
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 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-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");
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@@ -142,15 +142,15 @@ function addLine(price, title, color){
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
||||
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 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-breakeven").innerText =
|
||||
(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");
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@ BTC_LEVERAGE=10
|
||||
ALT_LEVERAGE=5
|
||||
# 交易日重置小时(北京时间)
|
||||
TRADING_DAY_RESET_HOUR=8
|
||||
# 整点前禁止新开仓:true=启用(默认),false=关闭(仍可保留 8 点作为交易日划分)
|
||||
TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
|
||||
# Gate 平仓历史:同步「趋势回调」交易记录与交易所已实现盈亏(北京日期 00:00 起,与 APP_TIMEZONE 一致);留空则从近 90 天拉取
|
||||
# EXCHANGE_POSITION_SYNC_FROM_BJ=2026-05-14
|
||||
# EXCHANGE_POSITION_HISTORY_LIMIT=200
|
||||
|
||||
# 是否开启 Gate 实盘下单(false=只做本地流程,true=真实下单)
|
||||
LIVE_TRADING_ENABLED=true
|
||||
|
||||
+514
-15
@@ -93,6 +93,11 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED = os.getenv(
|
||||
"TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "true"
|
||||
).lower() in ("1", "true", "yes", "on")
|
||||
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():
|
||||
@@ -1182,6 +1187,26 @@ def init_db():
|
||||
try:
|
||||
c.execute("ALTER TABLE trade_records ADD COLUMN reviewed_entry_reason TEXT")
|
||||
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:
|
||||
c.execute("ALTER TABLE journal_entries ADD COLUMN mood_ai_score INTEGER")
|
||||
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.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"))
|
||||
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 ""
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
if value in (None, ""):
|
||||
return "-"
|
||||
@@ -1722,8 +1825,14 @@ def format_price_for_symbol(symbol, value):
|
||||
return str(value)
|
||||
if v == 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)
|
||||
# 根据币价量级动态精度:低价币保留更多小数,高价币减少噪音位数
|
||||
if av >= 10000:
|
||||
d = 2
|
||||
elif av >= 100:
|
||||
@@ -1740,6 +1849,70 @@ def format_price_for_symbol(symbol, value):
|
||||
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):
|
||||
if not minutes:
|
||||
return "0分钟"
|
||||
@@ -1887,6 +2060,7 @@ def insert_trade_record(
|
||||
closed_at=None,
|
||||
closed_at_ms=None,
|
||||
exchange_trade_id=None,
|
||||
trend_plan_id=None,
|
||||
):
|
||||
hold_minutes = calc_hold_minutes(hold_seconds)
|
||||
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)
|
||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||
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,
|
||||
margin_capital, leverage, pnl_amount, hold_seconds,
|
||||
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
|
||||
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id
|
||||
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):
|
||||
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,))
|
||||
|
||||
|
||||
@@ -3030,6 +3213,236 @@ def get_live_position_exchange_metrics(exchange_symbol, direction):
|
||||
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):
|
||||
if not opened_at_str:
|
||||
return None
|
||||
@@ -3795,6 +4208,7 @@ def _trend_finalize_plan(conn, row, result_label, exit_price, closed_at=None):
|
||||
result=res,
|
||||
opened_at=opened_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")
|
||||
conn.execute(
|
||||
@@ -4417,9 +4831,9 @@ def render_main_page(page="trade"):
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals()
|
||||
# 资金账户:仅展示交易所读取结果(含 0)。不可用 TOTAL_CAPITAL 兜底,否则会与实盘不符。
|
||||
funding_usdt = round(funding_capital, 4) 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)
|
||||
recommended_capital = get_recommended_capital(current_capital)
|
||||
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
|
||||
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
||||
recommended_capital = round(get_recommended_capital(current_capital), 2)
|
||||
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()
|
||||
stats_bundle = compute_stats_bundle(conn, trading_day, now)
|
||||
@@ -4427,6 +4841,11 @@ def render_main_page(page="trade"):
|
||||
order_list = []
|
||||
for o in raw_order_list:
|
||||
order_list.append(enrich_order_item(row_to_dict(o), current_capital))
|
||||
if 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()
|
||||
records = [to_effective_trade_dict(r) for r in raw_records]
|
||||
total = len(records)
|
||||
@@ -4443,9 +4862,27 @@ def render_main_page(page="trade"):
|
||||
trend_active = conn.execute(
|
||||
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
|
||||
).fetchone()[0]
|
||||
trend_plans = conn.execute(
|
||||
trend_plans_raw = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC"
|
||||
).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 = (
|
||||
trading_day_reset_allows_new_open(now)
|
||||
and active_count == 0
|
||||
@@ -4508,6 +4945,9 @@ def render_main_page(page="trade"):
|
||||
active_count=active_count,
|
||||
can_trade=can_trade,
|
||||
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_preview_ttl=TREND_PULLBACK_PREVIEW_TTL_SECONDS,
|
||||
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,
|
||||
focus_key_id=(key_list[0]["id"] if key_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,
|
||||
risk_percent=RISK_PERCENT,
|
||||
breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
||||
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
||||
occupied_miss_total=occupied_miss_total,
|
||||
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_other_value=ENTRY_REASON_OTHER,
|
||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
@@ -4555,6 +4997,25 @@ def stats_page():
|
||||
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")
|
||||
@login_required
|
||||
def api_account_snapshot():
|
||||
@@ -4564,9 +5025,9 @@ def api_account_snapshot():
|
||||
session_row = ensure_session(conn, trading_day)
|
||||
local_current_capital = float(session_row["current_capital"])
|
||||
funding_capital, trading_capital = get_exchange_capitals(force=True)
|
||||
funding_usdt = round(funding_capital, 4) 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)
|
||||
recommended_capital = get_recommended_capital(current_capital)
|
||||
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
|
||||
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
||||
recommended_capital = round(get_recommended_capital(current_capital), 2)
|
||||
active_count = conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0]
|
||||
conn.close()
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count == 0
|
||||
@@ -4574,7 +5035,7 @@ def api_account_snapshot():
|
||||
return jsonify({
|
||||
"funding_usdt": funding_usdt,
|
||||
"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,
|
||||
"active_count": active_count,
|
||||
"can_trade": can_trade,
|
||||
@@ -5366,6 +5827,7 @@ def preview_trend_pullback():
|
||||
created,
|
||||
),
|
||||
)
|
||||
insert_trend_preview_snapshot(conn, pid, created, exp_ms, payload)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash(f"预览已生成,有效期 {TREND_PULLBACK_PREVIEW_TTL_SECONDS} 秒,请核对后点击「确认执行」。")
|
||||
@@ -5444,7 +5906,7 @@ def execute_trend_pullback():
|
||||
trading_day = get_trading_day(now)
|
||||
opened_at = app_now_str()
|
||||
opened_ms = _to_ms_with_fallback(None, opened_at)
|
||||
conn.execute(
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO trend_pullback_plans (
|
||||
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,
|
||||
@@ -5481,11 +5943,16 @@ def execute_trend_pullback():
|
||||
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.commit()
|
||||
conn.close()
|
||||
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} 档;已挂交易所止损,止盈由程序监控。"
|
||||
)
|
||||
return redirect(url_for("trade_page"))
|
||||
@@ -5497,6 +5964,10 @@ def cancel_trend_pullback_preview():
|
||||
pid = (request.form.get("preview_id") or "").strip()
|
||||
conn = get_db()
|
||||
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.commit()
|
||||
conn.close()
|
||||
@@ -5546,6 +6017,28 @@ def stop_trend_pullback(pid):
|
||||
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"])
|
||||
@login_required
|
||||
def delete_key_monitor(kid):
|
||||
@@ -5622,7 +6115,8 @@ def export_trade_records():
|
||||
rows = conn.execute(
|
||||
"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,"
|
||||
"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()
|
||||
conn.close()
|
||||
head_base = [
|
||||
@@ -5645,6 +6139,11 @@ def export_trade_records():
|
||||
"entry_reason",
|
||||
"reviewed_entry_reason",
|
||||
"created_at",
|
||||
"trend_plan_id",
|
||||
"exchange_realized_pnl",
|
||||
"exchange_opened_at",
|
||||
"exchange_closed_at",
|
||||
"exchange_sync_key",
|
||||
]
|
||||
head = head_base + ["开仓类型"]
|
||||
data = []
|
||||
|
||||
@@ -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.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">净盈亏(U)</div><div class="value">{{ 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">{% 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_profit is not none %}{{ 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.net_pnl_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 %}{{ 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 %}{{ money_fmt(s.max_single_profit) }}{% else %}-{% endif %}</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.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>
|
||||
{% endmacro %}
|
||||
@@ -149,12 +149,13 @@
|
||||
<div class="top-nav">
|
||||
<a href="/trade" class="{% if page == 'trade' %}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>
|
||||
</div>
|
||||
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
||||
|
||||
<div class="export-bar">
|
||||
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSV,UTF-8;交易记录含开仓类型列):</span>
|
||||
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSV,UTF-8;交易记录含开仓类型列及交易所对齐字段):</span>
|
||||
<a href="/export/trade_records">交易记录</a>
|
||||
</div>
|
||||
<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">{{ 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">资金账户(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" 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 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 }}%
|
||||
</div>
|
||||
<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>
|
||||
<form action="/manual_transfer" method="post" class="form-row">
|
||||
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
|
||||
@@ -236,14 +237,14 @@
|
||||
<div class="list-item">
|
||||
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
|
||||
<div>
|
||||
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ o.risk_amount or '-' }}U
|
||||
| {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ o.breakeven_price or '-' }}{% else %}移动保本:关{% endif %}
|
||||
风格:{{ 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→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||
<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-price-{{ 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 }}%
|
||||
</div>
|
||||
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||
@@ -282,15 +283,15 @@
|
||||
</div>
|
||||
<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 |
|
||||
预览可用快照 <strong>{{ trend_preview.snapshot_available_usdt }}</strong> U | 参考价 {{ 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>
|
||||
止损 {{ trend_preview.stop_loss }} | 补仓上沿 {{ trend_preview.add_upper }} | 止盈 {{ trend_preview.take_profit }} | 风险比例 {{ trend_preview.risk_percent }}%
|
||||
预览可用快照 <strong>{{ money_fmt(trend_preview.snapshot_available_usdt) }}</strong> U | 参考价 {{ price_fmt(trend_preview.symbol, trend_preview.live_price_ref) }} |
|
||||
计划保证金≈{{ 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>
|
||||
止损 {{ 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 class="table-wrap" style="margin-bottom:10px">
|
||||
<table>
|
||||
<tr><th>#</th><th>补仓触发价</th><th>该档张数</th></tr>
|
||||
{% 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 %}
|
||||
</table>
|
||||
</div>
|
||||
@@ -331,9 +332,10 @@
|
||||
<div class="list-item">
|
||||
<div><strong>#{{ t.id }} {{ t.symbol }}</strong> | {{ '做多' if t.direction == 'long' else '做空' }} | {{ t.leverage }}x</div>
|
||||
<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 }}
|
||||
<br>止损:{{ t.stop_loss }} 补仓上沿:{{ t.add_upper }} 止盈:{{ t.take_profit }}
|
||||
<br>均价:{{ t.avg_entry_price }} 已补仓:{{ t.legs_done }}/{{ 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>止损:{{ price_fmt(t.symbol, t.stop_loss) }} 补仓上沿:{{ price_fmt(t.symbol, t.add_upper) }} 止盈:{{ price_fmt(t.symbol, t.take_profit) }}
|
||||
<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>
|
||||
<a href="/stop_trend_pullback/{{ t.id }}" class="btn-del" onclick="return confirm('结束计划:市价平仓并撤掉该合约全部挂单,确定?')">结束计划</a>
|
||||
</div>
|
||||
@@ -349,25 +351,24 @@
|
||||
<h2>交易记录</h2>
|
||||
<div class="table-wrap">
|
||||
<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 %}
|
||||
<tr id="trade-row-{{ r.id }}">
|
||||
{% set pnl_val = (r.pnl_amount or 0)|float %}
|
||||
<td>{{ r.symbol }}</td>
|
||||
<td>{{ r.monitor_type }}</td>
|
||||
<td><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 tp_show = r.effective_take_profit or r.take_profit %}
|
||||
<td>{{ price_fmt(r.symbol, stop_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.effective_hold_minutes or 0 }}</td>
|
||||
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
|
||||
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
|
||||
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
|
||||
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ r.effective_pnl_amount or 0 }}</span></td>
|
||||
<td>{{ r.display_opened_at }}</td>
|
||||
<td>{{ r.display_closed_at }}</td>
|
||||
{% 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 '') }}">{{ 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>
|
||||
{% set effective_result = r.effective_result %}
|
||||
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
|
||||
@@ -407,6 +408,64 @@
|
||||
{% endif %}
|
||||
</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' %}
|
||||
<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">
|
||||
@@ -484,6 +543,41 @@ function validateJournalEntryReason(){
|
||||
function showImage(src){document.getElementById("bigImg").src=src;document.getElementById("imgModal").style.display="flex";}
|
||||
function closeModal(){document.getElementById("imgModal").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();}}
|
||||
|
||||
const journalCache = {};
|
||||
@@ -1060,7 +1154,7 @@ function refreshPriceSnapshot(){
|
||||
const mv = o.exchange_initial_margin;
|
||||
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
|
||||
if(!Number.isNaN(mn)){
|
||||
exM.innerText = `${mn.toFixed(4)}U`;
|
||||
exM.innerText = `${mn.toFixed(2)}U`;
|
||||
} else {
|
||||
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
|
||||
exM.innerText = (prc === 0) ? "无仓数据" : "-";
|
||||
@@ -1068,7 +1162,7 @@ function refreshPriceSnapshot(){
|
||||
}
|
||||
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
|
||||
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");
|
||||
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
|
||||
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 marginEl = document.getElementById("order-margin");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1113,18 +1207,18 @@ function refreshAccountSnapshot(){
|
||||
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
|
||||
if (typeof data.funding_usdt !== "undefined") {
|
||||
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") {
|
||||
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) {
|
||||
latestAvailableUsdt = Number(data.available_trading_usdt);
|
||||
}
|
||||
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00)";
|
||||
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){
|
||||
tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${canTradeText}${avail}`;
|
||||
}
|
||||
@@ -1140,7 +1234,7 @@ if(fullMarginEl){
|
||||
fullMarginEl.addEventListener("change", function(){
|
||||
const marginEl = document.getElementById("order-margin");
|
||||
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` 中对应记录;过期记录会在新预览或页面加载时清理。
|
||||
|
||||
### 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. 与「机器人下单监控」的差异
|
||||
@@ -82,10 +100,16 @@
|
||||
| `MONITOR_POLL_SECONDS` | 监控轮询间隔(秒) | `3` |
|
||||
| `LIVE_TRADING_ENABLED` | 是否允许真实下单 | `false` |
|
||||
| `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. 数据库
|
||||
|
||||
- **`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`。
|
||||
|
||||
@@ -150,6 +150,24 @@ TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT=5
|
||||
|
||||
- **生成预览**与**确认执行**时都会读取 **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(验证)
|
||||
|
||||
Reference in New Issue
Block a user