diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 0275dbe..c7b58c9 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -37,12 +37,21 @@ if _REPO_ROOT not in sys.path: from fib_key_monitor_lib import ( FIB_KEY_MONITOR_TYPES, calc_fib_plan, + entry_reason_from_key_signal, fib_invalidate_by_mark, fib_ratio_from_type, is_fib_key_monitor_type, key_signal_type_for_trade_record, 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): 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 +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( exchange_symbol, title_prefix, @@ -753,9 +797,15 @@ def generate_multi_timeframe_chart_png( ensure_markets_loaded() panels = [] 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: 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: ohlcv = [] rows = _ohlcv_to_rows(ohlcv)[-limit:] @@ -847,6 +897,7 @@ def journal_coin_from_symbol(symbol): EARLY_EXIT_TRIGGERS = ( "", + "止盈", "保本止盈", "移动止盈", "手动平仓", @@ -861,6 +912,19 @@ ENTRY_REASON_OPTIONS = ( "趋势多头:小分歧低吸入场(左侧),确认条件:二次探底", "趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶", "波段单: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) ENTRY_REASON_OTHER = "__OTHER__" @@ -1395,17 +1459,64 @@ def _calendar_month_bounds(local_dt): 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( "SELECT COUNT(*) FROM order_monitors WHERE session_date >= ? AND session_date <= ?", (start_td, end_td), ).fetchone()[0] -def _load_completed_live_pnls(conn): - q = """SELECT pnl_amount, reviewed_pnl_amount, closed_at, reviewed_closed_at, created_at, - result, reviewed_result +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 - WHERE monitor_type = '下单监控' ORDER BY COALESCE(closed_at, created_at, opened_at) ASC, id ASC""" rows = conn.execute(q).fetchall() out = [] @@ -1419,7 +1530,7 @@ def _load_completed_live_pnls(conn): 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"]) td = get_trading_day(t) if t else None - out.append((p, t, td)) + out.append((p, t, td, r)) return out @@ -1488,34 +1599,35 @@ def _compute_period_metrics(trades): def compute_stats_bundle(conn, trading_day, now_dt=None): - """日 / 周 / 月 统计:平仓按平仓时间所在交易日计入。""" + """日 / 周 / 月 统计:平仓按北京时间交易日(默认 8:00 切日)计入。""" 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] w_start, w_end = _session_week_bounds(trading_day) m_start, m_end = _calendar_month_bounds(now_dt) - def in_week(tr): - _p, _t, td = tr - return td and w_start <= td <= w_end + def slice_metrics(seg_key): + seg_rows = [tr for tr in pnls if _pnl_row_matches_segment(tr[3], seg_key)] + 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): - _p, _t, td = tr - return td and m_start <= td <= m_end + segments = [] + for seg_key, seg_title, _meta in STATS_SEGMENT_DEFS: + 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] - 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}(北京时间自然月)" + dm, wm, mm = slice_metrics("all") return { "trading_day": trading_day, @@ -1523,6 +1635,8 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): "day": dm, "week": wm, "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") 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_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_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")) @@ -2021,6 +2139,7 @@ def insert_trade_record( closed_at_ms=None, exchange_trade_id=None, key_signal_type=None, + entry_reason=None, ): hold_minutes = calc_hold_minutes(hold_seconds) 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) 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) + 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( - "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, 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"): now = app_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() session_row = ensure_session(conn, trading_day) 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) recommended_capital = get_recommended_capital(current_capital) key_list = conn.execute("SELECT * FROM key_monitors").fetchall() - key_history = conn.execute("SELECT * FROM key_monitor_history ORDER BY id DESC LIMIT 80").fetchall() + 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) raw_order_list = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall() order_list = [] @@ -5120,7 +5246,11 @@ def render_main_page(page="trade"): sync_trade_records_from_exchange(conn) except Exception: pass - raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall() + raw_records = conn.execute( + "SELECT * FROM trade_records 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] total = len(records) 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, focus_key_id=(key_list[0]["id"] if key_list else None), focus_order_id=(order_list[0]["id"] if order_list else None), - data_export_version=2, + data_export_version=3, + 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, risk_percent=RISK_PERCENT, breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER, @@ -6241,43 +6378,45 @@ def _md_response(filename, content): @app.route("/export/trade_records") @login_required 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() rows = conn.execute( - "SELECT id,symbol,monitor_type,direction,trigger_price,stop_loss,take_profit,margin_capital,leverage," - "pnl_amount,hold_seconds,hold_minutes,opened_at,closed_at,result,miss_reason," - "entry_reason,reviewed_entry_reason,created_at FROM trade_records ORDER BY id ASC" + "SELECT id,symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit," + "margin_capital,leverage,pnl_amount,hold_seconds,hold_minutes,planned_rr,actual_rr,risk_amount," + "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() conn.close() - head_base = [ - "id", - "symbol", - "monitor_type", - "direction", - "trigger_price", - "stop_loss", - "take_profit", - "margin_capital", - "leverage", - "pnl_amount", - "hold_seconds", - "hold_minutes", - "opened_at", - "closed_at", - "result", - "miss_reason", - "entry_reason", - "reviewed_entry_reason", - "created_at", + head = [ + "id", "symbol", "monitor_type", "key_signal_type", "direction", "trigger_price", + "stop_loss_open_snapshot", "initial_stop_loss", "take_profit", "margin_capital", "leverage", + "pnl_amount", "hold_seconds", "hold_minutes", "planned_rr", "actual_rr", "risk_amount", + "opened_at", "closed_at", "result", "miss_reason", "entry_reason", "reviewed_entry_reason", + "exchange_realized_pnl", "exchange_opened_at", "exchange_closed_at", "created_at", "开仓类型", ] - head = head_base + ["开仓类型"] data = [] for r in rows: er0 = (r["entry_reason"] or "").strip() if r["entry_reason"] else "" er1 = (r["reviewed_entry_reason"] or "").strip() if r["reviewed_entry_reason"] else "" - eff = er1 or er0 - data.append(tuple(r[h] for h in head_base) + (eff,)) + kst = (r["key_signal_type"] or "").strip() if "key_signal_type" in r.keys() else "" + 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") - 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") @@ -6349,10 +6488,13 @@ def export_key_monitors(): @app.route("/export/key_monitor_history") @login_required 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() rows = conn.execute( "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() conn.close() head = [ @@ -6567,9 +6709,10 @@ def add_journal(): symbol_guess = normalize_symbol_input(coin) or coin exchange_symbol = normalize_exchange_symbol(symbol_guess) title_prefix = f"{symbol_guess} journal {entry_id[:8]}" + close_ms = _local_input_datetime_to_ms(d.get("close_datetime")) marker_payload = { - "entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")), - "exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")), + "exit_ts_ms": close_ms, + "entry_ts_ms": close_ms, "entry_price": d.get("entry_price_hint"), "exit_price": None, } @@ -6634,8 +6777,14 @@ def add_journal(): @app.route("/api/journals") @login_required 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() - 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() result = [] for r in rows: @@ -6683,8 +6832,13 @@ def delete_journal(jid): @app.route("/api/reviews") @login_required 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() - 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() return jsonify([row_to_dict(r) for r in rows]) diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 77f6ea0..eb5d626 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -110,6 +110,10 @@ .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: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 h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px} .key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px} @@ -204,6 +208,23 @@ {% with msg=get_flashed_messages() %}{% if msg %}
{{ msg[0] }}
{% endif %}{% endwith %} +
+ 列表筛选(UTC,默认当日):{{ list_window.label }} + + + + + + + 统计页仍按北京时间 {{ stats_bundle.stats_reset_hour|default(reset_hour) }}:00 切日 +
数据导出(v{{ data_export_version }} CSV,UTF-8;交易记录含开仓类型列,复盘单独导出): 交易记录 @@ -504,7 +525,7 @@
- + {% for r in record %} {% set pnl_val = (r.pnl_amount or 0)|float %} @@ -512,7 +533,7 @@ - {% 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 %} @@ -537,9 +558,10 @@ onclick='fillJournalFromTrade({{ { "symbol": r.symbol, "monitor_type": r.monitor_type, + "key_signal_type": r.key_signal_type or "", "direction": r.direction, "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, "opened_at": r.effective_opened_at, "closed_at": r.effective_closed_at, @@ -619,6 +641,7 @@ + + + + + + + + + + + + 统计页仍按北京时间 {{ stats_bundle.stats_reset_hour|default(reset_hour) }}:00 切日 +
数据导出(v{{ data_export_version }} CSV,UTF-8;交易记录含开仓类型列,复盘单独导出): 交易记录 @@ -514,7 +535,7 @@
品种类型方向成交止损止盈基数杠杆持仓分钟开仓时间(北京)平仓时间(北京)盈亏U结果操作
品种类型方向成交止损(开仓)止盈基数杠杆持仓分钟开仓时间(北京)平仓时间(北京)盈亏U结果操作
{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %} {{ '做多' if r.direction == 'long' else '做空' }} {{ price_fmt(r.symbol, r.trigger_price) }}{{ price_fmt(r.symbol, stop_show) }} {{ price_fmt(r.symbol, tp_show) }}
- + {% for r in record %} {% set pnl_val = (r.pnl_amount or 0)|float %} @@ -522,7 +543,7 @@ - {% 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 %} @@ -547,9 +568,10 @@ onclick='fillJournalFromTrade({{ { "symbol": r.symbol, "monitor_type": r.monitor_type, + "key_signal_type": r.key_signal_type or "", "direction": r.direction, "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, "opened_at": r.effective_opened_at, "closed_at": r.effective_closed_at, @@ -629,6 +651,7 @@
品种类型方向成交止损止盈基数杠杆持仓分钟开仓时间(北京)平仓时间(北京)盈亏U结果操作
品种类型方向成交止损(开仓)止盈基数杠杆持仓分钟开仓时间(北京)平仓时间(北京)盈亏U结果操作
{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %} {{ '做多' if r.direction == 'long' else '做空' }} {{ price_fmt(r.symbol, r.trigger_price) }}{{ price_fmt(r.symbol, stop_show) }} {{ price_fmt(r.symbol, tp_show) }}