diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index dba582a..5919b12 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -642,6 +642,15 @@ def _local_input_datetime_to_ms(dt_text): return None +def _marker_tag_label(tag): + t = str(tag or "").strip().upper() + if t == "ENTRY": + return "开仓" + if t == "EXIT": + return "平仓" + return str(tag or "") + + def _pick_marker_point(rows, target_ts_ms, target_price=None): if not rows or target_ts_ms is None: return None, None @@ -696,7 +705,7 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255), continue if idx < 0 or idx >= n: continue - marker_by_idx[idx] = mp + marker_by_idx.setdefault(idx, []).append(mp) x0 = pad_l for i, r in enumerate(rows): @@ -723,27 +732,29 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255), draw.rectangle((left, top, left + body_w, bot), fill=(255, 255, 255), outline=edge_color, width=1) else: draw.rectangle((left, top, left + body_w, bot), fill=edge_color, outline=edge_color, width=1) - if i in marker_by_idx: - mp = marker_by_idx[i] + for j, mp in enumerate(marker_by_idx.get(i, [])): tag = str(mp.get("tag") or "") + label = _marker_tag_label(tag) m_price = float(mp.get("price") or r["c"]) y_m = pad_t + int((hi - m_price) / (hi - lo) * plot_h) y_m = max(pad_t + 4, min(pad_t + plot_h - 4, y_m)) + x_off = (j - (len(marker_by_idx[i]) - 1) / 2.0) * 14 + x_draw = int(x_mid + x_off) if tag == "ENTRY": m_color = (0, 195, 95) - tri = [(x_mid, y_m - 20), (x_mid - 9, y_m - 4), (x_mid + 9, y_m - 4)] + tri = [(x_draw, y_m - 20), (x_draw - 9, y_m - 4), (x_draw + 9, y_m - 4)] text_y = y_m - 36 else: m_color = (235, 65, 65) - tri = [(x_mid, y_m + 20), (x_mid - 9, y_m + 4), (x_mid + 9, y_m + 4)] + tri = [(x_draw, y_m + 20), (x_draw - 9, y_m + 4), (x_draw + 9, y_m + 4)] text_y = y_m + 12 - draw.ellipse((x_mid - 5, y_m - 5, x_mid + 5, y_m + 5), fill=m_color, outline=(255, 255, 255), width=1) + draw.ellipse((x_draw - 5, y_m - 5, x_draw + 5, y_m + 5), fill=m_color, outline=(255, 255, 255), width=1) draw.polygon(tri, fill=m_color) - draw.line((x_mid, y_m, x_mid, y_m - 16 if tag == "ENTRY" else y_m + 16), fill=m_color, width=3) + draw.line((x_draw, y_m, x_draw, y_m - 16 if tag == "ENTRY" else y_m + 16), fill=m_color, width=3) if font: - draw.text((x_mid + 8, text_y), tag, fill=m_color, font=font) + draw.text((x_draw + 8, text_y), label, fill=m_color, font=font) else: - draw.text((x_mid + 8, text_y), tag, fill=m_color) + draw.text((x_draw + 8, text_y), label, fill=m_color) x0 = x1 if len(marker_points or []) >= 2: @@ -853,6 +864,7 @@ def generate_multi_timeframe_chart_png( 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 + default_marker_tfs = {str(t).strip().lower() for t in timeframes} for tf in timeframes: try: ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) @@ -864,7 +876,13 @@ def generate_multi_timeframe_chart_png( title = f"{title_prefix} | {tf} x{len(rows)}" points = [] tf_key = str(tf).strip().lower() - marker_tfs = {str(x).strip().lower() for x in (marker_timeframes or []) if str(x).strip()} + if marker_payload: + if marker_timeframes: + marker_tfs = {str(x).strip().lower() for x in marker_timeframes if str(x).strip()} + else: + marker_tfs = default_marker_tfs + else: + marker_tfs = set() if marker_payload and tf_key in marker_tfs: entry_idx, entry_price = _pick_marker_point(rows, marker_payload.get("entry_ts_ms"), marker_payload.get("entry_price")) exit_idx, exit_price = _pick_marker_point(rows, marker_payload.get("exit_ts_ms"), marker_payload.get("exit_price")) @@ -922,7 +940,26 @@ def generate_multi_timeframe_chart_png( return fname -def generate_order_open_chart(exchange_symbol, title_prefix, timeframes=None, limit=None): +def generate_order_open_chart( + exchange_symbol, + title_prefix, + timeframes=None, + limit=None, + opened_at_ms=None, + entry_price=None, +): + marker_payload = None + if opened_at_ms: + marker_payload = { + "entry_ts_ms": opened_at_ms, + "exit_ts_ms": None, + "entry_price": entry_price, + "exit_price": None, + } + marker_tfs = ( + {x.strip().lower() for x in (timeframes or ORDER_CHART_TFS) if x and str(x).strip()} + or {"5m", "15m", "1h", "4h"} + ) return generate_multi_timeframe_chart_png( exchange_symbol, title_prefix, @@ -931,6 +968,8 @@ def generate_order_open_chart(exchange_symbol, title_prefix, timeframes=None, li out_dir=ORDER_CHART_DIR, filename=None, filename_prefix="order", + marker_payload=marker_payload, + marker_timeframes=marker_tfs, ) @@ -6572,7 +6611,12 @@ def add_order(): if make_order_chart and ORDER_CHART_ENABLED: try: title_prefix = f"{symbol} {direction} #{new_order_id}" - chart_name = generate_order_open_chart(exchange_symbol, title_prefix) + chart_name = generate_order_open_chart( + exchange_symbol, + title_prefix, + opened_at_ms=opened_at_ms, + entry_price=trigger_price, + ) if chart_name: chart_url = f"/static/images/order_charts/{chart_name}" except Exception: @@ -7147,10 +7191,9 @@ 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 = { - "exit_ts_ms": close_ms, - "entry_ts_ms": close_ms, + "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": None, } diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index d62c484..5a4f1ef 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -636,6 +636,15 @@ def _local_input_datetime_to_ms(dt_text): return None +def _marker_tag_label(tag): + t = str(tag or "").strip().upper() + if t == "ENTRY": + return "开仓" + if t == "EXIT": + return "平仓" + return str(tag or "") + + def _pick_marker_point(rows, target_ts_ms, target_price=None): if not rows or target_ts_ms is None: return None, None @@ -690,7 +699,7 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255), continue if idx < 0 or idx >= n: continue - marker_by_idx[idx] = mp + marker_by_idx.setdefault(idx, []).append(mp) x0 = pad_l for i, r in enumerate(rows): @@ -717,27 +726,29 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255), draw.rectangle((left, top, left + body_w, bot), fill=(255, 255, 255), outline=edge_color, width=1) else: draw.rectangle((left, top, left + body_w, bot), fill=edge_color, outline=edge_color, width=1) - if i in marker_by_idx: - mp = marker_by_idx[i] + for j, mp in enumerate(marker_by_idx.get(i, [])): tag = str(mp.get("tag") or "") + label = _marker_tag_label(tag) m_price = float(mp.get("price") or r["c"]) y_m = pad_t + int((hi - m_price) / (hi - lo) * plot_h) y_m = max(pad_t + 4, min(pad_t + plot_h - 4, y_m)) + x_off = (j - (len(marker_by_idx[i]) - 1) / 2.0) * 14 + x_draw = int(x_mid + x_off) if tag == "ENTRY": m_color = (0, 195, 95) - tri = [(x_mid, y_m - 20), (x_mid - 9, y_m - 4), (x_mid + 9, y_m - 4)] + tri = [(x_draw, y_m - 20), (x_draw - 9, y_m - 4), (x_draw + 9, y_m - 4)] text_y = y_m - 36 else: m_color = (235, 65, 65) - tri = [(x_mid, y_m + 20), (x_mid - 9, y_m + 4), (x_mid + 9, y_m + 4)] + tri = [(x_draw, y_m + 20), (x_draw - 9, y_m + 4), (x_draw + 9, y_m + 4)] text_y = y_m + 12 - draw.ellipse((x_mid - 5, y_m - 5, x_mid + 5, y_m + 5), fill=m_color, outline=(255, 255, 255), width=1) + draw.ellipse((x_draw - 5, y_m - 5, x_draw + 5, y_m + 5), fill=m_color, outline=(255, 255, 255), width=1) draw.polygon(tri, fill=m_color) - draw.line((x_mid, y_m, x_mid, y_m - 16 if tag == "ENTRY" else y_m + 16), fill=m_color, width=3) + draw.line((x_draw, y_m, x_draw, y_m - 16 if tag == "ENTRY" else y_m + 16), fill=m_color, width=3) if font: - draw.text((x_mid + 8, text_y), tag, fill=m_color, font=font) + draw.text((x_draw + 8, text_y), label, fill=m_color, font=font) else: - draw.text((x_mid + 8, text_y), tag, fill=m_color) + draw.text((x_draw + 8, text_y), label, fill=m_color) x0 = x1 if len(marker_points or []) >= 2: @@ -847,6 +858,7 @@ def generate_multi_timeframe_chart_png( 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 + default_marker_tfs = {str(t).strip().lower() for t in timeframes} for tf in timeframes: try: ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) @@ -858,7 +870,13 @@ def generate_multi_timeframe_chart_png( title = f"{title_prefix} | {tf} x{len(rows)}" points = [] tf_key = str(tf).strip().lower() - marker_tfs = {str(x).strip().lower() for x in (marker_timeframes or []) if str(x).strip()} + if marker_payload: + if marker_timeframes: + marker_tfs = {str(x).strip().lower() for x in marker_timeframes if str(x).strip()} + else: + marker_tfs = default_marker_tfs + else: + marker_tfs = set() if marker_payload and tf_key in marker_tfs: entry_idx, entry_price = _pick_marker_point(rows, marker_payload.get("entry_ts_ms"), marker_payload.get("entry_price")) exit_idx, exit_price = _pick_marker_point(rows, marker_payload.get("exit_ts_ms"), marker_payload.get("exit_price")) @@ -916,7 +934,26 @@ def generate_multi_timeframe_chart_png( return fname -def generate_order_open_chart(exchange_symbol, title_prefix, timeframes=None, limit=None): +def generate_order_open_chart( + exchange_symbol, + title_prefix, + timeframes=None, + limit=None, + opened_at_ms=None, + entry_price=None, +): + marker_payload = None + if opened_at_ms: + marker_payload = { + "entry_ts_ms": opened_at_ms, + "exit_ts_ms": None, + "entry_price": entry_price, + "exit_price": None, + } + marker_tfs = ( + {x.strip().lower() for x in (timeframes or ORDER_CHART_TFS) if x and str(x).strip()} + or {"5m", "15m", "1h", "4h"} + ) return generate_multi_timeframe_chart_png( exchange_symbol, title_prefix, @@ -925,6 +962,8 @@ def generate_order_open_chart(exchange_symbol, title_prefix, timeframes=None, li out_dir=ORDER_CHART_DIR, filename=None, filename_prefix="order", + marker_payload=marker_payload, + marker_timeframes=marker_tfs, ) @@ -6617,7 +6656,12 @@ def add_order(): if make_order_chart and ORDER_CHART_ENABLED: try: title_prefix = f"{symbol} {direction} #{new_order_id}" - chart_name = generate_order_open_chart(exchange_symbol, title_prefix) + chart_name = generate_order_open_chart( + exchange_symbol, + title_prefix, + opened_at_ms=opened_at_ms, + entry_price=trigger_price, + ) if chart_name: chart_url = f"/static/images/order_charts/{chart_name}" except Exception: @@ -7173,10 +7217,9 @@ 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 = { - "exit_ts_ms": close_ms, - "entry_ts_ms": close_ms, + "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": None, } diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 29f2dc1..6841e35 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -625,6 +625,67 @@ def _local_input_datetime_to_ms(dt_text): return None +def _marker_tag_label(tag): + t = str(tag or "").strip().upper() + if t == "ENTRY": + return "开仓" + if t == "EXIT": + return "平仓" + return str(tag or "") + + +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 _ohlcv_dict_rows_to_lists(rows, lim): + if not rows: + return [] + pick = rows[-lim:] if len(rows) >= lim else rows + return [[r["ts"], r["o"], r["h"], r["l"], r["c"], r.get("v", 0)] for r in pick] + + +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)) + try: + if not end_ts_ms: + ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim) + else: + period = _timeframe_period_ms(timeframe) + since = int(end_ts_ms) - period * (lim + 10) + ohlcv = exchange.fetch_ohlcv( + exchange_symbol, timeframe=timeframe, since=max(0, since), limit=lim + 20 + ) + except Exception: + return [] + rows = _ohlcv_to_rows(ohlcv) + if not rows: + return [] + if not end_ts_ms: + return _ohlcv_dict_rows_to_lists(rows, lim) + filtered = [r for r in rows if int(r["ts"]) <= int(end_ts_ms)] + if len(filtered) >= 2: + return _ohlcv_dict_rows_to_lists(filtered, lim) + return _ohlcv_dict_rows_to_lists(rows, lim) + + def _pick_marker_point(rows, target_ts_ms, target_price=None): if not rows or target_ts_ms is None: return None, None @@ -679,7 +740,7 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255), continue if idx < 0 or idx >= n: continue - marker_by_idx[idx] = mp + marker_by_idx.setdefault(idx, []).append(mp) x0 = pad_l for i, r in enumerate(rows): @@ -706,27 +767,29 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255), draw.rectangle((left, top, left + body_w, bot), fill=(255, 255, 255), outline=edge_color, width=1) else: draw.rectangle((left, top, left + body_w, bot), fill=edge_color, outline=edge_color, width=1) - if i in marker_by_idx: - mp = marker_by_idx[i] + for j, mp in enumerate(marker_by_idx.get(i, [])): tag = str(mp.get("tag") or "") + label = _marker_tag_label(tag) m_price = float(mp.get("price") or r["c"]) y_m = pad_t + int((hi - m_price) / (hi - lo) * plot_h) y_m = max(pad_t + 4, min(pad_t + plot_h - 4, y_m)) + x_off = (j - (len(marker_by_idx[i]) - 1) / 2.0) * 14 + x_draw = int(x_mid + x_off) if tag == "ENTRY": m_color = (0, 195, 95) - tri = [(x_mid, y_m - 20), (x_mid - 9, y_m - 4), (x_mid + 9, y_m - 4)] + tri = [(x_draw, y_m - 20), (x_draw - 9, y_m - 4), (x_draw + 9, y_m - 4)] text_y = y_m - 36 else: m_color = (235, 65, 65) - tri = [(x_mid, y_m + 20), (x_mid - 9, y_m + 4), (x_mid + 9, y_m + 4)] + tri = [(x_draw, y_m + 20), (x_draw - 9, y_m + 4), (x_draw + 9, y_m + 4)] text_y = y_m + 12 - draw.ellipse((x_mid - 5, y_m - 5, x_mid + 5, y_m + 5), fill=m_color, outline=(255, 255, 255), width=1) + draw.ellipse((x_draw - 5, y_m - 5, x_draw + 5, y_m + 5), fill=m_color, outline=(255, 255, 255), width=1) draw.polygon(tri, fill=m_color) - draw.line((x_mid, y_m, x_mid, y_m - 16 if tag == "ENTRY" else y_m + 16), fill=m_color, width=3) + draw.line((x_draw, y_m, x_draw, y_m - 16 if tag == "ENTRY" else y_m + 16), fill=m_color, width=3) if font: - draw.text((x_mid + 8, text_y), tag, fill=m_color, font=font) + draw.text((x_draw + 8, text_y), label, fill=m_color, font=font) else: - draw.text((x_mid + 8, text_y), tag, fill=m_color) + draw.text((x_draw + 8, text_y), label, fill=m_color) x0 = x1 if len(marker_points or []) >= 2: @@ -778,16 +841,31 @@ 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 + default_marker_tfs = {str(t).strip().lower() for t in timeframes} 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) + if not ohlcv and end_ts_ms: + ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit) except Exception: ohlcv = [] rows = _ohlcv_to_rows(ohlcv)[-limit:] title = f"{title_prefix} | {tf} x{len(rows)}" points = [] tf_key = str(tf).strip().lower() - marker_tfs = {str(x).strip().lower() for x in (marker_timeframes or []) if str(x).strip()} + if marker_payload: + if marker_timeframes: + marker_tfs = {str(x).strip().lower() for x in marker_timeframes if str(x).strip()} + else: + marker_tfs = default_marker_tfs + else: + marker_tfs = set() if marker_payload and tf_key in marker_tfs: entry_idx, entry_price = _pick_marker_point(rows, marker_payload.get("entry_ts_ms"), marker_payload.get("entry_price")) exit_idx, exit_price = _pick_marker_point(rows, marker_payload.get("exit_ts_ms"), marker_payload.get("exit_price")) @@ -845,7 +923,26 @@ def generate_multi_timeframe_chart_png( return fname -def generate_order_open_chart(exchange_symbol, title_prefix, timeframes=None, limit=None): +def generate_order_open_chart( + exchange_symbol, + title_prefix, + timeframes=None, + limit=None, + opened_at_ms=None, + entry_price=None, +): + marker_payload = None + if opened_at_ms: + marker_payload = { + "entry_ts_ms": opened_at_ms, + "exit_ts_ms": None, + "entry_price": entry_price, + "exit_price": None, + } + marker_tfs = ( + {x.strip().lower() for x in (timeframes or ORDER_CHART_TFS) if x and str(x).strip()} + or {"5m", "15m", "1h", "4h"} + ) return generate_multi_timeframe_chart_png( exchange_symbol, title_prefix, @@ -854,6 +951,8 @@ def generate_order_open_chart(exchange_symbol, title_prefix, timeframes=None, li out_dir=ORDER_CHART_DIR, filename=None, filename_prefix="order", + marker_payload=marker_payload, + marker_timeframes=marker_tfs, ) @@ -5925,7 +6024,12 @@ def add_order(): if make_order_chart and ORDER_CHART_ENABLED: try: title_prefix = f"{symbol} {direction} #{new_order_id}" - chart_name = generate_order_open_chart(exchange_symbol, title_prefix) + chart_name = generate_order_open_chart( + exchange_symbol, + title_prefix, + opened_at_ms=opened_at_ms, + entry_price=trigger_price, + ) if chart_name: chart_url = f"/static/images/order_charts/{chart_name}" except Exception: diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 1d01aa6..e2c3cd0 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -540,6 +540,15 @@ def _local_input_datetime_to_ms(dt_text): return None +def _marker_tag_label(tag): + t = str(tag or "").strip().upper() + if t == "ENTRY": + return "开仓" + if t == "EXIT": + return "平仓" + return str(tag or "") + + def _pick_marker_point(rows, target_ts_ms, target_price=None): if not rows or target_ts_ms is None: return None, None @@ -594,7 +603,7 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255), continue if idx < 0 or idx >= n: continue - marker_by_idx[idx] = mp + marker_by_idx.setdefault(idx, []).append(mp) x0 = pad_l for i, r in enumerate(rows): @@ -621,27 +630,29 @@ def _render_candles_subplot(rows, title, width, height, bg_rgb=(255, 255, 255), draw.rectangle((left, top, left + body_w, bot), fill=(255, 255, 255), outline=edge_color, width=1) else: draw.rectangle((left, top, left + body_w, bot), fill=edge_color, outline=edge_color, width=1) - if i in marker_by_idx: - mp = marker_by_idx[i] + for j, mp in enumerate(marker_by_idx.get(i, [])): tag = str(mp.get("tag") or "") + label = _marker_tag_label(tag) m_price = float(mp.get("price") or r["c"]) y_m = pad_t + int((hi - m_price) / (hi - lo) * plot_h) y_m = max(pad_t + 4, min(pad_t + plot_h - 4, y_m)) + x_off = (j - (len(marker_by_idx[i]) - 1) / 2.0) * 14 + x_draw = int(x_mid + x_off) if tag == "ENTRY": m_color = (0, 195, 95) - tri = [(x_mid, y_m - 20), (x_mid - 9, y_m - 4), (x_mid + 9, y_m - 4)] + tri = [(x_draw, y_m - 20), (x_draw - 9, y_m - 4), (x_draw + 9, y_m - 4)] text_y = y_m - 36 else: m_color = (235, 65, 65) - tri = [(x_mid, y_m + 20), (x_mid - 9, y_m + 4), (x_mid + 9, y_m + 4)] + tri = [(x_draw, y_m + 20), (x_draw - 9, y_m + 4), (x_draw + 9, y_m + 4)] text_y = y_m + 12 - draw.ellipse((x_mid - 5, y_m - 5, x_mid + 5, y_m + 5), fill=m_color, outline=(255, 255, 255), width=1) + draw.ellipse((x_draw - 5, y_m - 5, x_draw + 5, y_m + 5), fill=m_color, outline=(255, 255, 255), width=1) draw.polygon(tri, fill=m_color) - draw.line((x_mid, y_m, x_mid, y_m - 16 if tag == "ENTRY" else y_m + 16), fill=m_color, width=3) + draw.line((x_draw, y_m, x_draw, y_m - 16 if tag == "ENTRY" else y_m + 16), fill=m_color, width=3) if font: - draw.text((x_mid + 8, text_y), tag, fill=m_color, font=font) + draw.text((x_draw + 8, text_y), label, fill=m_color, font=font) else: - draw.text((x_mid + 8, text_y), tag, fill=m_color) + draw.text((x_draw + 8, text_y), label, fill=m_color) x0 = x1 if len(marker_points or []) >= 2: @@ -750,6 +761,7 @@ def generate_multi_timeframe_chart_png( 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 + default_marker_tfs = {str(t).strip().lower() for t in timeframes} for tf in timeframes: try: ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms) @@ -760,8 +772,15 @@ def generate_multi_timeframe_chart_png( rows = _ohlcv_to_rows(ohlcv)[-limit:] title = f"{title_prefix} | {tf} x{len(rows)}" points = [] - marker_tfs = set(marker_timeframes or []) - if marker_payload and tf in marker_tfs: + tf_key = str(tf).strip().lower() + if marker_payload: + if marker_timeframes: + marker_tfs = {str(x).strip().lower() for x in marker_timeframes if str(x).strip()} + else: + marker_tfs = default_marker_tfs + else: + marker_tfs = set() + if marker_payload and tf_key in marker_tfs: entry_idx, entry_price = _pick_marker_point(rows, marker_payload.get("entry_ts_ms"), marker_payload.get("entry_price")) exit_idx, exit_price = _pick_marker_point(rows, marker_payload.get("exit_ts_ms"), marker_payload.get("exit_price")) if entry_idx is not None and entry_price is not None: @@ -818,7 +837,26 @@ def generate_multi_timeframe_chart_png( return fname -def generate_order_open_chart(exchange_symbol, title_prefix, timeframes=None, limit=None): +def generate_order_open_chart( + exchange_symbol, + title_prefix, + timeframes=None, + limit=None, + opened_at_ms=None, + entry_price=None, +): + marker_payload = None + if opened_at_ms: + marker_payload = { + "entry_ts_ms": opened_at_ms, + "exit_ts_ms": None, + "entry_price": entry_price, + "exit_price": None, + } + marker_tfs = ( + {x.strip().lower() for x in (timeframes or ORDER_CHART_TFS) if x and str(x).strip()} + or {"5m", "15m", "1h", "4h"} + ) return generate_multi_timeframe_chart_png( exchange_symbol, title_prefix, @@ -827,6 +865,8 @@ def generate_order_open_chart(exchange_symbol, title_prefix, timeframes=None, li out_dir=ORDER_CHART_DIR, filename=None, filename_prefix="order", + marker_payload=marker_payload, + marker_timeframes=marker_tfs, ) @@ -4890,7 +4930,12 @@ def add_order(): if make_order_chart and ORDER_CHART_ENABLED: try: title_prefix = f"{symbol} {direction} #{new_order_id}" - chart_name = generate_order_open_chart(exchange_symbol, title_prefix) + chart_name = generate_order_open_chart( + exchange_symbol, + title_prefix, + opened_at_ms=opened_at_ms, + entry_price=trigger_price, + ) if chart_name: chart_url = f"/static/images/order_charts/{chart_name}" except Exception: @@ -5409,10 +5454,9 @@ def add_journal(): symbol_guess = normalize_symbol_input(coin) or coin exchange_symbol = normalize_okx_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 = { - "exit_ts_ms": close_ms, - "entry_ts_ms": close_ms, + "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": None, }