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 = []