修改交易记录问题
This commit is contained in:
+220
-66
@@ -37,12 +37,21 @@ if _REPO_ROOT not in sys.path:
|
|||||||
from fib_key_monitor_lib import (
|
from fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
calc_fib_plan,
|
calc_fib_plan,
|
||||||
|
entry_reason_from_key_signal,
|
||||||
fib_invalidate_by_mark,
|
fib_invalidate_by_mark,
|
||||||
fib_ratio_from_type,
|
fib_ratio_from_type,
|
||||||
is_fib_key_monitor_type,
|
is_fib_key_monitor_type,
|
||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
stored_key_signal_type,
|
||||||
)
|
)
|
||||||
|
from history_window_lib import (
|
||||||
|
PRESET_CUSTOM,
|
||||||
|
PRESET_UTC_LAST24H,
|
||||||
|
PRESET_UTC_LAST7D,
|
||||||
|
PRESET_UTC_TODAY,
|
||||||
|
resolve_window,
|
||||||
|
utc_window_to_bj_sql_strings,
|
||||||
|
)
|
||||||
|
|
||||||
def load_env_file(path):
|
def load_env_file(path):
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
@@ -725,6 +734,41 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255),
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def _timeframe_period_ms(tf):
|
||||||
|
s = (tf or "").strip().lower()
|
||||||
|
if s.endswith("m"):
|
||||||
|
try:
|
||||||
|
return int(s[:-1]) * 60 * 1000
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if s.endswith("h"):
|
||||||
|
try:
|
||||||
|
return int(s[:-1]) * 3600 * 1000
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if s.endswith("d"):
|
||||||
|
try:
|
||||||
|
return int(s[:-1]) * 86400 * 1000
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return 300000
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_ohlcv_ending_at(exchange_symbol, timeframe, limit, end_ts_ms):
|
||||||
|
"""以 end_ts_ms 为终点向前取 K 线(无 end 则拉最近 limit 根)。"""
|
||||||
|
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
||||||
|
if not end_ts_ms:
|
||||||
|
return exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
|
||||||
|
period = _timeframe_period_ms(timeframe)
|
||||||
|
since = int(end_ts_ms) - period * (lim + 5)
|
||||||
|
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, since=max(0, since), limit=lim + 10)
|
||||||
|
rows = _ohlcv_to_rows(ohlcv)
|
||||||
|
filtered = [r for r in rows if int(r[0]) <= int(end_ts_ms)]
|
||||||
|
if len(filtered) >= lim:
|
||||||
|
return [[r[0], r[1], r[2], r[3], r[4]] for r in filtered[-lim:]]
|
||||||
|
return ohlcv[-lim:] if ohlcv else []
|
||||||
|
|
||||||
|
|
||||||
def generate_multi_timeframe_chart_png(
|
def generate_multi_timeframe_chart_png(
|
||||||
exchange_symbol,
|
exchange_symbol,
|
||||||
title_prefix,
|
title_prefix,
|
||||||
@@ -753,9 +797,15 @@ def generate_multi_timeframe_chart_png(
|
|||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
panels = []
|
panels = []
|
||||||
cell_w, cell_h = 980, 520
|
cell_w, cell_h = 980, 520
|
||||||
|
end_ts_ms = None
|
||||||
|
if marker_payload:
|
||||||
|
try:
|
||||||
|
end_ts_ms = int(marker_payload.get("exit_ts_ms") or marker_payload.get("entry_ts_ms") or 0) or None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
end_ts_ms = None
|
||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
try:
|
try:
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit)
|
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
||||||
except Exception:
|
except Exception:
|
||||||
ohlcv = []
|
ohlcv = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
||||||
@@ -847,6 +897,7 @@ def journal_coin_from_symbol(symbol):
|
|||||||
|
|
||||||
EARLY_EXIT_TRIGGERS = (
|
EARLY_EXIT_TRIGGERS = (
|
||||||
"",
|
"",
|
||||||
|
"止盈",
|
||||||
"保本止盈",
|
"保本止盈",
|
||||||
"移动止盈",
|
"移动止盈",
|
||||||
"手动平仓",
|
"手动平仓",
|
||||||
@@ -861,6 +912,19 @@ ENTRY_REASON_OPTIONS = (
|
|||||||
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
|
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
|
||||||
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
|
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
|
||||||
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
|
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
|
||||||
|
"关键位箱体突破",
|
||||||
|
"关键位收敛突破",
|
||||||
|
"关键位斐波0.618",
|
||||||
|
"关键位斐波0.786",
|
||||||
|
)
|
||||||
|
|
||||||
|
STATS_SEGMENT_DEFS = (
|
||||||
|
("all", "全部已平仓", {"segment": "all"}),
|
||||||
|
("manual", "人工·下单监控", {"segment": "manual"}),
|
||||||
|
("key_box", "关键位箱体突破", {"segment": "key_box"}),
|
||||||
|
("key_conv", "关键位收敛突破", {"segment": "key_conv"}),
|
||||||
|
("key_fib618", "关键位斐波0.618", {"segment": "key_fib618"}),
|
||||||
|
("key_fib786", "关键位斐波0.786", {"segment": "key_fib786"}),
|
||||||
)
|
)
|
||||||
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom)
|
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom)
|
||||||
ENTRY_REASON_OTHER = "__OTHER__"
|
ENTRY_REASON_OTHER = "__OTHER__"
|
||||||
@@ -1395,17 +1459,64 @@ def _calendar_month_bounds(local_dt):
|
|||||||
|
|
||||||
|
|
||||||
def _count_opens_between(conn, start_td, end_td):
|
def _count_opens_between(conn, start_td, end_td):
|
||||||
|
return _count_opens_for_segment(conn, start_td, end_td, "all")
|
||||||
|
|
||||||
|
|
||||||
|
def _list_window_from_request():
|
||||||
|
return resolve_window(request.args, default_preset=PRESET_UTC_TODAY)
|
||||||
|
|
||||||
|
|
||||||
|
def _pnl_row_matches_segment(row, segment_key):
|
||||||
|
try:
|
||||||
|
mt = (row["monitor_type"] or "").strip()
|
||||||
|
kst = (row["key_signal_type"] or "").strip()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
if segment_key == "all":
|
||||||
|
return True
|
||||||
|
if segment_key == "manual":
|
||||||
|
return mt == ORDER_MONITOR_TYPE_MANUAL and not kst
|
||||||
|
if segment_key == "key_box":
|
||||||
|
return kst == "箱体突破"
|
||||||
|
if segment_key == "key_conv":
|
||||||
|
return kst == "收敛突破"
|
||||||
|
if segment_key == "key_fib618":
|
||||||
|
return kst == "斐波回调0.618"
|
||||||
|
if segment_key == "key_fib786":
|
||||||
|
return kst == "斐波回调0.786"
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _count_opens_for_segment(conn, start_td, end_td, segment_key):
|
||||||
|
if segment_key == "manual":
|
||||||
|
return conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ? "
|
||||||
|
"AND (monitor_type IS NULL OR monitor_type=? OR TRIM(monitor_type)='') "
|
||||||
|
"AND (key_signal_type IS NULL OR TRIM(key_signal_type)='')",
|
||||||
|
(start_td, end_td, ORDER_MONITOR_TYPE_MANUAL),
|
||||||
|
).fetchone()[0]
|
||||||
|
kst_map = {
|
||||||
|
"key_box": "箱体突破",
|
||||||
|
"key_conv": "收敛突破",
|
||||||
|
"key_fib618": "斐波回调0.618",
|
||||||
|
"key_fib786": "斐波回调0.786",
|
||||||
|
}
|
||||||
|
kst = kst_map.get(segment_key)
|
||||||
|
if kst:
|
||||||
|
return conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ? AND key_signal_type=?",
|
||||||
|
(start_td, end_td, kst),
|
||||||
|
).fetchone()[0]
|
||||||
return conn.execute(
|
return conn.execute(
|
||||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ?",
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ?",
|
||||||
(start_td, end_td),
|
(start_td, end_td),
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
def _load_completed_live_pnls(conn):
|
def _load_completed_trade_pnls(conn):
|
||||||
q = """SELECT pnl_amount, reviewed_pnl_amount, closed_at, reviewed_closed_at, created_at,
|
q = """SELECT pnl_amount, reviewed_pnl_amount, closed_at, reviewed_closed_at, created_at, opened_at,
|
||||||
result, reviewed_result
|
result, reviewed_result, monitor_type, key_signal_type
|
||||||
FROM trade_records
|
FROM trade_records
|
||||||
WHERE monitor_type = '下单监控'
|
|
||||||
ORDER BY COALESCE(closed_at, created_at, opened_at) ASC, id ASC"""
|
ORDER BY COALESCE(closed_at, created_at, opened_at) ASC, id ASC"""
|
||||||
rows = conn.execute(q).fetchall()
|
rows = conn.execute(q).fetchall()
|
||||||
out = []
|
out = []
|
||||||
@@ -1419,7 +1530,7 @@ def _load_completed_live_pnls(conn):
|
|||||||
p = 0.0
|
p = 0.0
|
||||||
t = parse_dt_for_trading_day(r["reviewed_closed_at"]) or parse_dt_for_trading_day(r["closed_at"]) or parse_dt_for_trading_day(r["created_at"])
|
t = parse_dt_for_trading_day(r["reviewed_closed_at"]) or parse_dt_for_trading_day(r["closed_at"]) or parse_dt_for_trading_day(r["created_at"])
|
||||||
td = get_trading_day(t) if t else None
|
td = get_trading_day(t) if t else None
|
||||||
out.append((p, t, td))
|
out.append((p, t, td, r))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -1488,34 +1599,35 @@ def _compute_period_metrics(trades):
|
|||||||
|
|
||||||
|
|
||||||
def compute_stats_bundle(conn, trading_day, now_dt=None):
|
def compute_stats_bundle(conn, trading_day, now_dt=None):
|
||||||
"""日 / 周 / 月 统计:平仓按平仓时间所在交易日计入。"""
|
"""日 / 周 / 月 统计:平仓按北京时间交易日(默认 8:00 切日)计入。"""
|
||||||
now_dt = now_dt or app_now()
|
now_dt = now_dt or app_now()
|
||||||
pnls = _load_completed_live_pnls(conn)
|
pnls = _load_completed_trade_pnls(conn)
|
||||||
total_opens_all = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0]
|
total_opens_all = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0]
|
||||||
w_start, w_end = _session_week_bounds(trading_day)
|
w_start, w_end = _session_week_bounds(trading_day)
|
||||||
m_start, m_end = _calendar_month_bounds(now_dt)
|
m_start, m_end = _calendar_month_bounds(now_dt)
|
||||||
|
|
||||||
def in_week(tr):
|
def slice_metrics(seg_key):
|
||||||
_p, _t, td = tr
|
seg_rows = [tr for tr in pnls if _pnl_row_matches_segment(tr[3], seg_key)]
|
||||||
return td and w_start <= td <= w_end
|
day_tr = [(p, t, td) for p, t, td, _r in seg_rows if td == trading_day]
|
||||||
|
week_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and w_start <= td <= w_end]
|
||||||
|
month_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and m_start <= td <= m_end]
|
||||||
|
dm = _compute_period_metrics(day_tr)
|
||||||
|
wm = _compute_period_metrics(week_tr)
|
||||||
|
mm = _compute_period_metrics(month_tr)
|
||||||
|
dm["opens_count"] = _count_opens_for_segment(conn, trading_day, trading_day, seg_key)
|
||||||
|
wm["opens_count"] = _count_opens_for_segment(conn, w_start, w_end, seg_key)
|
||||||
|
mm["opens_count"] = _count_opens_for_segment(conn, m_start, m_end, seg_key)
|
||||||
|
dm["range_label"] = f"北京时间交易日 {trading_day}({TRADING_DAY_RESET_HOUR}:00 切日)"
|
||||||
|
wm["range_label"] = f"{w_start} ~ {w_end}(北京日期,近7天)"
|
||||||
|
mm["range_label"] = f"{m_start} ~ {m_end}(北京自然月)"
|
||||||
|
return dm, wm, mm
|
||||||
|
|
||||||
def in_month(tr):
|
segments = []
|
||||||
_p, _t, td = tr
|
for seg_key, seg_title, _meta in STATS_SEGMENT_DEFS:
|
||||||
return td and m_start <= td <= m_end
|
dm, wm, mm = slice_metrics(seg_key)
|
||||||
|
segments.append({"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm})
|
||||||
|
|
||||||
day_trades = [tr for tr in pnls if tr[2] == trading_day]
|
dm, wm, mm = slice_metrics("all")
|
||||||
week_trades = [tr for tr in pnls if in_week(tr)]
|
|
||||||
month_trades = [tr for tr in pnls if in_month(tr)]
|
|
||||||
|
|
||||||
dm = _compute_period_metrics(day_trades)
|
|
||||||
wm = _compute_period_metrics(week_trades)
|
|
||||||
mm = _compute_period_metrics(month_trades)
|
|
||||||
dm["opens_count"] = _count_opens_between(conn, trading_day, trading_day)
|
|
||||||
wm["opens_count"] = _count_opens_between(conn, w_start, w_end)
|
|
||||||
mm["opens_count"] = _count_opens_between(conn, m_start, m_end)
|
|
||||||
dm["range_label"] = f"北京时间交易日 {trading_day}"
|
|
||||||
wm["range_label"] = f"{w_start} ~ {w_end}(北京日期,近7天窗口)"
|
|
||||||
mm["range_label"] = f"{m_start} ~ {m_end}(北京时间自然月)"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"trading_day": trading_day,
|
"trading_day": trading_day,
|
||||||
@@ -1523,6 +1635,8 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
|
|||||||
"day": dm,
|
"day": dm,
|
||||||
"week": wm,
|
"week": wm,
|
||||||
"month": mm,
|
"month": mm,
|
||||||
|
"segments": segments,
|
||||||
|
"stats_reset_hour": TRADING_DAY_RESET_HOUR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1750,7 +1864,11 @@ def to_effective_trade_dict(row):
|
|||||||
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
||||||
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
||||||
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
||||||
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", base_stop)
|
open_stop = item.get("initial_stop_loss")
|
||||||
|
if open_stop in (None, ""):
|
||||||
|
open_stop = base_stop
|
||||||
|
item["display_open_stop_loss"] = open_stop
|
||||||
|
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
||||||
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
||||||
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
||||||
item["effective_miss_reason"] = get_effective_trade_field(row, "reviewed_miss_reason", "miss_reason", item.get("miss_reason"))
|
item["effective_miss_reason"] = get_effective_trade_field(row, "reviewed_miss_reason", "miss_reason", item.get("miss_reason"))
|
||||||
@@ -2021,6 +2139,7 @@ def insert_trade_record(
|
|||||||
closed_at_ms=None,
|
closed_at_ms=None,
|
||||||
exchange_trade_id=None,
|
exchange_trade_id=None,
|
||||||
key_signal_type=None,
|
key_signal_type=None,
|
||||||
|
entry_reason=None,
|
||||||
):
|
):
|
||||||
hold_minutes = calc_hold_minutes(hold_seconds)
|
hold_minutes = calc_hold_minutes(hold_seconds)
|
||||||
open_ts = opened_at or app_now_str()
|
open_ts = opened_at or app_now_str()
|
||||||
@@ -2028,13 +2147,15 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
|
snap_sl = initial_stop_loss if initial_stop_loss not in (None, "") else stop_loss
|
||||||
|
er = (entry_reason or "").strip() or entry_reason_from_key_signal(kst) or ""
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO trade_records (symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
"INSERT INTO trade_records (symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id,entry_reason) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(
|
(
|
||||||
symbol, monitor_type, kst, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
|
symbol, monitor_type, kst, direction, trigger_price, snap_sl, snap_sl, take_profit,
|
||||||
margin_capital, leverage, pnl_amount, hold_seconds,
|
margin_capital, leverage, pnl_amount, hold_seconds,
|
||||||
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
|
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
|
||||||
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id
|
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id, er or None
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5100,6 +5221,8 @@ def sync_trade_records_from_exchange(conn):
|
|||||||
def render_main_page(page="trade"):
|
def render_main_page(page="trade"):
|
||||||
now = app_now()
|
now = app_now()
|
||||||
trading_day = get_trading_day(now)
|
trading_day = get_trading_day(now)
|
||||||
|
list_window = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(list_window["start_utc"], list_window["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
session_row = ensure_session(conn, trading_day)
|
session_row = ensure_session(conn, trading_day)
|
||||||
local_current_capital = float(session_row["current_capital"])
|
local_current_capital = float(session_row["current_capital"])
|
||||||
@@ -5109,7 +5232,10 @@ def render_main_page(page="trade"):
|
|||||||
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
|
||||||
recommended_capital = get_recommended_capital(current_capital)
|
recommended_capital = get_recommended_capital(current_capital)
|
||||||
key_list = conn.execute("SELECT * FROM key_monitors").fetchall()
|
key_list = conn.execute("SELECT * FROM key_monitors").fetchall()
|
||||||
key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall()
|
key_history = conn.execute(
|
||||||
|
"SELECT * FROM key_monitor_history WHERE closed_at >= ? AND closed_at <= ? ORDER BY id DESC LIMIT 500",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
stats_bundle = compute_stats_bundle(conn, trading_day, now)
|
stats_bundle = compute_stats_bundle(conn, trading_day, now)
|
||||||
raw_order_list = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
|
raw_order_list = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
|
||||||
order_list = []
|
order_list = []
|
||||||
@@ -5120,7 +5246,11 @@ def render_main_page(page="trade"):
|
|||||||
sync_trade_records_from_exchange(conn)
|
sync_trade_records_from_exchange(conn)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall()
|
raw_records = conn.execute(
|
||||||
|
"SELECT * FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? "
|
||||||
|
"AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id DESC LIMIT 1000",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
records = [to_effective_trade_dict(r) for r in raw_records]
|
records = [to_effective_trade_dict(r) for r in raw_records]
|
||||||
total = len(records)
|
total = len(records)
|
||||||
miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过")
|
miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过")
|
||||||
@@ -5172,7 +5302,14 @@ def render_main_page(page="trade"):
|
|||||||
can_trade=can_trade,
|
can_trade=can_trade,
|
||||||
focus_key_id=(key_list[0]["id"] if key_list else None),
|
focus_key_id=(key_list[0]["id"] if key_list else None),
|
||||||
focus_order_id=(order_list[0]["id"] if order_list else None),
|
focus_order_id=(order_list[0]["id"] if order_list else None),
|
||||||
data_export_version=2,
|
data_export_version=3,
|
||||||
|
list_window=list_window,
|
||||||
|
list_window_presets={
|
||||||
|
"utc_today": PRESET_UTC_TODAY,
|
||||||
|
"utc_last24h": PRESET_UTC_LAST24H,
|
||||||
|
"utc_last7d": PRESET_UTC_LAST7D,
|
||||||
|
"custom": PRESET_CUSTOM,
|
||||||
|
},
|
||||||
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
||||||
risk_percent=RISK_PERCENT,
|
risk_percent=RISK_PERCENT,
|
||||||
breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
||||||
@@ -6241,43 +6378,45 @@ def _md_response(filename, content):
|
|||||||
@app.route("/export/trade_records")
|
@app.route("/export/trade_records")
|
||||||
@login_required
|
@login_required
|
||||||
def export_trade_records():
|
def export_trade_records():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id,symbol,monitor_type,direction,trigger_price,stop_loss,take_profit,margin_capital,leverage,"
|
"SELECT id,symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,"
|
||||||
"pnl_amount,hold_seconds,hold_minutes,opened_at,closed_at,result,miss_reason,"
|
"margin_capital,leverage,pnl_amount,hold_seconds,hold_minutes,planned_rr,actual_rr,risk_amount,"
|
||||||
"entry_reason,reviewed_entry_reason,created_at FROM trade_records ORDER BY id ASC"
|
"opened_at,closed_at,result,miss_reason,entry_reason,reviewed_entry_reason,"
|
||||||
|
"exchange_realized_pnl,exchange_opened_at,exchange_closed_at,created_at "
|
||||||
|
"FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? "
|
||||||
|
"AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id ASC",
|
||||||
|
(start_bj, end_bj),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
head_base = [
|
head = [
|
||||||
"id",
|
"id", "symbol", "monitor_type", "key_signal_type", "direction", "trigger_price",
|
||||||
"symbol",
|
"stop_loss_open_snapshot", "initial_stop_loss", "take_profit", "margin_capital", "leverage",
|
||||||
"monitor_type",
|
"pnl_amount", "hold_seconds", "hold_minutes", "planned_rr", "actual_rr", "risk_amount",
|
||||||
"direction",
|
"opened_at", "closed_at", "result", "miss_reason", "entry_reason", "reviewed_entry_reason",
|
||||||
"trigger_price",
|
"exchange_realized_pnl", "exchange_opened_at", "exchange_closed_at", "created_at", "开仓类型",
|
||||||
"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",
|
|
||||||
]
|
]
|
||||||
head = head_base + ["开仓类型"]
|
|
||||||
data = []
|
data = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
er0 = (r["entry_reason"] or "").strip() if r["entry_reason"] else ""
|
er0 = (r["entry_reason"] or "").strip() if r["entry_reason"] else ""
|
||||||
er1 = (r["reviewed_entry_reason"] or "").strip() if r["reviewed_entry_reason"] else ""
|
er1 = (r["reviewed_entry_reason"] or "").strip() if r["reviewed_entry_reason"] else ""
|
||||||
eff = er1 or er0
|
kst = (r["key_signal_type"] or "").strip() if "key_signal_type" in r.keys() else ""
|
||||||
data.append(tuple(r[h] for h in head_base) + (eff,))
|
eff = er1 or er0 or entry_reason_from_key_signal(kst) or ""
|
||||||
|
snap = r["initial_stop_loss"] if r["initial_stop_loss"] not in (None, "") else r["stop_loss"]
|
||||||
|
data.append((
|
||||||
|
r["id"], r["symbol"], r["monitor_type"], kst, r["direction"], r["trigger_price"],
|
||||||
|
snap, r["initial_stop_loss"], r["take_profit"], r["margin_capital"], r["leverage"],
|
||||||
|
r["pnl_amount"], r["hold_seconds"], r["hold_minutes"], r["planned_rr"], r["actual_rr"], r["risk_amount"],
|
||||||
|
r["opened_at"], r["closed_at"], r["result"], r["miss_reason"], r["entry_reason"], r["reviewed_entry_reason"],
|
||||||
|
r["exchange_realized_pnl"] if "exchange_realized_pnl" in r.keys() else None,
|
||||||
|
r["exchange_opened_at"] if "exchange_opened_at" in r.keys() else None,
|
||||||
|
r["exchange_closed_at"] if "exchange_closed_at" in r.keys() else None,
|
||||||
|
r["created_at"], eff,
|
||||||
|
))
|
||||||
day = app_now().strftime("%Y%m%d")
|
day = app_now().strftime("%Y%m%d")
|
||||||
return _csv_response(f"trade_records_v2_{day}.csv", data, head)
|
return _csv_response(f"trade_records_v3_{day}.csv", data, head)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/export/journal_entries")
|
@app.route("/export/journal_entries")
|
||||||
@@ -6349,10 +6488,13 @@ def export_key_monitors():
|
|||||||
@app.route("/export/key_monitor_history")
|
@app.route("/export/key_monitor_history")
|
||||||
@login_required
|
@login_required
|
||||||
def export_key_monitor_history():
|
def export_key_monitor_history():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id,symbol,monitor_type,direction,upper,lower,notification_count,last_alert_message,close_reason,closed_at "
|
"SELECT id,symbol,monitor_type,direction,upper,lower,notification_count,last_alert_message,close_reason,closed_at "
|
||||||
"FROM key_monitor_history ORDER BY id ASC"
|
"FROM key_monitor_history WHERE closed_at >= ? AND closed_at <= ? ORDER BY id ASC",
|
||||||
|
(start_bj, end_bj),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
head = [
|
head = [
|
||||||
@@ -6567,9 +6709,10 @@ def add_journal():
|
|||||||
symbol_guess = normalize_symbol_input(coin) or coin
|
symbol_guess = normalize_symbol_input(coin) or coin
|
||||||
exchange_symbol = normalize_exchange_symbol(symbol_guess)
|
exchange_symbol = normalize_exchange_symbol(symbol_guess)
|
||||||
title_prefix = f"{symbol_guess} journal {entry_id[:8]}"
|
title_prefix = f"{symbol_guess} journal {entry_id[:8]}"
|
||||||
|
close_ms = _local_input_datetime_to_ms(d.get("close_datetime"))
|
||||||
marker_payload = {
|
marker_payload = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"exit_ts_ms": close_ms,
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"entry_ts_ms": close_ms,
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"entry_price": d.get("entry_price_hint"),
|
||||||
"exit_price": None,
|
"exit_price": None,
|
||||||
}
|
}
|
||||||
@@ -6634,8 +6777,14 @@ def add_journal():
|
|||||||
@app.route("/api/journals")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
def api_journals():
|
def api_journals():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute("SELECT * FROM journal_entries ORDER BY created_at DESC").fetchall()
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM journal_entries WHERE COALESCE(close_datetime, created_at, open_datetime) >= ? "
|
||||||
|
"AND COALESCE(close_datetime, created_at, open_datetime) <= ? ORDER BY created_at DESC LIMIT 500",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
result = []
|
result = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
@@ -6683,8 +6832,13 @@ def delete_journal(jid):
|
|||||||
@app.route("/api/reviews")
|
@app.route("/api/reviews")
|
||||||
@login_required
|
@login_required
|
||||||
def api_reviews():
|
def api_reviews():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute("SELECT * FROM ai_reviews ORDER BY created_at DESC").fetchall()
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM ai_reviews WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT 200",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,10 @@
|
|||||||
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
|
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
|
||||||
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
|
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
|
||||||
.export-bar a:hover{background:#1f2740}
|
.export-bar a:hover{background:#1f2740}
|
||||||
|
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
|
||||||
|
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
|
||||||
|
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
|
||||||
|
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
|
||||||
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
|
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
|
||||||
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
||||||
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
||||||
@@ -204,6 +208,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
||||||
|
|
||||||
|
<div class="list-window-bar">
|
||||||
|
<span style="color:#cfd3ef">列表筛选(<strong>UTC</strong>,默认当日):{{ list_window.label }}</span>
|
||||||
|
<label>预设
|
||||||
|
<select id="win-preset-select" onchange="toggleListWindowCustom()">
|
||||||
|
<option value="utc_today" {% if list_window.preset == 'utc_today' %}selected{% endif %}>UTC 当日</option>
|
||||||
|
<option value="utc_last24h" {% if list_window.preset == 'utc_last24h' %}selected{% endif %}>近 24 小时</option>
|
||||||
|
<option value="utc_last7d" {% if list_window.preset == 'utc_last7d' %}selected{% endif %}>近 7 天</option>
|
||||||
|
<option value="custom" {% if list_window.preset == 'custom' %}selected{% endif %}>自定义</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<span id="win-custom-range" style="{% if list_window.preset != 'custom' %}display:none{% endif %}">
|
||||||
|
<label>起(UTC) <input type="datetime-local" id="win-from-utc" value="{{ list_window.start_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
|
||||||
|
<label>止(UTC) <input type="datetime-local" id="win-to-utc" value="{{ list_window.end_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
|
||||||
|
</span>
|
||||||
|
<button type="button" style="padding:6px 12px" onclick="applyListWindow()">应用</button>
|
||||||
|
<span style="color:#8892b0;font-size:.75rem">统计页仍按北京时间 {{ stats_bundle.stats_reset_hour|default(reset_hour) }}:00 切日</span>
|
||||||
|
</div>
|
||||||
<div class="export-bar">
|
<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>
|
<a href="/export/trade_records">交易记录</a>
|
||||||
@@ -504,7 +525,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
|
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损(开仓)</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
|
||||||
{% for r in record %}
|
{% for r in record %}
|
||||||
<tr id="trade-row-{{ r.id }}">
|
<tr id="trade-row-{{ r.id }}">
|
||||||
{% set pnl_val = (r.pnl_amount or 0)|float %}
|
{% set pnl_val = (r.pnl_amount or 0)|float %}
|
||||||
@@ -512,7 +533,7 @@
|
|||||||
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
|
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
|
||||||
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
|
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
|
||||||
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
|
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
|
||||||
{% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
|
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
|
||||||
{% set tp_show = r.effective_take_profit or r.take_profit %}
|
{% set tp_show = r.effective_take_profit or r.take_profit %}
|
||||||
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
|
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
|
||||||
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
|
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
|
||||||
@@ -537,9 +558,10 @@
|
|||||||
onclick='fillJournalFromTrade({{ {
|
onclick='fillJournalFromTrade({{ {
|
||||||
"symbol": r.symbol,
|
"symbol": r.symbol,
|
||||||
"monitor_type": r.monitor_type,
|
"monitor_type": r.monitor_type,
|
||||||
|
"key_signal_type": r.key_signal_type or "",
|
||||||
"direction": r.direction,
|
"direction": r.direction,
|
||||||
"trigger_price": r.trigger_price,
|
"trigger_price": r.trigger_price,
|
||||||
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
|
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
|
||||||
"take_profit": r.effective_take_profit or r.take_profit,
|
"take_profit": r.effective_take_profit or r.take_profit,
|
||||||
"opened_at": r.effective_opened_at,
|
"opened_at": r.effective_opened_at,
|
||||||
"closed_at": r.effective_closed_at,
|
"closed_at": r.effective_closed_at,
|
||||||
@@ -619,6 +641,7 @@
|
|||||||
<input name="real_rr" placeholder="实际RR">
|
<input name="real_rr" placeholder="实际RR">
|
||||||
<select name="early_exit_trigger" required title="平仓如何触发">
|
<select name="early_exit_trigger" required title="平仓如何触发">
|
||||||
<option value="">离场触发(必选)</option>
|
<option value="">离场触发(必选)</option>
|
||||||
|
<option value="止盈">止盈</option>
|
||||||
<option value="保本止盈">保本止盈</option>
|
<option value="保本止盈">保本止盈</option>
|
||||||
<option value="移动止盈">移动止盈</option>
|
<option value="移动止盈">移动止盈</option>
|
||||||
<option value="手动平仓">手动平仓</option>
|
<option value="手动平仓">手动平仓</option>
|
||||||
@@ -692,12 +715,17 @@
|
|||||||
<div class="stat-item"><div class="label">持仓占用导致错过(累计)</div><div class="value">{{ occupied_miss_total }}</div></div>
|
<div class="stat-item"><div class="label">持仓占用导致错过(累计)</div><div class="value">{{ occupied_miss_total }}</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
|
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
|
||||||
已平仓「下单监控」按平仓时间归入<strong>北京时间</strong>下的交易日;胜率按盈笔数/(盈+亏)。历史总开仓(累计):
|
统计分析按<strong>北京时间 {{ stats_bundle.stats_reset_hour }}:00</strong>切日计入;下列为各品类已平仓。历史总开仓(累计):
|
||||||
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong> 次
|
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong> 次
|
||||||
</div>
|
</div>
|
||||||
{{ period_stats("日统计", stats_bundle.day) }}
|
{% for seg in stats_bundle.segments %}
|
||||||
{{ period_stats("周统计", stats_bundle.week) }}
|
<div class="stats-segment-block">
|
||||||
{{ period_stats("月统计", stats_bundle.month) }}
|
<h2>{{ seg.title }}</h2>
|
||||||
|
{{ period_stats("日统计", seg.day) }}
|
||||||
|
{{ period_stats("周统计", seg.week) }}
|
||||||
|
{{ period_stats("月统计", seg.month) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -921,8 +949,50 @@ function deleteKeyHistory(id){
|
|||||||
.catch(()=>{ window.location.href = `${window.location.pathname}?_ts=${Date.now()}`; });
|
.catch(()=>{ window.location.href = `${window.location.pathname}?_ts=${Date.now()}`; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listWindowQueryString(){
|
||||||
|
const presetEl = document.getElementById("win-preset-select");
|
||||||
|
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_today";
|
||||||
|
const q = new URLSearchParams(window.location.search);
|
||||||
|
q.set("win_preset", preset);
|
||||||
|
if(preset === "custom"){
|
||||||
|
const fromEl = document.getElementById("win-from-utc");
|
||||||
|
const toEl = document.getElementById("win-to-utc");
|
||||||
|
if(fromEl && fromEl.value) q.set("from_utc", fromEl.value.replace("T", " ") + ":00");
|
||||||
|
else q.delete("from_utc");
|
||||||
|
if(toEl && toEl.value) q.set("to_utc", toEl.value.replace("T", " ") + ":00");
|
||||||
|
else q.delete("to_utc");
|
||||||
|
} else {
|
||||||
|
q.delete("from_utc");
|
||||||
|
q.delete("to_utc");
|
||||||
|
}
|
||||||
|
return q.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleListWindowCustom(){
|
||||||
|
const preset = document.getElementById("win-preset-select");
|
||||||
|
const box = document.getElementById("win-custom-range");
|
||||||
|
if(!preset || !box) return;
|
||||||
|
box.style.display = preset.value === "custom" ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyListWindow(){
|
||||||
|
const qs = listWindowQueryString();
|
||||||
|
const path = window.location.pathname || "/trade";
|
||||||
|
window.location.href = qs ? (path + "?" + qs) : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachListWindowToExports(){
|
||||||
|
const qs = listWindowQueryString();
|
||||||
|
if(!qs) return;
|
||||||
|
document.querySelectorAll('.export-bar a[href^="/export/trade_records"], .export-bar a[href^="/export/key_monitor_history"]').forEach(a=>{
|
||||||
|
const base = a.getAttribute("href").split("?")[0];
|
||||||
|
a.setAttribute("href", base + "?" + qs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadJournals(){
|
function loadJournals(){
|
||||||
fetch("/api/journals").then(r=>r.json()).then(data=>{
|
const qs = listWindowQueryString();
|
||||||
|
fetch("/api/journals" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
|
||||||
Object.keys(journalCache).forEach(k=>delete journalCache[k]);
|
Object.keys(journalCache).forEach(k=>delete journalCache[k]);
|
||||||
let html="";
|
let html="";
|
||||||
data.forEach(o=>{
|
data.forEach(o=>{
|
||||||
@@ -944,7 +1014,8 @@ function loadJournals(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadReviews(){
|
function loadReviews(){
|
||||||
fetch("/api/reviews").then(r=>r.json()).then(data=>{
|
const qs = listWindowQueryString();
|
||||||
|
fetch("/api/reviews" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
|
||||||
Object.keys(reviewCache).forEach(k=>delete reviewCache[k]);
|
Object.keys(reviewCache).forEach(k=>delete reviewCache[k]);
|
||||||
let html="";
|
let html="";
|
||||||
data.forEach(r=>{
|
data.forEach(r=>{
|
||||||
@@ -1016,7 +1087,13 @@ function setJournalField(name, value){
|
|||||||
el.value = String(value);
|
el.value = String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EARLY_EXIT_TRIGGERS = new Set(["保本止盈","移动止盈","手动平仓","止损","其他"]);
|
const EARLY_EXIT_TRIGGERS = new Set(["止盈","保本止盈","移动止盈","手动平仓","止损","其他"]);
|
||||||
|
const KEY_ENTRY_REASON_BY_SIGNAL = {
|
||||||
|
"箱体突破": "关键位箱体突破",
|
||||||
|
"收敛突破": "关键位收敛突破",
|
||||||
|
"斐波回调0.618": "关键位斐波0.618",
|
||||||
|
"斐波回调0.786": "关键位斐波0.786"
|
||||||
|
};
|
||||||
|
|
||||||
function splitLegacyEarlyExitReason(raw){
|
function splitLegacyEarlyExitReason(raw){
|
||||||
const s = String(raw || "").trim();
|
const s = String(raw || "").trim();
|
||||||
@@ -1106,11 +1183,17 @@ function fillJournalFromTrade(t){
|
|||||||
if(dirHint){ dirHint.value = t.direction || "long"; }
|
if(dirHint){ dirHint.value = t.direction || "long"; }
|
||||||
setJournalField("early_exit_trigger", "");
|
setJournalField("early_exit_trigger", "");
|
||||||
setJournalField("early_exit_note", "");
|
setJournalField("early_exit_note", "");
|
||||||
|
const kst = String(t.key_signal_type || "").trim();
|
||||||
|
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
|
||||||
|
if(erFromKey && JOURNAL_ENTRY_REASON_OPTIONS.includes(erFromKey)){
|
||||||
|
setJournalField("entry_reason", erFromKey);
|
||||||
|
} else {
|
||||||
setJournalField("entry_reason", "");
|
setJournalField("entry_reason", "");
|
||||||
|
}
|
||||||
setJournalField("entry_reason_custom", "");
|
setJournalField("entry_reason_custom", "");
|
||||||
syncJournalEntryReasonOtherUi();
|
syncJournalEntryReasonOtherUi();
|
||||||
const er = String(t.result || "").trim();
|
const er = String(t.result || "").trim();
|
||||||
const exitTrigMap = { 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
|
const exitTrigMap = { 止盈: "止盈", 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
|
||||||
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
|
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
|
||||||
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${t.trigger_price || "-"} 止损:${t.stop_loss || "-"} 止盈:${t.take_profit || "-"} | 类型:${t.monitor_type || "-"}`;
|
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${t.trigger_price || "-"} 止损:${t.stop_loss || "-"} 止盈:${t.take_profit || "-"} | 类型:${t.monitor_type || "-"}`;
|
||||||
setJournalField("note", note);
|
setJournalField("note", note);
|
||||||
@@ -1201,6 +1284,8 @@ function toggleStatsCard(){
|
|||||||
btn.innerText = collapsed ? "展开" : "折叠";
|
btn.innerText = collapsed ? "展开" : "折叠";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachListWindowToExports();
|
||||||
|
toggleListWindowCustom();
|
||||||
if(document.getElementById("journal-list")) loadJournals();
|
if(document.getElementById("journal-list")) loadJournals();
|
||||||
if(document.getElementById("review-list")) loadReviews();
|
if(document.getElementById("review-list")) loadReviews();
|
||||||
const reviewToggle = document.getElementById("review-mode-toggle");
|
const reviewToggle = document.getElementById("review-mode-toggle");
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 1 | 关键位监控 | `/key_monitor` | 关键位添加、实时门控、历史 |
|
| 1 | 关键位监控 | `/key_monitor` | 关键位添加、实时门控、历史 |
|
||||||
| 2 | 实盘下单 | `/trade` | 人工开仓、划转、实时持仓(**默认首页** `/` → `/trade`) |
|
| 2 | 实盘下单 | `/trade` | 人工开仓、划转、实时持仓(**默认首页** `/` → `/trade`) |
|
||||||
| 3 | 交易记录与复盘 | `/records` | 未改动 |
|
| 3 | 交易记录与复盘 | `/records` | 交易记录、复盘表单、AI 历史(受顶栏 UTC 时间窗筛选) |
|
||||||
| 4 | 统计分析 | `/stats` | 未改动 |
|
| 4 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
|
||||||
|
|
||||||
## 关键位监控页
|
## 关键位监控页
|
||||||
|
|
||||||
- 标题去掉「5m」;规则条从 `.env` 读取(周期、确认K、量能、自动开仓盈亏比、日成交量排名)。
|
- 标题去掉「5m」;规则条从 `.env` 读取(周期、确认K、量能、自动开仓盈亏比、日成交量排名)。
|
||||||
- 左列:活跃关键位,**pos-card** 样式展示现价/距上沿/距下沿/门控。
|
- 左列:活跃关键位,**pos-card** 样式展示现价/距上沿/距下沿/门控。
|
||||||
- 右列:关键位历史(失效/结案),与左列等高滚动。
|
- 右列:关键位历史(失效/结案),与左列等高滚动;**受顶栏 UTC 列表时间窗筛选**(默认 UTC 当日)。
|
||||||
- 监控类型新增:**斐波回调0.618**、**斐波回调0.786**(与 Gate 主站同一套规则,计算逻辑见仓库根目录 `fib_key_monitor_lib.py`)。
|
- 监控类型新增:**斐波回调0.618**、**斐波回调0.786**(与 Gate 主站同一套规则,计算逻辑见仓库根目录 `fib_key_monitor_lib.py`)。
|
||||||
|
|
||||||
### 斐波关键位监控(方案 A:交易所限价)
|
### 斐波关键位监控(方案 A:交易所限价)
|
||||||
@@ -41,11 +41,52 @@
|
|||||||
- 自动开仓写入 `order_monitors.key_signal_type`:`箱体突破` 或 `收敛突破`。
|
- 自动开仓写入 `order_monitors.key_signal_type`:`箱体突破` 或 `收敛突破`。
|
||||||
- 持仓与交易记录展示「来源 · 信号类型」。
|
- 持仓与交易记录展示「来源 · 信号类型」。
|
||||||
|
|
||||||
|
## 列表时间窗(UTC,全站顶栏)
|
||||||
|
|
||||||
|
共用模块:仓库根目录 `history_window_lib.py`(Gate / Binance 主站一致)。
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| 默认 | **UTC 当日**(`win_preset=utc_today`,从 UTC 0:00 至当前时刻) |
|
||||||
|
| 可选 | 近 24 小时、近 7 天、自定义起止(UTC,`datetime-local`) |
|
||||||
|
| 作用范围 | 关键位历史、交易记录列表、复盘记录 API、AI 历史 API、导出「交易记录」「关键位历史」 |
|
||||||
|
| 与统计的关系 | **仅影响列表/导出**;**统计分析页仍按北京时间 `TRADING_DAY_RESET_HOUR`(默认 8:00)切交易日** |
|
||||||
|
| 库内时间 | DB 存北京时间字符串;后端用 `utc_window_to_bj_sql_strings()` 换算后再 SQL 比较 |
|
||||||
|
| 切换方式 | 顶栏「列表筛选(UTC)」→ 选预设 → **应用**(保留当前路由,如 `/records?win_preset=…`) |
|
||||||
|
|
||||||
|
查询参数示例:
|
||||||
|
|
||||||
|
- `?win_preset=utc_today`
|
||||||
|
- `?win_preset=utc_last24h` / `utc_last7d`
|
||||||
|
- `?win_preset=custom&from_utc=2026-05-18 00:00:00&to_utc=2026-05-19 12:00:00`
|
||||||
|
|
||||||
## 交易记录与复盘
|
## 交易记录与复盘
|
||||||
|
|
||||||
- 支持从交易所收入流水等同步已实现盈亏;盈亏列标注 **所** / **估**。
|
- 支持从交易所收入流水等同步已实现盈亏;盈亏列标注 **所** / **估**。
|
||||||
- 记录页 **立即同步**(`POST /api/sync_exchange_pnl`)。
|
- 记录页 **立即同步**(`POST /api/sync_exchange_pnl`)。
|
||||||
- 未人工复盘时优先展示交易所盈亏(已同步时)。
|
- 未人工复盘时优先展示交易所盈亏(已同步时)。
|
||||||
|
- **列表默认只显示当前 UTC 时间窗内**的记录(见上节);导出 CSV 同步该时间窗。
|
||||||
|
- 表头 **「止损(开仓)」**:展示开仓快照 `initial_stop_loss`(无则回退 `stop_loss`);核对/复盘仍可用有效止损字段。
|
||||||
|
- 平仓写入 `trade_records` 时:`stop_loss` 与 `initial_stop_loss` 均写入**开仓时止损快照**;`key_signal_type` 保留箱体/收敛/斐波来源(`fib_key_monitor_lib.key_signal_type_for_trade_record`)。
|
||||||
|
- **开仓类型**(`entry_reason`):机器单平仓入库时,若未手填,按 `key_signal_type` 自动映射(见下表);列表/导出「开仓类型」列 = 复盘核对值优先,否则入库值,否则按信号映射。
|
||||||
|
|
||||||
|
| `key_signal_type` | 自动写入的 `entry_reason` |
|
||||||
|
|-------------------|---------------------------|
|
||||||
|
| 箱体突破 | 关键位箱体突破 |
|
||||||
|
| 收敛突破 | 关键位收敛突破 |
|
||||||
|
| 斐波回调0.618 | 关键位斐波0.618 |
|
||||||
|
| 斐波回调0.786 | 关键位斐波0.786 |
|
||||||
|
|
||||||
|
- 复盘表单 **开仓类型** 下拉新增上述四条固定文案(与趋势/波段类并列)。
|
||||||
|
- 复盘 **离场触发** 新增 **「止盈」**;从交易记录「填入复盘」时,若结果为「止盈/保本止盈/移动止盈/止损/手动平仓」会自动选中对应触发项,并按 `key_signal_type` 预填开仓类型。
|
||||||
|
- 勾选「保存时自动生成多周期 K 线图」时:以 **平仓时间** 为锚点,各周期向前约 `ORDER_CHART_LIMIT`(默认 100)根 K 线(`_fetch_ohlcv_ending_at`),不再固定拉「最近 100 根」。
|
||||||
|
- `/api/journals`、`/api/reviews` 支持同一时间窗 query,与列表一致。
|
||||||
|
|
||||||
|
### 导出(交易记录 v3)
|
||||||
|
|
||||||
|
- 文件名:`trade_records_v3_YYYYMMDD.csv`
|
||||||
|
- 相对 v2 增加:`key_signal_type`、`initial_stop_loss`(及开仓快照列)、`planned_rr`、`actual_rr`、`risk_amount`、交易所盈亏与时间字段等;末列「开仓类型」为有效展示文案。
|
||||||
|
- 「关键位历史」导出同样受 UTC 时间窗限制。
|
||||||
|
|
||||||
## 实盘下单页
|
## 实盘下单页
|
||||||
|
|
||||||
@@ -53,6 +94,16 @@
|
|||||||
- 右列:实时持仓(独立模块)。
|
- 右列:实时持仓(独立模块)。
|
||||||
- **人工开仓门控**:计划盈亏比 < `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝。
|
- **人工开仓门控**:计划盈亏比 < `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝。
|
||||||
|
|
||||||
|
## 统计分析页(`/stats`)
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| 切日 | **北京时间**;交易日边界 = 每日 `TRADING_DAY_RESET_HOUR:00`(`.env` 默认 **8**) |
|
||||||
|
| 分块 | 页内按品类各一块:**全部已平仓**、**人工·下单监控**、**关键位箱体突破**、**关键位收敛突破**、**关键位斐波0.618**、**关键位斐波0.786** |
|
||||||
|
| 每块指标 | 日 / 周 / 月:开单次数、平仓笔数、胜率、净盈亏、回撤、连续亏损等(与原口径一致) |
|
||||||
|
| 开单次数 | 人工块:`monitor_type=下单监控` 且无 `key_signal_type`;关键位块:按 `order_monitors.key_signal_type` 计数 |
|
||||||
|
| 不受 UTC 窗影响 | 统计始终基于库内全部已平仓记录,按北京交易日归类,**不**随顶栏 UTC 列表窗切换 |
|
||||||
|
|
||||||
## 持仓与计仓
|
## 持仓与计仓
|
||||||
|
|
||||||
- `MAX_ACTIVE_POSITIONS` 默认 **1**(可在 `.env` 调大)。
|
- `MAX_ACTIVE_POSITIONS` 默认 **1**(可在 `.env` 调大)。
|
||||||
@@ -72,12 +123,24 @@
|
|||||||
|
|
||||||
`key_monitors` 斐波字段:`fib_limit_order_id`、`fib_entry_price`、`fib_stop_loss`、`fib_take_profit`、`fib_order_amount`、`fib_margin_capital`、`fib_leverage`。
|
`key_monitors` 斐波字段:`fib_limit_order_id`、`fib_entry_price`、`fib_stop_loss`、`fib_take_profit`、`fib_order_amount`、`fib_margin_capital`、`fib_leverage`。
|
||||||
|
|
||||||
`trade_records` / `order_monitors`:`key_signal_type`、`exchange_realized_pnl`、`exchange_opened_at`、`exchange_closed_at`、`exchange_sync_key`。
|
`trade_records` / `order_monitors`:`key_signal_type`、`exchange_realized_pnl`、`exchange_opened_at`、`exchange_closed_at`、`exchange_sync_key`、`entry_reason`、`reviewed_entry_reason`、`initial_stop_loss`。
|
||||||
|
|
||||||
|
**历史数据**:本次**不做**旧记录的批量回填(`entry_reason` / `initial_stop_loss` / `key_signal_type` 等);仅**新产生**的平仓与复盘按新逻辑写入。旧行展示可回退已有字段。
|
||||||
|
|
||||||
|
## 涉及文件(便于排查)
|
||||||
|
|
||||||
|
| 路径 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `history_window_lib.py` | UTC 时间窗解析与转北京时间 SQL 字符串 |
|
||||||
|
| `fib_key_monitor_lib.py` | 斐波计算、`KEY_ENTRY_REASON_BY_SIGNAL`、`entry_reason_from_key_signal` |
|
||||||
|
| `crypto_monitor_binance/app.py` | 列表筛选、统计分块、导出 v3、复盘 K 线锚点、入库逻辑 |
|
||||||
|
| `crypto_monitor_binance/templates/index.html` | 顶栏时间窗、统计分块 UI、止损(开仓)列、复盘预填 |
|
||||||
|
|
||||||
## 升级步骤
|
## 升级步骤
|
||||||
|
|
||||||
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`。
|
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`。
|
||||||
2. 在 VPS 上为 Binance / Gate / Gate Bot **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。
|
2. 在 VPS 上为 Binance / Gate / Gate Bot **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。
|
||||||
3. 重启 Binance 实例(如 `pm2 restart crypto_binance`);SQLite 会自动 `ALTER` 新列。
|
3. 重启 Binance 实例(如 `pm2 restart crypto_binance`);SQLite 会自动 `ALTER` 缺列(斐波、交易所盈亏、`entry_reason` 等)。
|
||||||
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。
|
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。
|
||||||
5. 建议先用测试币验证斐波:限价挂出、标记价失效撤单、成交后 TP/SL 与订单监控是否正常。
|
5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**;`/stats` 可见分品类统计与「北京 8:00 切日」说明。
|
||||||
|
6. 建议先用测试币验证斐波:限价挂出、标记价失效撤单、成交后 TP/SL 与订单监控是否正常;平仓后检查交易记录止损(开仓)与开仓类型。
|
||||||
|
|||||||
+228
-64
@@ -36,13 +36,23 @@ if _REPO_ROOT not in sys.path:
|
|||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from fib_key_monitor_lib import (
|
from fib_key_monitor_lib import (
|
||||||
FIB_KEY_MONITOR_TYPES,
|
FIB_KEY_MONITOR_TYPES,
|
||||||
|
KEY_ENTRY_REASON_BY_SIGNAL,
|
||||||
calc_fib_plan,
|
calc_fib_plan,
|
||||||
|
entry_reason_from_key_signal,
|
||||||
fib_invalidate_by_mark,
|
fib_invalidate_by_mark,
|
||||||
fib_ratio_from_type,
|
fib_ratio_from_type,
|
||||||
is_fib_key_monitor_type,
|
is_fib_key_monitor_type,
|
||||||
key_signal_type_for_trade_record,
|
key_signal_type_for_trade_record,
|
||||||
stored_key_signal_type,
|
stored_key_signal_type,
|
||||||
)
|
)
|
||||||
|
from history_window_lib import (
|
||||||
|
PRESET_CUSTOM,
|
||||||
|
PRESET_UTC_LAST24H,
|
||||||
|
PRESET_UTC_LAST7D,
|
||||||
|
PRESET_UTC_TODAY,
|
||||||
|
resolve_window,
|
||||||
|
utc_window_to_bj_sql_strings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(path):
|
def load_env_file(path):
|
||||||
@@ -723,6 +733,41 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255),
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def _timeframe_period_ms(tf):
|
||||||
|
s = (tf or "").strip().lower()
|
||||||
|
if s.endswith("m"):
|
||||||
|
try:
|
||||||
|
return int(s[:-1]) * 60 * 1000
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if s.endswith("h"):
|
||||||
|
try:
|
||||||
|
return int(s[:-1]) * 3600 * 1000
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if s.endswith("d"):
|
||||||
|
try:
|
||||||
|
return int(s[:-1]) * 86400 * 1000
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return 300000
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_ohlcv_ending_at(exchange_symbol, timeframe, limit, end_ts_ms):
|
||||||
|
"""以 end_ts_ms 为终点向前取 K 线(无 end 则拉最近 limit 根)。"""
|
||||||
|
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
||||||
|
if not end_ts_ms:
|
||||||
|
return exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
|
||||||
|
period = _timeframe_period_ms(timeframe)
|
||||||
|
since = int(end_ts_ms) - period * (lim + 5)
|
||||||
|
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, since=max(0, since), limit=lim + 10)
|
||||||
|
rows = _ohlcv_to_rows(ohlcv)
|
||||||
|
filtered = [r for r in rows if int(r[0]) <= int(end_ts_ms)]
|
||||||
|
if len(filtered) >= lim:
|
||||||
|
return [[r[0], r[1], r[2], r[3], r[4]] for r in filtered[-lim:]]
|
||||||
|
return ohlcv[-lim:] if ohlcv else []
|
||||||
|
|
||||||
|
|
||||||
def generate_multi_timeframe_chart_png(
|
def generate_multi_timeframe_chart_png(
|
||||||
exchange_symbol,
|
exchange_symbol,
|
||||||
title_prefix,
|
title_prefix,
|
||||||
@@ -751,9 +796,15 @@ def generate_multi_timeframe_chart_png(
|
|||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
panels = []
|
panels = []
|
||||||
cell_w, cell_h = 980, 520
|
cell_w, cell_h = 980, 520
|
||||||
|
end_ts_ms = None
|
||||||
|
if marker_payload:
|
||||||
|
try:
|
||||||
|
end_ts_ms = int(marker_payload.get("exit_ts_ms") or marker_payload.get("entry_ts_ms") or 0) or None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
end_ts_ms = None
|
||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
try:
|
try:
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit)
|
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
||||||
except Exception:
|
except Exception:
|
||||||
ohlcv = []
|
ohlcv = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
||||||
@@ -845,6 +896,7 @@ def journal_coin_from_symbol(symbol):
|
|||||||
|
|
||||||
EARLY_EXIT_TRIGGERS = (
|
EARLY_EXIT_TRIGGERS = (
|
||||||
"",
|
"",
|
||||||
|
"止盈",
|
||||||
"保本止盈",
|
"保本止盈",
|
||||||
"移动止盈",
|
"移动止盈",
|
||||||
"手动平仓",
|
"手动平仓",
|
||||||
@@ -852,13 +904,26 @@ EARLY_EXIT_TRIGGERS = (
|
|||||||
"其他",
|
"其他",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 与用户约定的固定开仓类型(仅做这几类单子)
|
# 与用户约定的固定开仓类型
|
||||||
ENTRY_REASON_OPTIONS = (
|
ENTRY_REASON_OPTIONS = (
|
||||||
"趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低",
|
"趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低",
|
||||||
"趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高",
|
"趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高",
|
||||||
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
|
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
|
||||||
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
|
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
|
||||||
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
|
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
|
||||||
|
"关键位箱体突破",
|
||||||
|
"关键位收敛突破",
|
||||||
|
"关键位斐波0.618",
|
||||||
|
"关键位斐波0.786",
|
||||||
|
)
|
||||||
|
|
||||||
|
STATS_SEGMENT_DEFS = (
|
||||||
|
("all", "全部已平仓", {"segment": "all"}),
|
||||||
|
("manual", "人工·下单监控", {"segment": "manual"}),
|
||||||
|
("key_box", "关键位箱体突破", {"segment": "key_box"}),
|
||||||
|
("key_conv", "关键位收敛突破", {"segment": "key_conv"}),
|
||||||
|
("key_fib618", "关键位斐波0.618", {"segment": "key_fib618"}),
|
||||||
|
("key_fib786", "关键位斐波0.786", {"segment": "key_fib786"}),
|
||||||
)
|
)
|
||||||
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom)
|
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom)
|
||||||
ENTRY_REASON_OTHER = "__OTHER__"
|
ENTRY_REASON_OTHER = "__OTHER__"
|
||||||
@@ -1403,11 +1468,61 @@ def _count_opens_between(conn, start_td, end_td):
|
|||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
def _load_completed_live_pnls(conn):
|
def _list_window_from_request():
|
||||||
q = """SELECT pnl_amount, reviewed_pnl_amount, closed_at, reviewed_closed_at, created_at,
|
return resolve_window(request.args, default_preset=PRESET_UTC_TODAY)
|
||||||
result, reviewed_result
|
|
||||||
|
|
||||||
|
def _pnl_row_matches_segment(row, segment_key):
|
||||||
|
try:
|
||||||
|
mt = (row["monitor_type"] or "").strip()
|
||||||
|
kst = (row["key_signal_type"] or "").strip()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
if segment_key == "all":
|
||||||
|
return True
|
||||||
|
if segment_key == "manual":
|
||||||
|
return mt == ORDER_MONITOR_TYPE_MANUAL and not kst
|
||||||
|
if segment_key == "key_box":
|
||||||
|
return kst == "箱体突破"
|
||||||
|
if segment_key == "key_conv":
|
||||||
|
return kst == "收敛突破"
|
||||||
|
if segment_key == "key_fib618":
|
||||||
|
return kst == "斐波回调0.618"
|
||||||
|
if segment_key == "key_fib786":
|
||||||
|
return kst == "斐波回调0.786"
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _count_opens_for_segment(conn, start_td, end_td, segment_key):
|
||||||
|
if segment_key == "manual":
|
||||||
|
return conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ? "
|
||||||
|
"AND (monitor_type IS NULL OR monitor_type=? OR TRIM(monitor_type)='') "
|
||||||
|
"AND (key_signal_type IS NULL OR TRIM(key_signal_type)='')",
|
||||||
|
(start_td, end_td, ORDER_MONITOR_TYPE_MANUAL),
|
||||||
|
).fetchone()[0]
|
||||||
|
kst_map = {
|
||||||
|
"key_box": "箱体突破",
|
||||||
|
"key_conv": "收敛突破",
|
||||||
|
"key_fib618": "斐波回调0.618",
|
||||||
|
"key_fib786": "斐波回调0.786",
|
||||||
|
}
|
||||||
|
kst = kst_map.get(segment_key)
|
||||||
|
if kst:
|
||||||
|
return conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ? AND key_signal_type=?",
|
||||||
|
(start_td, end_td, kst),
|
||||||
|
).fetchone()[0]
|
||||||
|
return conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ?",
|
||||||
|
(start_td, end_td),
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_completed_trade_pnls(conn):
|
||||||
|
q = """SELECT pnl_amount, reviewed_pnl_amount, closed_at, reviewed_closed_at, created_at, opened_at,
|
||||||
|
result, reviewed_result, monitor_type, key_signal_type
|
||||||
FROM trade_records
|
FROM trade_records
|
||||||
WHERE monitor_type = '下单监控'
|
|
||||||
ORDER BY COALESCE(closed_at, created_at, opened_at) ASC, id ASC"""
|
ORDER BY COALESCE(closed_at, created_at, opened_at) ASC, id ASC"""
|
||||||
rows = conn.execute(q).fetchall()
|
rows = conn.execute(q).fetchall()
|
||||||
out = []
|
out = []
|
||||||
@@ -1421,7 +1536,7 @@ def _load_completed_live_pnls(conn):
|
|||||||
p = 0.0
|
p = 0.0
|
||||||
t = parse_dt_for_trading_day(r["reviewed_closed_at"]) or parse_dt_for_trading_day(r["closed_at"]) or parse_dt_for_trading_day(r["created_at"])
|
t = parse_dt_for_trading_day(r["reviewed_closed_at"]) or parse_dt_for_trading_day(r["closed_at"]) or parse_dt_for_trading_day(r["created_at"])
|
||||||
td = get_trading_day(t) if t else None
|
td = get_trading_day(t) if t else None
|
||||||
out.append((p, t, td))
|
out.append((p, t, td, r))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -1490,34 +1605,41 @@ def _compute_period_metrics(trades):
|
|||||||
|
|
||||||
|
|
||||||
def compute_stats_bundle(conn, trading_day, now_dt=None):
|
def compute_stats_bundle(conn, trading_day, now_dt=None):
|
||||||
"""日 / 周 / 月 统计:平仓按平仓时间所在交易日计入。"""
|
"""日 / 周 / 月 统计:平仓按北京时间交易日(默认 8:00 切日)计入。"""
|
||||||
now_dt = now_dt or app_now()
|
now_dt = now_dt or app_now()
|
||||||
pnls = _load_completed_live_pnls(conn)
|
pnls = _load_completed_trade_pnls(conn)
|
||||||
total_opens_all = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0]
|
total_opens_all = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0]
|
||||||
w_start, w_end = _session_week_bounds(trading_day)
|
w_start, w_end = _session_week_bounds(trading_day)
|
||||||
m_start, m_end = _calendar_month_bounds(now_dt)
|
m_start, m_end = _calendar_month_bounds(now_dt)
|
||||||
|
|
||||||
def in_week(tr):
|
def in_week(tr):
|
||||||
_p, _t, td = tr
|
return tr[2] and w_start <= tr[2] <= w_end
|
||||||
return td and w_start <= td <= w_end
|
|
||||||
|
|
||||||
def in_month(tr):
|
def in_month(tr):
|
||||||
_p, _t, td = tr
|
return tr[2] and m_start <= tr[2] <= m_end
|
||||||
return td and m_start <= td <= m_end
|
|
||||||
|
|
||||||
day_trades = [tr for tr in pnls if tr[2] == trading_day]
|
def slice_metrics(seg_key):
|
||||||
week_trades = [tr for tr in pnls if in_week(tr)]
|
seg_rows = [tr for tr in pnls if _pnl_row_matches_segment(tr[3], seg_key)]
|
||||||
month_trades = [tr for tr in pnls if in_month(tr)]
|
day_tr = [(p, t, td) for p, t, td, _r in seg_rows if td == trading_day]
|
||||||
|
week_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and w_start <= td <= w_end]
|
||||||
|
month_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and m_start <= td <= m_end]
|
||||||
|
dm = _compute_period_metrics(day_tr)
|
||||||
|
wm = _compute_period_metrics(week_tr)
|
||||||
|
mm = _compute_period_metrics(month_tr)
|
||||||
|
dm["opens_count"] = _count_opens_for_segment(conn, trading_day, trading_day, seg_key)
|
||||||
|
wm["opens_count"] = _count_opens_for_segment(conn, w_start, w_end, seg_key)
|
||||||
|
mm["opens_count"] = _count_opens_for_segment(conn, m_start, m_end, seg_key)
|
||||||
|
dm["range_label"] = f"北京时间交易日 {trading_day}({TRADING_DAY_RESET_HOUR}:00 切日)"
|
||||||
|
wm["range_label"] = f"{w_start} ~ {w_end}(北京日期,近7天)"
|
||||||
|
mm["range_label"] = f"{m_start} ~ {m_end}(北京自然月)"
|
||||||
|
return dm, wm, mm
|
||||||
|
|
||||||
dm = _compute_period_metrics(day_trades)
|
segments = []
|
||||||
wm = _compute_period_metrics(week_trades)
|
for seg_key, seg_title, _meta in STATS_SEGMENT_DEFS:
|
||||||
mm = _compute_period_metrics(month_trades)
|
dm, wm, mm = slice_metrics(seg_key)
|
||||||
dm["opens_count"] = _count_opens_between(conn, trading_day, trading_day)
|
segments.append({"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm})
|
||||||
wm["opens_count"] = _count_opens_between(conn, w_start, w_end)
|
|
||||||
mm["opens_count"] = _count_opens_between(conn, m_start, m_end)
|
dm, wm, mm = slice_metrics("all")
|
||||||
dm["range_label"] = f"北京时间交易日 {trading_day}"
|
|
||||||
wm["range_label"] = f"{w_start} ~ {w_end}(北京日期,近7天窗口)"
|
|
||||||
mm["range_label"] = f"{m_start} ~ {m_end}(北京时间自然月)"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"trading_day": trading_day,
|
"trading_day": trading_day,
|
||||||
@@ -1525,6 +1647,8 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
|
|||||||
"day": dm,
|
"day": dm,
|
||||||
"week": wm,
|
"week": wm,
|
||||||
"month": mm,
|
"month": mm,
|
||||||
|
"segments": segments,
|
||||||
|
"stats_reset_hour": TRADING_DAY_RESET_HOUR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1709,7 +1833,11 @@ def to_effective_trade_dict(row):
|
|||||||
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
||||||
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
||||||
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
||||||
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", base_stop)
|
open_stop = item.get("initial_stop_loss")
|
||||||
|
if open_stop in (None, ""):
|
||||||
|
open_stop = base_stop
|
||||||
|
item["display_open_stop_loss"] = open_stop
|
||||||
|
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
||||||
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
||||||
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
||||||
item["effective_miss_reason"] = get_effective_trade_field(row, "reviewed_miss_reason", "miss_reason", item.get("miss_reason"))
|
item["effective_miss_reason"] = get_effective_trade_field(row, "reviewed_miss_reason", "miss_reason", item.get("miss_reason"))
|
||||||
@@ -1999,6 +2127,7 @@ def insert_trade_record(
|
|||||||
closed_at_ms=None,
|
closed_at_ms=None,
|
||||||
exchange_trade_id=None,
|
exchange_trade_id=None,
|
||||||
key_signal_type=None,
|
key_signal_type=None,
|
||||||
|
entry_reason=None,
|
||||||
):
|
):
|
||||||
hold_minutes = calc_hold_minutes(hold_seconds)
|
hold_minutes = calc_hold_minutes(hold_seconds)
|
||||||
open_ts = opened_at or app_now_str()
|
open_ts = opened_at or app_now_str()
|
||||||
@@ -2006,13 +2135,15 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
|
snap_sl = initial_stop_loss if initial_stop_loss not in (None, "") else stop_loss
|
||||||
|
er = (entry_reason or "").strip() or entry_reason_from_key_signal(kst) or ""
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO trade_records (symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
"INSERT INTO trade_records (symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id,entry_reason) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(
|
(
|
||||||
symbol, monitor_type, kst, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
|
symbol, monitor_type, kst, direction, trigger_price, snap_sl, snap_sl, take_profit,
|
||||||
margin_capital, leverage, pnl_amount, hold_seconds,
|
margin_capital, leverage, pnl_amount, hold_seconds,
|
||||||
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
|
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
|
||||||
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id
|
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id, er or None
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5313,6 +5444,8 @@ def sync_trade_records_from_exchange(conn, force=False):
|
|||||||
def render_main_page(page="trade"):
|
def render_main_page(page="trade"):
|
||||||
now = app_now()
|
now = app_now()
|
||||||
trading_day = get_trading_day(now)
|
trading_day = get_trading_day(now)
|
||||||
|
list_window = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(list_window["start_utc"], list_window["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
session_row = ensure_session(conn, trading_day)
|
session_row = ensure_session(conn, trading_day)
|
||||||
local_current_capital = float(session_row["current_capital"])
|
local_current_capital = float(session_row["current_capital"])
|
||||||
@@ -5322,7 +5455,10 @@ def render_main_page(page="trade"):
|
|||||||
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
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)
|
recommended_capital = round(float(get_recommended_capital(current_capital)), 2)
|
||||||
key_list = conn.execute("SELECT * FROM key_monitors").fetchall()
|
key_list = conn.execute("SELECT * FROM key_monitors").fetchall()
|
||||||
key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall()
|
key_history = conn.execute(
|
||||||
|
"SELECT * FROM key_monitor_history WHERE closed_at >= ? AND closed_at <= ? ORDER BY id DESC LIMIT 500",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
stats_bundle = compute_stats_bundle(conn, trading_day, now)
|
stats_bundle = compute_stats_bundle(conn, trading_day, now)
|
||||||
raw_order_list = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
|
raw_order_list = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
|
||||||
order_list = []
|
order_list = []
|
||||||
@@ -5334,7 +5470,11 @@ def render_main_page(page="trade"):
|
|||||||
exchange_pnl_sync = sync_trade_records_from_exchange(conn) or {}
|
exchange_pnl_sync = sync_trade_records_from_exchange(conn) or {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exchange_pnl_sync = {"ok": False, "reason": str(e)}
|
exchange_pnl_sync = {"ok": False, "reason": str(e)}
|
||||||
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall()
|
raw_records = conn.execute(
|
||||||
|
"SELECT * FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? "
|
||||||
|
"AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id DESC LIMIT 1000",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
records = [to_effective_trade_dict(r) for r in raw_records]
|
records = [to_effective_trade_dict(r) for r in raw_records]
|
||||||
total = len(records)
|
total = len(records)
|
||||||
miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过")
|
miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过")
|
||||||
@@ -5386,7 +5526,14 @@ def render_main_page(page="trade"):
|
|||||||
can_trade=can_trade,
|
can_trade=can_trade,
|
||||||
focus_key_id=(key_list[0]["id"] if key_list else None),
|
focus_key_id=(key_list[0]["id"] if key_list else None),
|
||||||
focus_order_id=(order_list[0]["id"] if order_list else None),
|
focus_order_id=(order_list[0]["id"] if order_list else None),
|
||||||
data_export_version=2,
|
data_export_version=3,
|
||||||
|
list_window=list_window,
|
||||||
|
list_window_presets={
|
||||||
|
"utc_today": PRESET_UTC_TODAY,
|
||||||
|
"utc_last24h": PRESET_UTC_LAST24H,
|
||||||
|
"utc_last7d": PRESET_UTC_LAST7D,
|
||||||
|
"custom": PRESET_CUSTOM,
|
||||||
|
},
|
||||||
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
||||||
risk_percent=RISK_PERCENT,
|
risk_percent=RISK_PERCENT,
|
||||||
breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
||||||
@@ -6521,43 +6668,45 @@ def _md_response(filename, content):
|
|||||||
@app.route("/export/trade_records")
|
@app.route("/export/trade_records")
|
||||||
@login_required
|
@login_required
|
||||||
def export_trade_records():
|
def export_trade_records():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id,symbol,monitor_type,direction,trigger_price,stop_loss,take_profit,margin_capital,leverage,"
|
"SELECT id,symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,"
|
||||||
"pnl_amount,hold_seconds,hold_minutes,opened_at,closed_at,result,miss_reason,"
|
"margin_capital,leverage,pnl_amount,hold_seconds,hold_minutes,planned_rr,actual_rr,risk_amount,"
|
||||||
"entry_reason,reviewed_entry_reason,created_at FROM trade_records ORDER BY id ASC"
|
"opened_at,closed_at,result,miss_reason,entry_reason,reviewed_entry_reason,"
|
||||||
|
"exchange_realized_pnl,exchange_opened_at,exchange_closed_at,created_at "
|
||||||
|
"FROM trade_records WHERE COALESCE(closed_at, created_at, opened_at) >= ? "
|
||||||
|
"AND COALESCE(closed_at, created_at, opened_at) <= ? ORDER BY id ASC",
|
||||||
|
(start_bj, end_bj),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
head_base = [
|
head = [
|
||||||
"id",
|
"id", "symbol", "monitor_type", "key_signal_type", "direction", "trigger_price",
|
||||||
"symbol",
|
"stop_loss_open_snapshot", "initial_stop_loss", "take_profit", "margin_capital", "leverage",
|
||||||
"monitor_type",
|
"pnl_amount", "hold_seconds", "hold_minutes", "planned_rr", "actual_rr", "risk_amount",
|
||||||
"direction",
|
"opened_at", "closed_at", "result", "miss_reason", "entry_reason", "reviewed_entry_reason",
|
||||||
"trigger_price",
|
"exchange_realized_pnl", "exchange_opened_at", "exchange_closed_at", "created_at", "开仓类型",
|
||||||
"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",
|
|
||||||
]
|
]
|
||||||
head = head_base + ["开仓类型"]
|
|
||||||
data = []
|
data = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
er0 = (r["entry_reason"] or "").strip() if r["entry_reason"] else ""
|
er0 = (r["entry_reason"] or "").strip() if r["entry_reason"] else ""
|
||||||
er1 = (r["reviewed_entry_reason"] or "").strip() if r["reviewed_entry_reason"] else ""
|
er1 = (r["reviewed_entry_reason"] or "").strip() if r["reviewed_entry_reason"] else ""
|
||||||
eff = er1 or er0
|
kst = (r["key_signal_type"] or "").strip() if "key_signal_type" in r.keys() else ""
|
||||||
data.append(tuple(r[h] for h in head_base) + (eff,))
|
eff = er1 or er0 or entry_reason_from_key_signal(kst) or ""
|
||||||
|
snap = r["initial_stop_loss"] if r["initial_stop_loss"] not in (None, "") else r["stop_loss"]
|
||||||
|
data.append((
|
||||||
|
r["id"], r["symbol"], r["monitor_type"], kst, r["direction"], r["trigger_price"],
|
||||||
|
snap, r["initial_stop_loss"], r["take_profit"], r["margin_capital"], r["leverage"],
|
||||||
|
r["pnl_amount"], r["hold_seconds"], r["hold_minutes"], r["planned_rr"], r["actual_rr"], r["risk_amount"],
|
||||||
|
r["opened_at"], r["closed_at"], r["result"], r["miss_reason"], r["entry_reason"], r["reviewed_entry_reason"],
|
||||||
|
r["exchange_realized_pnl"] if "exchange_realized_pnl" in r.keys() else None,
|
||||||
|
r["exchange_opened_at"] if "exchange_opened_at" in r.keys() else None,
|
||||||
|
r["exchange_closed_at"] if "exchange_closed_at" in r.keys() else None,
|
||||||
|
r["created_at"], eff,
|
||||||
|
))
|
||||||
day = app_now().strftime("%Y%m%d")
|
day = app_now().strftime("%Y%m%d")
|
||||||
return _csv_response(f"trade_records_v2_{day}.csv", data, head)
|
return _csv_response(f"trade_records_v3_{day}.csv", data, head)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/export/journal_entries")
|
@app.route("/export/journal_entries")
|
||||||
@@ -6629,10 +6778,13 @@ def export_key_monitors():
|
|||||||
@app.route("/export/key_monitor_history")
|
@app.route("/export/key_monitor_history")
|
||||||
@login_required
|
@login_required
|
||||||
def export_key_monitor_history():
|
def export_key_monitor_history():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id,symbol,monitor_type,direction,upper,lower,notification_count,last_alert_message,close_reason,closed_at "
|
"SELECT id,symbol,monitor_type,direction,upper,lower,notification_count,last_alert_message,close_reason,closed_at "
|
||||||
"FROM key_monitor_history ORDER BY id ASC"
|
"FROM key_monitor_history WHERE closed_at >= ? AND closed_at <= ? ORDER BY id ASC",
|
||||||
|
(start_bj, end_bj),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
head = [
|
head = [
|
||||||
@@ -6862,9 +7014,10 @@ def add_journal():
|
|||||||
symbol_guess = normalize_symbol_input(coin) or coin
|
symbol_guess = normalize_symbol_input(coin) or coin
|
||||||
exchange_symbol = normalize_exchange_symbol(symbol_guess)
|
exchange_symbol = normalize_exchange_symbol(symbol_guess)
|
||||||
title_prefix = f"{symbol_guess} journal {entry_id[:8]}"
|
title_prefix = f"{symbol_guess} journal {entry_id[:8]}"
|
||||||
|
close_ms = _local_input_datetime_to_ms(d.get("close_datetime"))
|
||||||
marker_payload = {
|
marker_payload = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"exit_ts_ms": close_ms,
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"entry_ts_ms": close_ms,
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"entry_price": d.get("entry_price_hint"),
|
||||||
"exit_price": None,
|
"exit_price": None,
|
||||||
}
|
}
|
||||||
@@ -6929,8 +7082,14 @@ def add_journal():
|
|||||||
@app.route("/api/journals")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
def api_journals():
|
def api_journals():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute("SELECT * FROM journal_entries ORDER BY created_at DESC").fetchall()
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM journal_entries WHERE COALESCE(close_datetime, created_at, open_datetime) >= ? "
|
||||||
|
"AND COALESCE(close_datetime, created_at, open_datetime) <= ? ORDER BY created_at DESC LIMIT 500",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
result = []
|
result = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
@@ -6978,8 +7137,13 @@ def delete_journal(jid):
|
|||||||
@app.route("/api/reviews")
|
@app.route("/api/reviews")
|
||||||
@login_required
|
@login_required
|
||||||
def api_reviews():
|
def api_reviews():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute("SELECT * FROM ai_reviews ORDER BY created_at DESC").fetchall()
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM ai_reviews WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT 200",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,10 @@
|
|||||||
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
|
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
|
||||||
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
|
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
|
||||||
.export-bar a:hover{background:#1f2740}
|
.export-bar a:hover{background:#1f2740}
|
||||||
|
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
|
||||||
|
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
|
||||||
|
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
|
||||||
|
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
|
||||||
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
|
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
|
||||||
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
||||||
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
||||||
@@ -204,6 +208,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
||||||
|
|
||||||
|
<div class="list-window-bar">
|
||||||
|
<span style="color:#cfd3ef">列表筛选(<strong>UTC</strong>,默认当日):{{ list_window.label }}</span>
|
||||||
|
<label>预设
|
||||||
|
<select id="win-preset-select" onchange="toggleListWindowCustom()">
|
||||||
|
<option value="utc_today" {% if list_window.preset == 'utc_today' %}selected{% endif %}>UTC 当日</option>
|
||||||
|
<option value="utc_last24h" {% if list_window.preset == 'utc_last24h' %}selected{% endif %}>近 24 小时</option>
|
||||||
|
<option value="utc_last7d" {% if list_window.preset == 'utc_last7d' %}selected{% endif %}>近 7 天</option>
|
||||||
|
<option value="custom" {% if list_window.preset == 'custom' %}selected{% endif %}>自定义</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<span id="win-custom-range" style="{% if list_window.preset != 'custom' %}display:none{% endif %}">
|
||||||
|
<label>起(UTC) <input type="datetime-local" id="win-from-utc" value="{{ list_window.start_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
|
||||||
|
<label>止(UTC) <input type="datetime-local" id="win-to-utc" value="{{ list_window.end_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
|
||||||
|
</span>
|
||||||
|
<button type="button" style="padding:6px 12px" onclick="applyListWindow()">应用</button>
|
||||||
|
<span style="color:#8892b0;font-size:.75rem">统计页仍按北京时间 {{ stats_bundle.stats_reset_hour|default(reset_hour) }}:00 切日</span>
|
||||||
|
</div>
|
||||||
<div class="export-bar">
|
<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>
|
<a href="/export/trade_records">交易记录</a>
|
||||||
@@ -514,7 +535,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
|
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损(开仓)</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
|
||||||
{% for r in record %}
|
{% for r in record %}
|
||||||
<tr id="trade-row-{{ r.id }}">
|
<tr id="trade-row-{{ r.id }}">
|
||||||
{% set pnl_val = (r.pnl_amount or 0)|float %}
|
{% set pnl_val = (r.pnl_amount or 0)|float %}
|
||||||
@@ -522,7 +543,7 @@
|
|||||||
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
|
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
|
||||||
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
|
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
|
||||||
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
|
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
|
||||||
{% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
|
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
|
||||||
{% set tp_show = r.effective_take_profit or r.take_profit %}
|
{% set tp_show = r.effective_take_profit or r.take_profit %}
|
||||||
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
|
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
|
||||||
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
|
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
|
||||||
@@ -547,9 +568,10 @@
|
|||||||
onclick='fillJournalFromTrade({{ {
|
onclick='fillJournalFromTrade({{ {
|
||||||
"symbol": r.symbol,
|
"symbol": r.symbol,
|
||||||
"monitor_type": r.monitor_type,
|
"monitor_type": r.monitor_type,
|
||||||
|
"key_signal_type": r.key_signal_type or "",
|
||||||
"direction": r.direction,
|
"direction": r.direction,
|
||||||
"trigger_price": r.trigger_price,
|
"trigger_price": r.trigger_price,
|
||||||
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
|
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
|
||||||
"take_profit": r.effective_take_profit or r.take_profit,
|
"take_profit": r.effective_take_profit or r.take_profit,
|
||||||
"opened_at": r.effective_opened_at,
|
"opened_at": r.effective_opened_at,
|
||||||
"closed_at": r.effective_closed_at,
|
"closed_at": r.effective_closed_at,
|
||||||
@@ -629,6 +651,7 @@
|
|||||||
<input name="real_rr" placeholder="实际RR">
|
<input name="real_rr" placeholder="实际RR">
|
||||||
<select name="early_exit_trigger" required title="平仓如何触发">
|
<select name="early_exit_trigger" required title="平仓如何触发">
|
||||||
<option value="">离场触发(必选)</option>
|
<option value="">离场触发(必选)</option>
|
||||||
|
<option value="止盈">止盈</option>
|
||||||
<option value="保本止盈">保本止盈</option>
|
<option value="保本止盈">保本止盈</option>
|
||||||
<option value="移动止盈">移动止盈</option>
|
<option value="移动止盈">移动止盈</option>
|
||||||
<option value="手动平仓">手动平仓</option>
|
<option value="手动平仓">手动平仓</option>
|
||||||
@@ -702,12 +725,17 @@
|
|||||||
<div class="stat-item"><div class="label">持仓占用导致错过(累计)</div><div class="value">{{ occupied_miss_total }}</div></div>
|
<div class="stat-item"><div class="label">持仓占用导致错过(累计)</div><div class="value">{{ occupied_miss_total }}</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
|
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
|
||||||
已平仓「下单监控」按平仓时间归入<strong>北京时间</strong>下的交易日;胜率按盈笔数/(盈+亏)。历史总开仓(累计):
|
统计分析按<strong>北京时间 {{ stats_bundle.stats_reset_hour }}:00</strong>切日计入;下列为各品类已平仓。历史总开仓(累计):
|
||||||
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong> 次
|
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong> 次
|
||||||
</div>
|
</div>
|
||||||
{{ period_stats("日统计", stats_bundle.day) }}
|
{% for seg in stats_bundle.segments %}
|
||||||
{{ period_stats("周统计", stats_bundle.week) }}
|
<div class="stats-segment-block">
|
||||||
{{ period_stats("月统计", stats_bundle.month) }}
|
<h2>{{ seg.title }}</h2>
|
||||||
|
{{ period_stats("日统计", seg.day) }}
|
||||||
|
{{ period_stats("周统计", seg.week) }}
|
||||||
|
{{ period_stats("月统计", seg.month) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -931,8 +959,50 @@ function deleteKeyHistory(id){
|
|||||||
.catch(()=>{ window.location.href = `${window.location.pathname}?_ts=${Date.now()}`; });
|
.catch(()=>{ window.location.href = `${window.location.pathname}?_ts=${Date.now()}`; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listWindowQueryString(){
|
||||||
|
const presetEl = document.getElementById("win-preset-select");
|
||||||
|
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_today";
|
||||||
|
const q = new URLSearchParams(window.location.search);
|
||||||
|
q.set("win_preset", preset);
|
||||||
|
if(preset === "custom"){
|
||||||
|
const fromEl = document.getElementById("win-from-utc");
|
||||||
|
const toEl = document.getElementById("win-to-utc");
|
||||||
|
if(fromEl && fromEl.value) q.set("from_utc", fromEl.value.replace("T", " ") + ":00");
|
||||||
|
else q.delete("from_utc");
|
||||||
|
if(toEl && toEl.value) q.set("to_utc", toEl.value.replace("T", " ") + ":00");
|
||||||
|
else q.delete("to_utc");
|
||||||
|
} else {
|
||||||
|
q.delete("from_utc");
|
||||||
|
q.delete("to_utc");
|
||||||
|
}
|
||||||
|
return q.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleListWindowCustom(){
|
||||||
|
const preset = document.getElementById("win-preset-select");
|
||||||
|
const box = document.getElementById("win-custom-range");
|
||||||
|
if(!preset || !box) return;
|
||||||
|
box.style.display = preset.value === "custom" ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyListWindow(){
|
||||||
|
const qs = listWindowQueryString();
|
||||||
|
const path = window.location.pathname || "/trade";
|
||||||
|
window.location.href = qs ? (path + "?" + qs) : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachListWindowToExports(){
|
||||||
|
const qs = listWindowQueryString();
|
||||||
|
if(!qs) return;
|
||||||
|
document.querySelectorAll('.export-bar a[href^="/export/trade_records"], .export-bar a[href^="/export/key_monitor_history"]').forEach(a=>{
|
||||||
|
const base = a.getAttribute("href").split("?")[0];
|
||||||
|
a.setAttribute("href", base + "?" + qs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadJournals(){
|
function loadJournals(){
|
||||||
fetch("/api/journals").then(r=>r.json()).then(data=>{
|
const qs = listWindowQueryString();
|
||||||
|
fetch("/api/journals" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
|
||||||
Object.keys(journalCache).forEach(k=>delete journalCache[k]);
|
Object.keys(journalCache).forEach(k=>delete journalCache[k]);
|
||||||
let html="";
|
let html="";
|
||||||
data.forEach(o=>{
|
data.forEach(o=>{
|
||||||
@@ -954,7 +1024,8 @@ function loadJournals(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadReviews(){
|
function loadReviews(){
|
||||||
fetch("/api/reviews").then(r=>r.json()).then(data=>{
|
const qs = listWindowQueryString();
|
||||||
|
fetch("/api/reviews" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
|
||||||
Object.keys(reviewCache).forEach(k=>delete reviewCache[k]);
|
Object.keys(reviewCache).forEach(k=>delete reviewCache[k]);
|
||||||
let html="";
|
let html="";
|
||||||
data.forEach(r=>{
|
data.forEach(r=>{
|
||||||
@@ -1026,7 +1097,13 @@ function setJournalField(name, value){
|
|||||||
el.value = String(value);
|
el.value = String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EARLY_EXIT_TRIGGERS = new Set(["保本止盈","移动止盈","手动平仓","止损","其他"]);
|
const EARLY_EXIT_TRIGGERS = new Set(["止盈","保本止盈","移动止盈","手动平仓","止损","其他"]);
|
||||||
|
const KEY_ENTRY_REASON_BY_SIGNAL = {
|
||||||
|
"箱体突破": "关键位箱体突破",
|
||||||
|
"收敛突破": "关键位收敛突破",
|
||||||
|
"斐波回调0.618": "关键位斐波0.618",
|
||||||
|
"斐波回调0.786": "关键位斐波0.786"
|
||||||
|
};
|
||||||
|
|
||||||
function splitLegacyEarlyExitReason(raw){
|
function splitLegacyEarlyExitReason(raw){
|
||||||
const s = String(raw || "").trim();
|
const s = String(raw || "").trim();
|
||||||
@@ -1116,11 +1193,17 @@ function fillJournalFromTrade(t){
|
|||||||
if(dirHint){ dirHint.value = t.direction || "long"; }
|
if(dirHint){ dirHint.value = t.direction || "long"; }
|
||||||
setJournalField("early_exit_trigger", "");
|
setJournalField("early_exit_trigger", "");
|
||||||
setJournalField("early_exit_note", "");
|
setJournalField("early_exit_note", "");
|
||||||
|
const kst = String(t.key_signal_type || "").trim();
|
||||||
|
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
|
||||||
|
if(erFromKey && JOURNAL_ENTRY_REASON_OPTIONS.includes(erFromKey)){
|
||||||
|
setJournalField("entry_reason", erFromKey);
|
||||||
|
} else {
|
||||||
setJournalField("entry_reason", "");
|
setJournalField("entry_reason", "");
|
||||||
|
}
|
||||||
setJournalField("entry_reason_custom", "");
|
setJournalField("entry_reason_custom", "");
|
||||||
syncJournalEntryReasonOtherUi();
|
syncJournalEntryReasonOtherUi();
|
||||||
const er = String(t.result || "").trim();
|
const er = String(t.result || "").trim();
|
||||||
const exitTrigMap = { 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
|
const exitTrigMap = { 止盈: "止盈", 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
|
||||||
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
|
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
|
||||||
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${t.trigger_price || "-"} 止损:${t.stop_loss || "-"} 止盈:${t.take_profit || "-"} | 类型:${t.monitor_type || "-"}`;
|
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${t.trigger_price || "-"} 止损:${t.stop_loss || "-"} 止盈:${t.take_profit || "-"} | 类型:${t.monitor_type || "-"}`;
|
||||||
setJournalField("note", note);
|
setJournalField("note", note);
|
||||||
@@ -1211,6 +1294,8 @@ function toggleStatsCard(){
|
|||||||
btn.innerText = collapsed ? "展开" : "折叠";
|
btn.innerText = collapsed ? "展开" : "折叠";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachListWindowToExports();
|
||||||
|
toggleListWindowCustom();
|
||||||
if(document.getElementById("journal-list")) loadJournals();
|
if(document.getElementById("journal-list")) loadJournals();
|
||||||
if(document.getElementById("review-list")) loadReviews();
|
if(document.getElementById("review-list")) loadReviews();
|
||||||
const reviewToggle = document.getElementById("review-mode-toggle");
|
const reviewToggle = document.getElementById("review-mode-toggle");
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 1 | 关键位监控 | `/key_monitor` | 关键位添加、实时门控、历史 |
|
| 1 | 关键位监控 | `/key_monitor` | 关键位添加、实时门控、历史 |
|
||||||
| 2 | 实盘下单 | `/trade` | 人工开仓、划转、实时持仓(**默认首页** `/` → `/trade`) |
|
| 2 | 实盘下单 | `/trade` | 人工开仓、划转、实时持仓(**默认首页** `/` → `/trade`) |
|
||||||
| 3 | 交易记录与复盘 | `/records` | 未改动 |
|
| 3 | 交易记录与复盘 | `/records` | 交易记录、复盘表单、AI 历史(受顶栏 UTC 时间窗筛选) |
|
||||||
| 4 | 统计分析 | `/stats` | 未改动 |
|
| 4 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
|
||||||
|
|
||||||
## 关键位监控页
|
## 关键位监控页
|
||||||
|
|
||||||
- 标题去掉「5m」;规则条从 `.env` 读取(周期、确认K、量能、自动开仓盈亏比、日成交量排名)。
|
- 标题去掉「5m」;规则条从 `.env` 读取(周期、确认K、量能、自动开仓盈亏比、日成交量排名)。
|
||||||
- 左列:活跃关键位,**pos-card** 样式展示现价/距上沿/距下沿/门控。
|
- 左列:活跃关键位,**pos-card** 样式展示现价/距上沿/距下沿/门控。
|
||||||
- 右列:关键位历史(失效/结案),与左列等高滚动。
|
- 右列:关键位历史(失效/结案),与左列等高滚动;**受顶栏 UTC 列表时间窗筛选**(默认 UTC 当日)。
|
||||||
- 监控类型新增:**斐波回调0.618**、**斐波回调0.786**(与 Binance 主站同一套规则,计算逻辑见仓库根目录 `fib_key_monitor_lib.py`)。
|
- 监控类型新增:**斐波回调0.618**、**斐波回调0.786**(与 Binance 主站同一套规则,计算逻辑见仓库根目录 `fib_key_monitor_lib.py`)。
|
||||||
|
|
||||||
### 斐波关键位监控(方案 A:交易所限价)
|
### 斐波关键位监控(方案 A:交易所限价)
|
||||||
@@ -41,11 +41,52 @@
|
|||||||
- 自动开仓写入 `order_monitors.key_signal_type`:`箱体突破` 或 `收敛突破`。
|
- 自动开仓写入 `order_monitors.key_signal_type`:`箱体突破` 或 `收敛突破`。
|
||||||
- 持仓卡片、交易记录列表会显示「来源 · 信号类型」。
|
- 持仓卡片、交易记录列表会显示「来源 · 信号类型」。
|
||||||
|
|
||||||
|
## 列表时间窗(UTC,全站顶栏)
|
||||||
|
|
||||||
|
共用模块:仓库根目录 `history_window_lib.py`(Gate / Binance 主站一致)。
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| 默认 | **UTC 当日**(`win_preset=utc_today`,从 UTC 0:00 至当前时刻) |
|
||||||
|
| 可选 | 近 24 小时、近 7 天、自定义起止(UTC,`datetime-local`) |
|
||||||
|
| 作用范围 | 关键位历史、交易记录列表、复盘记录 API、AI 历史 API、导出「交易记录」「关键位历史」 |
|
||||||
|
| 与统计的关系 | **仅影响列表/导出**;**统计分析页仍按北京时间 `TRADING_DAY_RESET_HOUR`(默认 8:00)切交易日** |
|
||||||
|
| 库内时间 | DB 存北京时间字符串;后端用 `utc_window_to_bj_sql_strings()` 换算后再 SQL 比较 |
|
||||||
|
| 切换方式 | 顶栏「列表筛选(UTC)」→ 选预设 → **应用**(保留当前路由,如 `/records?win_preset=…`) |
|
||||||
|
|
||||||
|
查询参数示例:
|
||||||
|
|
||||||
|
- `?win_preset=utc_today`
|
||||||
|
- `?win_preset=utc_last24h` / `utc_last7d`
|
||||||
|
- `?win_preset=custom&from_utc=2026-05-18 00:00:00&to_utc=2026-05-19 12:00:00`
|
||||||
|
|
||||||
## 交易记录与复盘
|
## 交易记录与复盘
|
||||||
|
|
||||||
- 平仓记录可同步交易所已实现盈亏(Gate 仓位历史等);列表盈亏列优先显示交易所数据,标注 **所** / **估**。
|
- 平仓记录可同步交易所已实现盈亏(Gate 仓位历史等);列表盈亏列优先显示交易所数据,标注 **所** / **估**。
|
||||||
- 记录页提供 **立即同步**(`POST /api/sync_exchange_pnl`),用于补全或刷新 `exchange_realized_pnl` 等字段。
|
- 记录页提供 **立即同步**(`POST /api/sync_exchange_pnl`),用于补全或刷新 `exchange_realized_pnl` 等字段。
|
||||||
- 未做人工复盘时,展示以交易所盈亏为准(有同步数据时)。
|
- 未做人工复盘时,展示以交易所盈亏为准(有同步数据时)。
|
||||||
|
- **列表默认只显示当前 UTC 时间窗内**的记录(见上节);导出 CSV 同步该时间窗。
|
||||||
|
- 表头 **「止损(开仓)」**:展示开仓快照 `initial_stop_loss`(无则回退 `stop_loss`);核对/复盘仍可用有效止损字段。
|
||||||
|
- 平仓写入 `trade_records` 时:`stop_loss` 与 `initial_stop_loss` 均写入**开仓时止损快照**;`key_signal_type` 保留箱体/收敛/斐波来源(`fib_key_monitor_lib.key_signal_type_for_trade_record`)。
|
||||||
|
- **开仓类型**(`entry_reason`):机器单平仓入库时,若未手填,按 `key_signal_type` 自动映射(见下表);列表/导出「开仓类型」列 = 复盘核对值优先,否则入库值,否则按信号映射。
|
||||||
|
|
||||||
|
| `key_signal_type` | 自动写入的 `entry_reason` |
|
||||||
|
|-------------------|---------------------------|
|
||||||
|
| 箱体突破 | 关键位箱体突破 |
|
||||||
|
| 收敛突破 | 关键位收敛突破 |
|
||||||
|
| 斐波回调0.618 | 关键位斐波0.618 |
|
||||||
|
| 斐波回调0.786 | 关键位斐波0.786 |
|
||||||
|
|
||||||
|
- 复盘表单 **开仓类型** 下拉新增上述四条固定文案(与趋势/波段类并列)。
|
||||||
|
- 复盘 **离场触发** 新增 **「止盈」**;从交易记录「填入复盘」时,若结果为「止盈/保本止盈/移动止盈/止损/手动平仓」会自动选中对应触发项,并按 `key_signal_type` 预填开仓类型。
|
||||||
|
- 勾选「保存时自动生成多周期 K 线图」时:以 **平仓时间** 为锚点,各周期向前约 `ORDER_CHART_LIMIT`(默认 100)根 K 线(`_fetch_ohlcv_ending_at`),不再固定拉「最近 100 根」。
|
||||||
|
- `/api/journals`、`/api/reviews` 支持同一时间窗 query,与列表一致。
|
||||||
|
|
||||||
|
### 导出(交易记录 v3)
|
||||||
|
|
||||||
|
- 文件名:`trade_records_v3_YYYYMMDD.csv`
|
||||||
|
- 相对 v2 增加:`key_signal_type`、`initial_stop_loss`(及开仓快照列)、`planned_rr`、`actual_rr`、`risk_amount`、交易所盈亏与时间字段等;末列「开仓类型」为有效展示文案。
|
||||||
|
- 「关键位历史」导出同样受 UTC 时间窗限制。
|
||||||
|
|
||||||
## 实盘下单页
|
## 实盘下单页
|
||||||
|
|
||||||
@@ -53,6 +94,16 @@
|
|||||||
- 右列:实时持仓(独立模块)。
|
- 右列:实时持仓(独立模块)。
|
||||||
- **人工开仓门控**:计划盈亏比 < `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝。
|
- **人工开仓门控**:计划盈亏比 < `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝。
|
||||||
|
|
||||||
|
## 统计分析页(`/stats`)
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| 切日 | **北京时间**;交易日边界 = 每日 `TRADING_DAY_RESET_HOUR:00`(`.env` 默认 **8**) |
|
||||||
|
| 分块 | 页内按品类各一块:**全部已平仓**、**人工·下单监控**、**关键位箱体突破**、**关键位收敛突破**、**关键位斐波0.618**、**关键位斐波0.786** |
|
||||||
|
| 每块指标 | 日 / 周 / 月:开单次数、平仓笔数、胜率、净盈亏、回撤、连续亏损等(与原口径一致) |
|
||||||
|
| 开单次数 | 人工块:`monitor_type=下单监控` 且无 `key_signal_type`;关键位块:按 `order_monitors.key_signal_type` 计数 |
|
||||||
|
| 不受 UTC 窗影响 | 统计始终基于库内全部已平仓记录,按北京交易日归类,**不**随顶栏 UTC 列表窗切换 |
|
||||||
|
|
||||||
## 持仓与计仓
|
## 持仓与计仓
|
||||||
|
|
||||||
- `MAX_ACTIVE_POSITIONS` 默认 **1**(可在 `.env` 调大)。
|
- `MAX_ACTIVE_POSITIONS` 默认 **1**(可在 `.env` 调大)。
|
||||||
@@ -72,12 +123,24 @@
|
|||||||
|
|
||||||
`key_monitors` 新增斐波字段(示例):`fib_limit_order_id`、`fib_entry_price`、`fib_stop_loss`、`fib_take_profit`、`fib_order_amount`、`fib_margin_capital`、`fib_leverage`。
|
`key_monitors` 新增斐波字段(示例):`fib_limit_order_id`、`fib_entry_price`、`fib_stop_loss`、`fib_take_profit`、`fib_order_amount`、`fib_margin_capital`、`fib_leverage`。
|
||||||
|
|
||||||
`trade_records` / `order_monitors` 新增或沿用:`key_signal_type`、`exchange_realized_pnl`、`exchange_opened_at`、`exchange_closed_at`、`exchange_sync_key`。
|
`trade_records` / `order_monitors` 新增或沿用:`key_signal_type`、`exchange_realized_pnl`、`exchange_opened_at`、`exchange_closed_at`、`exchange_sync_key`、`entry_reason`、`reviewed_entry_reason`、`initial_stop_loss`。
|
||||||
|
|
||||||
|
**历史数据**:本次**不做**旧记录的批量回填(`entry_reason` / `initial_stop_loss` / `key_signal_type` 等);仅**新产生**的平仓与复盘按新逻辑写入。旧行展示可回退已有字段。
|
||||||
|
|
||||||
|
## 涉及文件(便于排查)
|
||||||
|
|
||||||
|
| 路径 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `history_window_lib.py` | UTC 时间窗解析与转北京时间 SQL 字符串 |
|
||||||
|
| `fib_key_monitor_lib.py` | 斐波计算、`KEY_ENTRY_REASON_BY_SIGNAL`、`entry_reason_from_key_signal` |
|
||||||
|
| `crypto_monitor_gate/app.py` | 列表筛选、统计分块、导出 v3、复盘 K 线锚点、入库逻辑 |
|
||||||
|
| `crypto_monitor_gate/templates/index.html` | 顶栏时间窗、统计分块 UI、止损(开仓)列、复盘预填 |
|
||||||
|
|
||||||
## 升级步骤
|
## 升级步骤
|
||||||
|
|
||||||
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`。
|
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`。
|
||||||
2. 在 VPS 上为 Binance / Gate / Gate Bot **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。
|
2. 在 VPS 上为 Binance / Gate / Gate Bot **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。
|
||||||
3. 重启 Gate 实例服务(如 `pm2 restart crypto_gate`);首次启动会自动 `ALTER TABLE` 斐波与交易所盈亏相关列。
|
3. 重启 Gate 实例服务(如 `pm2 restart crypto_gate`);首次启动会自动 `ALTER TABLE` 缺列(斐波、交易所盈亏、`entry_reason` 等)。
|
||||||
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。
|
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。
|
||||||
5. 建议在测试币上先添加一条斐波监控,确认:限价已挂出、标记价失效会撤单、成交后出现持仓监控且 TP/SL 已挂上。
|
5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**;`/stats` 可见分品类统计与「北京 8:00 切日」说明。
|
||||||
|
6. 建议在测试币上先添加一条斐波监控,确认:限价已挂出、标记价失效会撤单、成交后出现持仓监控且 TP/SL 已挂上;平仓后交易记录止损(开仓)与开仓类型是否正确。
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ def stored_key_signal_type(monitor_type):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
KEY_ENTRY_REASON_BY_SIGNAL = {
|
||||||
|
"箱体突破": "关键位箱体突破",
|
||||||
|
"收敛突破": "关键位收敛突破",
|
||||||
|
"斐波回调0.618": "关键位斐波0.618",
|
||||||
|
"斐波回调0.786": "关键位斐波0.786",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def entry_reason_from_key_signal(key_signal_type):
|
||||||
|
return KEY_ENTRY_REASON_BY_SIGNAL.get((key_signal_type or "").strip())
|
||||||
|
|
||||||
|
|
||||||
def key_signal_type_for_trade_record(key_signal_type, box_auto_types):
|
def key_signal_type_for_trade_record(key_signal_type, box_auto_types):
|
||||||
"""平仓写入 trade_records 时保留箱体/收敛/斐波来源。"""
|
"""平仓写入 trade_records 时保留箱体/收敛/斐波来源。"""
|
||||||
kst = (key_signal_type or "").strip()
|
kst = (key_signal_type or "").strip()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""列表/导出用 UTC 时间窗(Gate / Binance 主站共用)。"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
PRESET_UTC_TODAY = "utc_today"
|
||||||
|
PRESET_UTC_LAST24H = "utc_last24h"
|
||||||
|
PRESET_UTC_LAST7D = "utc_last7d"
|
||||||
|
PRESET_CUSTOM = "custom"
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now():
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def utc_today_bounds(now=None):
|
||||||
|
now = now or utc_now()
|
||||||
|
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
return start, now
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_window(query_mapping, default_preset=PRESET_UTC_TODAY):
|
||||||
|
"""
|
||||||
|
从 ?win_preset= & from_utc= & to_utc= 解析窗口。
|
||||||
|
返回 dict: preset, start_utc, end_utc, label, start_ms, end_ms
|
||||||
|
"""
|
||||||
|
preset = (query_mapping.get("win_preset") or default_preset or PRESET_UTC_TODAY).strip().lower()
|
||||||
|
now = utc_now()
|
||||||
|
|
||||||
|
if preset == PRESET_UTC_LAST24H:
|
||||||
|
start = now - timedelta(hours=24)
|
||||||
|
end = now
|
||||||
|
label = "近24小时(UTC)"
|
||||||
|
elif preset == PRESET_UTC_LAST7D:
|
||||||
|
start = now - timedelta(days=7)
|
||||||
|
end = now
|
||||||
|
label = "近7天(UTC)"
|
||||||
|
elif preset == PRESET_CUSTOM:
|
||||||
|
start = _parse_utc_input(query_mapping.get("from_utc")) or utc_today_bounds(now)[0]
|
||||||
|
end = _parse_utc_input(query_mapping.get("to_utc")) or now
|
||||||
|
if end < start:
|
||||||
|
start, end = end, start
|
||||||
|
label = f"{start.strftime('%Y-%m-%d %H:%M')} ~ {end.strftime('%Y-%m-%d %H:%M')} UTC"
|
||||||
|
else:
|
||||||
|
start, end = utc_today_bounds(now)
|
||||||
|
preset = PRESET_UTC_TODAY
|
||||||
|
label = f"UTC当日 {start.strftime('%Y-%m-%d')}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"preset": preset,
|
||||||
|
"start_utc": start,
|
||||||
|
"end_utc": end,
|
||||||
|
"label": label,
|
||||||
|
"start_ms": int(start.timestamp() * 1000),
|
||||||
|
"end_ms": int(end.timestamp() * 1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_utc_input(raw):
|
||||||
|
s = (raw or "").strip().replace("T", " ").replace("Z", "").strip()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
for fmt, n in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(s[:n], fmt)
|
||||||
|
return dt.replace(tzinfo=timezone.utc)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def utc_window_to_bj_sql_strings(start_utc, end_utc, app_tz):
|
||||||
|
"""DB 存北京时间字符串时,用于 SQLite 字符串范围比较。"""
|
||||||
|
start_bj = start_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
end_bj = end_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
return start_bj, end_bj
|
||||||
Reference in New Issue
Block a user