修改
This commit is contained in:
+52
-53
@@ -46,6 +46,18 @@ from fib_key_monitor_lib import (
|
||||
key_signal_type_for_trade_record,
|
||||
stored_key_signal_type,
|
||||
)
|
||||
from journal_chart_lib import (
|
||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||
JOURNAL_CHART_DEFAULT_TF1,
|
||||
JOURNAL_CHART_DEFAULT_TF2,
|
||||
JOURNAL_CHART_TF_CHOICES,
|
||||
compose_chart_panels,
|
||||
marker_points_for_timeframe,
|
||||
parse_journal_chart_limit,
|
||||
parse_journal_chart_timeframes,
|
||||
price_levels_from_marker_payload,
|
||||
render_candles_subplot,
|
||||
)
|
||||
from key_sl_tp_lib import (
|
||||
breakeven_enabled_from_row,
|
||||
normalize_sl_tp_mode,
|
||||
@@ -793,20 +805,24 @@ def generate_multi_timeframe_chart_png(
|
||||
filename_prefix="chart",
|
||||
marker_payload=None,
|
||||
marker_timeframes=None,
|
||||
layout="grid",
|
||||
):
|
||||
if not ORDER_CHART_ENABLED:
|
||||
return None
|
||||
if not Image:
|
||||
return None
|
||||
requested = timeframes or ORDER_CHART_TFS
|
||||
requested = list(timeframes or ORDER_CHART_TFS)
|
||||
limit = limit or ORDER_CHART_LIMIT
|
||||
preferred_layout = ["5m", "15m", "1h", "4h"]
|
||||
requested_set = set(requested or [])
|
||||
ordered = [tf for tf in preferred_layout if tf in requested_set]
|
||||
for tf in requested:
|
||||
if tf not in ordered:
|
||||
ordered.append(tf)
|
||||
timeframes = ordered[:4] if ordered else preferred_layout
|
||||
if layout == "vertical":
|
||||
timeframes = requested[:2] if requested else [JOURNAL_CHART_DEFAULT_TF1, JOURNAL_CHART_DEFAULT_TF2]
|
||||
else:
|
||||
preferred_layout = ["5m", "15m", "1h", "4h"]
|
||||
requested_set = set(requested or [])
|
||||
ordered = [tf for tf in preferred_layout if tf in requested_set]
|
||||
for tf in requested:
|
||||
if tf not in ordered:
|
||||
ordered.append(tf)
|
||||
timeframes = ordered[:4] if ordered else preferred_layout
|
||||
|
||||
ensure_markets_loaded()
|
||||
panels = []
|
||||
@@ -818,6 +834,7 @@ def generate_multi_timeframe_chart_png(
|
||||
except (TypeError, ValueError):
|
||||
end_ts_ms = None
|
||||
default_marker_tfs = {str(t).strip().lower() for t in timeframes}
|
||||
price_levels = price_levels_from_marker_payload(marker_payload)
|
||||
for tf in timeframes:
|
||||
try:
|
||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
||||
@@ -827,7 +844,6 @@ def generate_multi_timeframe_chart_png(
|
||||
ohlcv = []
|
||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
||||
title = f"{title_prefix} | {tf} x{len(rows)}"
|
||||
points = []
|
||||
tf_key = str(tf).strip().lower()
|
||||
if marker_payload:
|
||||
if marker_timeframes:
|
||||
@@ -836,54 +852,29 @@ def generate_multi_timeframe_chart_png(
|
||||
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:
|
||||
points.append({"idx": entry_idx, "price": entry_price, "tag": "ENTRY"})
|
||||
if exit_idx is not None and exit_price is not None:
|
||||
points.append({"idx": exit_idx, "price": exit_price, "tag": "EXIT"})
|
||||
points = (
|
||||
marker_points_for_timeframe(rows, marker_payload)
|
||||
if marker_payload and tf_key in marker_tfs
|
||||
else []
|
||||
)
|
||||
panels.append(
|
||||
_render_candles_subplot(
|
||||
render_candles_subplot(
|
||||
rows,
|
||||
title,
|
||||
width=cell_w,
|
||||
height=cell_h,
|
||||
bg_rgb=(255, 255, 255),
|
||||
marker_points=points,
|
||||
price_levels=price_levels,
|
||||
)
|
||||
)
|
||||
|
||||
if not panels:
|
||||
return None
|
||||
|
||||
gap = 10
|
||||
cols = 2
|
||||
rows_n = int(math.ceil(len(panels) / cols))
|
||||
w = cols * cell_w + (cols - 1) * gap
|
||||
h = rows_n * cell_h + (rows_n - 1) * gap
|
||||
out = Image.new("RGB", (w, h), (255, 255, 255))
|
||||
idx = 0
|
||||
for r in range(rows_n):
|
||||
for c in range(cols):
|
||||
if idx >= len(panels):
|
||||
break
|
||||
x = c * (cell_w + gap)
|
||||
y = r * (cell_h + gap)
|
||||
out.paste(panels[idx], (x, y))
|
||||
idx += 1
|
||||
|
||||
# 四宫格间隔线(仅在拼图间隙处画线,不进入单张子图)
|
||||
if ImageDraw and rows_n >= 1:
|
||||
draw_out = ImageDraw.Draw(out)
|
||||
line_col = (220, 225, 232)
|
||||
x_mid = cell_w + gap // 2
|
||||
if w > x_mid >= 0:
|
||||
draw_out.line((x_mid, 0, x_mid, h), fill=line_col, width=2)
|
||||
for rr in range(1, rows_n):
|
||||
y_mid = rr * cell_h + (rr - 1) * gap + gap // 2
|
||||
if 0 <= y_mid <= h:
|
||||
draw_out.line((0, y_mid, w, y_mid), fill=line_col, width=2)
|
||||
out = compose_chart_panels(panels, layout=layout, cell_w=cell_w, cell_h=cell_h, gap=10)
|
||||
if out is None:
|
||||
return None
|
||||
|
||||
target_dir = out_dir or ORDER_CHART_DIR
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
@@ -5849,6 +5840,10 @@ def render_main_page(page="trade"):
|
||||
signed_usdt_fmt=format_signed_usdt,
|
||||
entry_reason_options=list(ENTRY_REASON_OPTIONS),
|
||||
entry_reason_other_value=ENTRY_REASON_OTHER,
|
||||
journal_chart_tf_choices=JOURNAL_CHART_TF_CHOICES,
|
||||
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
||||
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
||||
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
|
||||
@@ -7432,32 +7427,36 @@ 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]}"
|
||||
journal_tfs = parse_journal_chart_timeframes(
|
||||
d.get("journal_chart_tf1"),
|
||||
d.get("journal_chart_tf2"),
|
||||
ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None,
|
||||
)
|
||||
journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT)
|
||||
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": None,
|
||||
"exit_price": d.get("exit_price_hint"),
|
||||
"stop_loss_price": d.get("stop_loss_hint"),
|
||||
}
|
||||
try:
|
||||
chart_fname = f"journal_{entry_id}.png"
|
||||
saved = generate_multi_timeframe_chart_png(
|
||||
exchange_symbol,
|
||||
title_prefix,
|
||||
timeframes=ORDER_CHART_TFS,
|
||||
limit=ORDER_CHART_LIMIT,
|
||||
timeframes=journal_tfs,
|
||||
limit=journal_limit,
|
||||
out_dir=app.config["UPLOAD_FOLDER"],
|
||||
filename=chart_fname,
|
||||
filename_prefix="journal",
|
||||
marker_payload=marker_payload,
|
||||
marker_timeframes=(
|
||||
{x.strip().lower() for x in ORDER_CHART_TFS if x and str(x).strip()}
|
||||
if ORDER_CHART_TFS
|
||||
else {"5m", "15m", "1h", "4h"}
|
||||
),
|
||||
marker_timeframes={x.strip().lower() for x in journal_tfs},
|
||||
layout="vertical",
|
||||
)
|
||||
if saved:
|
||||
image_filename = saved
|
||||
chart_msg = f"已生成多周期K线图:/static/images/{saved}"
|
||||
chart_msg = f"已生成复盘K线图({'/'.join(journal_tfs)} 各{journal_limit}根):/static/images/{saved}"
|
||||
if uploaded_tmp:
|
||||
try:
|
||||
old_path = os.path.join(app.config["UPLOAD_FOLDER"], uploaded_tmp)
|
||||
|
||||
Reference in New Issue
Block a user