修改币种精度
This commit is contained in:
+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 = []
|
||||
|
||||
Reference in New Issue
Block a user