This commit is contained in:
dekun
2026-05-27 15:22:40 +08:00
parent ffb83c0526
commit 7bb3f942ed
9 changed files with 631 additions and 220 deletions
+52 -53
View File
@@ -45,6 +45,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,
@@ -799,20 +811,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 = []
@@ -824,6 +840,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)
@@ -833,7 +850,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:
@@ -842,54 +858,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)
@@ -5842,6 +5833,10 @@ def render_main_page(page="trade"):
funds_fmt=format_funds_u,
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,
@@ -7352,32 +7347,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)
+22 -2
View File
@@ -657,6 +657,7 @@
<input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
<input type="hidden" name="entry_price_hint" id="entry-price-hint">
<input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
<input type="hidden" name="exit_price_hint" id="exit-price-hint">
<input type="hidden" name="direction_hint" id="direction-hint">
<div class="form-grid">
<input type="datetime-local" name="open_datetime" required>
@@ -688,12 +689,31 @@
<select name="new_trade_while_occupied"><option value="否">占用时新开仓:否</option><option value="是">占用时新开仓:是</option></select>
<input id="journal-screenshot" type="file" name="screenshot" accept="image/*">
</div>
<div class="form-row" style="margin-top:8px">
<div class="form-row" style="margin-top:8px;flex-wrap:wrap;gap:10px;align-items:center">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="journal_exchange_chart" value="true" checked>
保存时自动生成多周期K线图(4h/1h/15m/5m 各100)并作为截图
保存时自动生成 K 线图并作为截图
</label>
<label style="font-size:.82rem;color:#9aa">周期1</label>
<select name="journal_chart_tf1" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">周期2</label>
<select name="journal_chart_tf2" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线数</label>
<select name="journal_chart_limit" style="min-width:72px">
{% for n in [100, 150, 200, 250, 300, 400, 500] %}
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
</div>
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位</div>
<div class="form-row" style="margin-top:8px">
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
</div>