From 6093bf6b941b0f453a9e391296bac300624598a1 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 27 May 2026 15:32:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_binance/app.py | 37 +++++- crypto_monitor_binance/templates/index.html | 7 +- crypto_monitor_gate/app.py | 37 +++++- crypto_monitor_gate/templates/index.html | 7 +- crypto_monitor_gate_bot/app.py | 37 +++++- crypto_monitor_gate_bot/templates/index.html | 7 +- crypto_monitor_okx/app.py | 37 +++++- crypto_monitor_okx/templates/index.html | 7 +- journal_chart_lib.py | 117 +++++++++++++++++++ 9 files changed, 269 insertions(+), 24 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 53ed3d6..e1a28bf 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -52,10 +52,14 @@ from journal_chart_lib import ( JOURNAL_CHART_TF_CHOICES, compose_chart_panels, marker_points_for_timeframe, + parse_journal_chart_anchor, parse_journal_chart_limit, parse_journal_chart_timeframes, + JOURNAL_CHART_DEFAULT_ANCHOR, price_levels_from_marker_payload, render_candles_subplot, + trade_review_fetch_window, + trim_rows_for_trade_review, ) from key_sl_tp_lib import ( breakeven_enabled_from_row, @@ -842,13 +846,32 @@ def generate_multi_timeframe_chart_png( default_marker_tfs = {str(t).strip().lower() for t in timeframes} price_levels = price_levels_from_marker_payload(marker_payload) for tf in timeframes: + rows = [] try: - ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) - if not ohlcv and end_ts_ms: - ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) + if layout == "vertical" and marker_payload: + win = trade_review_fetch_window( + marker_payload.get("entry_ts_ms"), + marker_payload.get("exit_ts_ms"), + tf, + limit, + anchor=marker_payload.get("chart_anchor"), + now_ms=marker_payload.get("now_ts_ms"), + ) + if win: + ohlcv = exchange.fetch_ohlcv( + exchange_symbol, + timeframe=tf, + since=max(0, int(win["since_ms"])), + limit=int(win["fetch_limit"]), + ) + rows = trim_rows_for_trade_review(_ohlcv_to_rows(ohlcv), win) + if not rows: + ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) + if not ohlcv and end_ts_ms: + ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) + rows = _ohlcv_to_rows(ohlcv)[-limit:] except Exception: - ohlcv = [] - rows = _ohlcv_to_rows(ohlcv)[-limit:] + rows = [] title = f"{title_prefix} | {tf} x{len(rows)}" tf_key = str(tf).strip().lower() if marker_payload: @@ -5837,6 +5860,7 @@ def render_main_page(page="trade"): journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1, journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2, journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT, + journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR, exchange_display=EXCHANGE_DISPLAY_NAME, max_active_positions=MAX_ACTIVE_POSITIONS, manual_min_planned_rr=MANUAL_MIN_PLANNED_RR, @@ -7353,12 +7377,15 @@ def add_journal(): ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None, ) journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT) + chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor")) 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")), "entry_price": d.get("entry_price_hint"), "exit_price": d.get("exit_price_hint"), "stop_loss_price": d.get("stop_loss_hint"), + "chart_anchor": chart_anchor, + "now_ts_ms": int(app_now().timestamp() * 1000), } try: chart_fname = f"journal_{entry_id}.png" diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index fdbf19c..fd44892 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -712,8 +712,13 @@ {% endfor %} + + -
双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位
+
双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位
diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index a64d1bb..24761a6 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -53,10 +53,14 @@ from journal_chart_lib import ( JOURNAL_CHART_TF_CHOICES, compose_chart_panels, marker_points_for_timeframe, + parse_journal_chart_anchor, parse_journal_chart_limit, parse_journal_chart_timeframes, + JOURNAL_CHART_DEFAULT_ANCHOR, price_levels_from_marker_payload, render_candles_subplot, + trade_review_fetch_window, + trim_rows_for_trade_review, ) from key_sl_tp_lib import ( breakeven_enabled_from_row, @@ -836,13 +840,32 @@ def generate_multi_timeframe_chart_png( default_marker_tfs = {str(t).strip().lower() for t in timeframes} price_levels = price_levels_from_marker_payload(marker_payload) for tf in timeframes: + rows = [] try: - ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) - if not ohlcv and end_ts_ms: - ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) + if layout == "vertical" and marker_payload: + win = trade_review_fetch_window( + marker_payload.get("entry_ts_ms"), + marker_payload.get("exit_ts_ms"), + tf, + limit, + anchor=marker_payload.get("chart_anchor"), + now_ms=marker_payload.get("now_ts_ms"), + ) + if win: + ohlcv = exchange.fetch_ohlcv( + exchange_symbol, + timeframe=tf, + since=max(0, int(win["since_ms"])), + limit=int(win["fetch_limit"]), + ) + rows = trim_rows_for_trade_review(_ohlcv_to_rows(ohlcv), win) + if not rows: + ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) + if not ohlcv and end_ts_ms: + ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) + rows = _ohlcv_to_rows(ohlcv)[-limit:] except Exception: - ohlcv = [] - rows = _ohlcv_to_rows(ohlcv)[-limit:] + rows = [] title = f"{title_prefix} | {tf} x{len(rows)}" tf_key = str(tf).strip().lower() if marker_payload: @@ -5844,6 +5867,7 @@ def render_main_page(page="trade"): journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1, journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2, journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT, + journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR, exchange_display=EXCHANGE_DISPLAY_NAME, max_active_positions=MAX_ACTIVE_POSITIONS, manual_min_planned_rr=MANUAL_MIN_PLANNED_RR, @@ -7433,12 +7457,15 @@ def add_journal(): ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None, ) journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT) + chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor")) 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")), "entry_price": d.get("entry_price_hint"), "exit_price": d.get("exit_price_hint"), "stop_loss_price": d.get("stop_loss_hint"), + "chart_anchor": chart_anchor, + "now_ts_ms": int(app_now().timestamp() * 1000), } try: chart_fname = f"journal_{entry_id}.png" diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index fdbf19c..fd44892 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -712,8 +712,13 @@ {% endfor %} + + -
双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位
+
双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位
diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 0dca753..83a9942 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -42,10 +42,14 @@ from journal_chart_lib import ( JOURNAL_CHART_TF_CHOICES, compose_chart_panels, marker_points_for_timeframe, + parse_journal_chart_anchor, parse_journal_chart_limit, parse_journal_chart_timeframes, + JOURNAL_CHART_DEFAULT_ANCHOR, price_levels_from_marker_payload, render_candles_subplot, + trade_review_fetch_window, + trim_rows_for_trade_review, ) from hub_auth import request_allowed as hub_request_allowed from history_window_lib import ( @@ -813,13 +817,32 @@ def generate_multi_timeframe_chart_png( default_marker_tfs = {str(t).strip().lower() for t in timeframes} price_levels = price_levels_from_marker_payload(marker_payload) for tf in timeframes: + rows = [] try: - ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) - if not ohlcv and end_ts_ms: - ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) + if layout == "vertical" and marker_payload: + win = trade_review_fetch_window( + marker_payload.get("entry_ts_ms"), + marker_payload.get("exit_ts_ms"), + tf, + limit, + anchor=marker_payload.get("chart_anchor"), + now_ms=marker_payload.get("now_ts_ms"), + ) + if win: + ohlcv = exchange.fetch_ohlcv( + exchange_symbol, + timeframe=tf, + since=max(0, int(win["since_ms"])), + limit=int(win["fetch_limit"]), + ) + rows = trim_rows_for_trade_review(_ohlcv_to_rows(ohlcv), win) + if not rows: + ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) + if not ohlcv and end_ts_ms: + ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) + rows = _ohlcv_to_rows(ohlcv)[-limit:] except Exception: - ohlcv = [] - rows = _ohlcv_to_rows(ohlcv)[-limit:] + rows = [] title = f"{title_prefix} | {tf} x{len(rows)}" tf_key = str(tf).strip().lower() if marker_payload: @@ -5318,6 +5341,7 @@ def render_main_page(page="trade"): journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1, journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2, journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT, + journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR, exchange_display=EXCHANGE_DISPLAY_NAME, **strategy_extra, ) @@ -6840,12 +6864,15 @@ def add_journal(): ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None, ) journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT) + chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor")) 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")), "entry_price": d.get("entry_price_hint"), "exit_price": d.get("exit_price_hint"), "stop_loss_price": d.get("stop_loss_hint"), + "chart_anchor": chart_anchor, + "now_ts_ms": int(app_now().timestamp() * 1000), } try: chart_fname = f"journal_{entry_id}.png" diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index b65859b..f4a407c 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -538,8 +538,13 @@ {% endfor %} + + -
双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位
+
双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位
diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index fce693f..4add1be 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -53,10 +53,14 @@ from journal_chart_lib import ( JOURNAL_CHART_TF_CHOICES, compose_chart_panels, marker_points_for_timeframe, + parse_journal_chart_anchor, parse_journal_chart_limit, parse_journal_chart_timeframes, + JOURNAL_CHART_DEFAULT_ANCHOR, price_levels_from_marker_payload, render_candles_subplot, + trade_review_fetch_window, + trim_rows_for_trade_review, ) from key_sl_tp_lib import ( breakeven_enabled_from_row, @@ -806,13 +810,32 @@ def generate_multi_timeframe_chart_png( default_marker_tfs = {str(t).strip().lower() for t in timeframes} price_levels = price_levels_from_marker_payload(marker_payload) for tf in timeframes: + rows = [] try: - ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) - if not ohlcv and end_ts_ms: - ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) + if layout == "vertical" and marker_payload: + win = trade_review_fetch_window( + marker_payload.get("entry_ts_ms"), + marker_payload.get("exit_ts_ms"), + tf, + limit, + anchor=marker_payload.get("chart_anchor"), + now_ms=marker_payload.get("now_ts_ms"), + ) + if win: + ohlcv = exchange.fetch_ohlcv( + exchange_symbol, + timeframe=tf, + since=max(0, int(win["since_ms"])), + limit=int(win["fetch_limit"]), + ) + rows = trim_rows_for_trade_review(_ohlcv_to_rows(ohlcv), win) + if not rows: + ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) + if not ohlcv and end_ts_ms: + ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) + rows = _ohlcv_to_rows(ohlcv)[-limit:] except Exception: - ohlcv = [] - rows = _ohlcv_to_rows(ohlcv)[-limit:] + rows = [] title = f"{title_prefix} | {tf} x{len(rows)}" tf_key = str(tf).strip().lower() if marker_payload: @@ -5203,6 +5226,7 @@ def render_main_page(page="trade"): journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1, journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2, journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT, + journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR, key_gate_rule_text=key_gate_rule_text, funds_fmt=format_funds_u, exchange_display=EXCHANGE_DISPLAY_NAME, @@ -6748,12 +6772,15 @@ def add_journal(): ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None, ) journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT) + chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor")) 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")), "entry_price": d.get("entry_price_hint"), "exit_price": d.get("exit_price_hint"), "stop_loss_price": d.get("stop_loss_hint"), + "chart_anchor": chart_anchor, + "now_ts_ms": int(app_now().timestamp() * 1000), } try: chart_fname = f"journal_{entry_id}.png" diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index fa7c9f2..6f05fe3 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -721,8 +721,13 @@ {% endfor %} + + -
双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位
+
双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位
diff --git a/journal_chart_lib.py b/journal_chart_lib.py index 43dd059..3068c18 100644 --- a/journal_chart_lib.py +++ b/journal_chart_lib.py @@ -15,6 +15,9 @@ JOURNAL_CHART_DEFAULT_TF2 = "1h" JOURNAL_CHART_DEFAULT_LIMIT = 300 JOURNAL_CHART_LIMIT_MIN = 50 JOURNAL_CHART_LIMIT_MAX = 500 +JOURNAL_CHART_ANCHOR_CLOSE = "close" +JOURNAL_CHART_ANCHOR_NOW = "now" +JOURNAL_CHART_DEFAULT_ANCHOR = JOURNAL_CHART_ANCHOR_CLOSE def _load_font(size): @@ -90,6 +93,13 @@ def parse_positive_price(raw): return None +def parse_journal_chart_anchor(raw): + s = str(raw or "").strip().lower() + if s in (JOURNAL_CHART_ANCHOR_NOW, "current", "当前", "当前时间"): + return JOURNAL_CHART_ANCHOR_NOW + return JOURNAL_CHART_ANCHOR_CLOSE + + def parse_journal_chart_limit(raw, fallback=None): fb = int(fallback if fallback is not None else JOURNAL_CHART_DEFAULT_LIMIT) try: @@ -106,6 +116,113 @@ def normalize_chart_timeframe(raw): return "" +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 _to_int_ms(value): + if value is None: + return None + try: + v = int(value) + return v if v > 0 else None + except (TypeError, ValueError): + return None + + +def trade_review_fetch_window(entry_ts_ms, exit_ts_ms, timeframe, limit, anchor=None, now_ms=None): + """ + 复盘 K 线窗口(anchor=close): + - 有开/平仓:从开仓前若干根起,到平仓 K 线止(覆盖整笔交易 + 入场前背景) + - 仅开仓:以开仓时间为终点向前 limit 根 + - 仅平仓:以平仓时间为终点向前 limit 根 + anchor=now:以当前时间为终点向前 limit 根(可看平仓后走势) + """ + period = timeframe_period_ms(timeframe) + lim = max(2, int(limit)) + entry_ms = _to_int_ms(entry_ts_ms) + exit_ms = _to_int_ms(exit_ts_ms) + anch = (anchor or JOURNAL_CHART_DEFAULT_ANCHOR).strip().lower() + + if anch == JOURNAL_CHART_ANCHOR_NOW: + end_ms = _to_int_ms(now_ms) + if not end_ms: + return None + since_ms = end_ms - period * (lim + 10) + return { + "since_ms": since_ms, + "end_ms": end_ms, + "window_start_ms": since_ms, + "fetch_limit": lim + 20, + "display_limit": lim, + } + + if entry_ms and exit_ms: + if exit_ms < entry_ms: + entry_ms, exit_ms = exit_ms, entry_ms + span_bars = max(1, (exit_ms - entry_ms) // period + 1) + pre_bars = max(40, min(120, lim // 3)) + need = span_bars + pre_bars + fetch_limit = min(JOURNAL_CHART_LIMIT_MAX, max(lim, need + 15)) + since_ms = entry_ms - period * pre_bars + return { + "since_ms": since_ms, + "end_ms": exit_ms, + "window_start_ms": since_ms, + "fetch_limit": fetch_limit, + "display_limit": lim, + } + if entry_ms: + end_ms = entry_ms + since_ms = end_ms - period * (lim + 10) + return { + "since_ms": since_ms, + "end_ms": end_ms, + "window_start_ms": since_ms, + "fetch_limit": lim + 20, + "display_limit": lim, + } + if exit_ms: + end_ms = exit_ms + since_ms = end_ms - period * (lim + 10) + return { + "since_ms": since_ms, + "end_ms": end_ms, + "window_start_ms": since_ms, + "fetch_limit": lim + 20, + "display_limit": lim, + } + return None + + +def trim_rows_for_trade_review(rows, window): + if not window: + return list(rows or []) + start_ms = int(window["window_start_ms"]) + end_ms = int(window["end_ms"]) + lim = int(window["display_limit"]) + filt = [r for r in (rows or []) if start_ms <= int(r["ts"]) <= end_ms] + if len(filt) > lim: + filt = filt[-lim:] + return filt + + def parse_journal_chart_timeframes(tf1, tf2, fallback_tfs=None): """复盘表单:最多两个周期,去重保序。""" out = []