修改
This commit is contained in:
@@ -52,10 +52,14 @@ from journal_chart_lib import (
|
|||||||
JOURNAL_CHART_TF_CHOICES,
|
JOURNAL_CHART_TF_CHOICES,
|
||||||
compose_chart_panels,
|
compose_chart_panels,
|
||||||
marker_points_for_timeframe,
|
marker_points_for_timeframe,
|
||||||
|
parse_journal_chart_anchor,
|
||||||
parse_journal_chart_limit,
|
parse_journal_chart_limit,
|
||||||
parse_journal_chart_timeframes,
|
parse_journal_chart_timeframes,
|
||||||
|
JOURNAL_CHART_DEFAULT_ANCHOR,
|
||||||
price_levels_from_marker_payload,
|
price_levels_from_marker_payload,
|
||||||
render_candles_subplot,
|
render_candles_subplot,
|
||||||
|
trade_review_fetch_window,
|
||||||
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from key_sl_tp_lib import (
|
from key_sl_tp_lib import (
|
||||||
breakeven_enabled_from_row,
|
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}
|
default_marker_tfs = {str(t).strip().lower() for t in timeframes}
|
||||||
price_levels = price_levels_from_marker_payload(marker_payload)
|
price_levels = price_levels_from_marker_payload(marker_payload)
|
||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
|
rows = []
|
||||||
try:
|
try:
|
||||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
if layout == "vertical" and marker_payload:
|
||||||
if not ohlcv and end_ts_ms:
|
win = trade_review_fetch_window(
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit)
|
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:
|
except Exception:
|
||||||
ohlcv = []
|
rows = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
|
||||||
title = f"{title_prefix} | {tf} x{len(rows)}"
|
title = f"{title_prefix} | {tf} x{len(rows)}"
|
||||||
tf_key = str(tf).strip().lower()
|
tf_key = str(tf).strip().lower()
|
||||||
if marker_payload:
|
if marker_payload:
|
||||||
@@ -5837,6 +5860,7 @@ def render_main_page(page="trade"):
|
|||||||
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
||||||
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
||||||
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
|
journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR,
|
||||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||||
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
|
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,
|
ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None,
|
||||||
)
|
)
|
||||||
journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT)
|
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 = {
|
marker_payload = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"entry_price": d.get("entry_price_hint"),
|
||||||
"exit_price": d.get("exit_price_hint"),
|
"exit_price": d.get("exit_price_hint"),
|
||||||
"stop_loss_price": d.get("stop_loss_hint"),
|
"stop_loss_price": d.get("stop_loss_hint"),
|
||||||
|
"chart_anchor": chart_anchor,
|
||||||
|
"now_ts_ms": int(app_now().timestamp() * 1000),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
chart_fname = f"journal_{entry_id}.png"
|
chart_fname = f"journal_{entry_id}.png"
|
||||||
|
|||||||
@@ -712,8 +712,13 @@
|
|||||||
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
|
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<label style="font-size:.82rem;color:#9aa">K线截止</label>
|
||||||
|
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
|
||||||
|
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
|
||||||
|
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位</div>
|
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位</div>
|
||||||
<div class="form-row" style="margin-top:8px">
|
<div class="form-row" style="margin-top:8px">
|
||||||
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,10 +53,14 @@ from journal_chart_lib import (
|
|||||||
JOURNAL_CHART_TF_CHOICES,
|
JOURNAL_CHART_TF_CHOICES,
|
||||||
compose_chart_panels,
|
compose_chart_panels,
|
||||||
marker_points_for_timeframe,
|
marker_points_for_timeframe,
|
||||||
|
parse_journal_chart_anchor,
|
||||||
parse_journal_chart_limit,
|
parse_journal_chart_limit,
|
||||||
parse_journal_chart_timeframes,
|
parse_journal_chart_timeframes,
|
||||||
|
JOURNAL_CHART_DEFAULT_ANCHOR,
|
||||||
price_levels_from_marker_payload,
|
price_levels_from_marker_payload,
|
||||||
render_candles_subplot,
|
render_candles_subplot,
|
||||||
|
trade_review_fetch_window,
|
||||||
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from key_sl_tp_lib import (
|
from key_sl_tp_lib import (
|
||||||
breakeven_enabled_from_row,
|
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}
|
default_marker_tfs = {str(t).strip().lower() for t in timeframes}
|
||||||
price_levels = price_levels_from_marker_payload(marker_payload)
|
price_levels = price_levels_from_marker_payload(marker_payload)
|
||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
|
rows = []
|
||||||
try:
|
try:
|
||||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
if layout == "vertical" and marker_payload:
|
||||||
if not ohlcv and end_ts_ms:
|
win = trade_review_fetch_window(
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit)
|
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:
|
except Exception:
|
||||||
ohlcv = []
|
rows = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
|
||||||
title = f"{title_prefix} | {tf} x{len(rows)}"
|
title = f"{title_prefix} | {tf} x{len(rows)}"
|
||||||
tf_key = str(tf).strip().lower()
|
tf_key = str(tf).strip().lower()
|
||||||
if marker_payload:
|
if marker_payload:
|
||||||
@@ -5844,6 +5867,7 @@ def render_main_page(page="trade"):
|
|||||||
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
||||||
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
||||||
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
|
journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR,
|
||||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||||
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
|
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,
|
ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None,
|
||||||
)
|
)
|
||||||
journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT)
|
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 = {
|
marker_payload = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"entry_price": d.get("entry_price_hint"),
|
||||||
"exit_price": d.get("exit_price_hint"),
|
"exit_price": d.get("exit_price_hint"),
|
||||||
"stop_loss_price": d.get("stop_loss_hint"),
|
"stop_loss_price": d.get("stop_loss_hint"),
|
||||||
|
"chart_anchor": chart_anchor,
|
||||||
|
"now_ts_ms": int(app_now().timestamp() * 1000),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
chart_fname = f"journal_{entry_id}.png"
|
chart_fname = f"journal_{entry_id}.png"
|
||||||
|
|||||||
@@ -712,8 +712,13 @@
|
|||||||
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
|
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<label style="font-size:.82rem;color:#9aa">K线截止</label>
|
||||||
|
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
|
||||||
|
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
|
||||||
|
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位</div>
|
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位</div>
|
||||||
<div class="form-row" style="margin-top:8px">
|
<div class="form-row" style="margin-top:8px">
|
||||||
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,10 +42,14 @@ from journal_chart_lib import (
|
|||||||
JOURNAL_CHART_TF_CHOICES,
|
JOURNAL_CHART_TF_CHOICES,
|
||||||
compose_chart_panels,
|
compose_chart_panels,
|
||||||
marker_points_for_timeframe,
|
marker_points_for_timeframe,
|
||||||
|
parse_journal_chart_anchor,
|
||||||
parse_journal_chart_limit,
|
parse_journal_chart_limit,
|
||||||
parse_journal_chart_timeframes,
|
parse_journal_chart_timeframes,
|
||||||
|
JOURNAL_CHART_DEFAULT_ANCHOR,
|
||||||
price_levels_from_marker_payload,
|
price_levels_from_marker_payload,
|
||||||
render_candles_subplot,
|
render_candles_subplot,
|
||||||
|
trade_review_fetch_window,
|
||||||
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from hub_auth import request_allowed as hub_request_allowed
|
||||||
from history_window_lib import (
|
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}
|
default_marker_tfs = {str(t).strip().lower() for t in timeframes}
|
||||||
price_levels = price_levels_from_marker_payload(marker_payload)
|
price_levels = price_levels_from_marker_payload(marker_payload)
|
||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
|
rows = []
|
||||||
try:
|
try:
|
||||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
if layout == "vertical" and marker_payload:
|
||||||
if not ohlcv and end_ts_ms:
|
win = trade_review_fetch_window(
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit)
|
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:
|
except Exception:
|
||||||
ohlcv = []
|
rows = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
|
||||||
title = f"{title_prefix} | {tf} x{len(rows)}"
|
title = f"{title_prefix} | {tf} x{len(rows)}"
|
||||||
tf_key = str(tf).strip().lower()
|
tf_key = str(tf).strip().lower()
|
||||||
if marker_payload:
|
if marker_payload:
|
||||||
@@ -5318,6 +5341,7 @@ def render_main_page(page="trade"):
|
|||||||
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
||||||
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
||||||
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
|
journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR,
|
||||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||||
**strategy_extra,
|
**strategy_extra,
|
||||||
)
|
)
|
||||||
@@ -6840,12 +6864,15 @@ def add_journal():
|
|||||||
ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None,
|
ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None,
|
||||||
)
|
)
|
||||||
journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT)
|
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 = {
|
marker_payload = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"entry_price": d.get("entry_price_hint"),
|
||||||
"exit_price": d.get("exit_price_hint"),
|
"exit_price": d.get("exit_price_hint"),
|
||||||
"stop_loss_price": d.get("stop_loss_hint"),
|
"stop_loss_price": d.get("stop_loss_hint"),
|
||||||
|
"chart_anchor": chart_anchor,
|
||||||
|
"now_ts_ms": int(app_now().timestamp() * 1000),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
chart_fname = f"journal_{entry_id}.png"
|
chart_fname = f"journal_{entry_id}.png"
|
||||||
|
|||||||
@@ -538,8 +538,13 @@
|
|||||||
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
|
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<label style="font-size:.82rem;color:#9aa">K线截止</label>
|
||||||
|
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
|
||||||
|
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
|
||||||
|
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位</div>
|
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位</div>
|
||||||
<div class="form-row" style="margin-top:8px">
|
<div class="form-row" style="margin-top:8px">
|
||||||
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,10 +53,14 @@ from journal_chart_lib import (
|
|||||||
JOURNAL_CHART_TF_CHOICES,
|
JOURNAL_CHART_TF_CHOICES,
|
||||||
compose_chart_panels,
|
compose_chart_panels,
|
||||||
marker_points_for_timeframe,
|
marker_points_for_timeframe,
|
||||||
|
parse_journal_chart_anchor,
|
||||||
parse_journal_chart_limit,
|
parse_journal_chart_limit,
|
||||||
parse_journal_chart_timeframes,
|
parse_journal_chart_timeframes,
|
||||||
|
JOURNAL_CHART_DEFAULT_ANCHOR,
|
||||||
price_levels_from_marker_payload,
|
price_levels_from_marker_payload,
|
||||||
render_candles_subplot,
|
render_candles_subplot,
|
||||||
|
trade_review_fetch_window,
|
||||||
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from key_sl_tp_lib import (
|
from key_sl_tp_lib import (
|
||||||
breakeven_enabled_from_row,
|
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}
|
default_marker_tfs = {str(t).strip().lower() for t in timeframes}
|
||||||
price_levels = price_levels_from_marker_payload(marker_payload)
|
price_levels = price_levels_from_marker_payload(marker_payload)
|
||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
|
rows = []
|
||||||
try:
|
try:
|
||||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
if layout == "vertical" and marker_payload:
|
||||||
if not ohlcv and end_ts_ms:
|
win = trade_review_fetch_window(
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=tf, limit=limit)
|
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:
|
except Exception:
|
||||||
ohlcv = []
|
rows = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
|
||||||
title = f"{title_prefix} | {tf} x{len(rows)}"
|
title = f"{title_prefix} | {tf} x{len(rows)}"
|
||||||
tf_key = str(tf).strip().lower()
|
tf_key = str(tf).strip().lower()
|
||||||
if marker_payload:
|
if marker_payload:
|
||||||
@@ -5203,6 +5226,7 @@ def render_main_page(page="trade"):
|
|||||||
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
|
||||||
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2,
|
||||||
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
journal_chart_default_limit=JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
|
journal_chart_default_anchor=JOURNAL_CHART_DEFAULT_ANCHOR,
|
||||||
key_gate_rule_text=key_gate_rule_text,
|
key_gate_rule_text=key_gate_rule_text,
|
||||||
funds_fmt=format_funds_u,
|
funds_fmt=format_funds_u,
|
||||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||||
@@ -6748,12 +6772,15 @@ def add_journal():
|
|||||||
ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None,
|
ORDER_CHART_TFS[:2] if ORDER_CHART_TFS else None,
|
||||||
)
|
)
|
||||||
journal_limit = parse_journal_chart_limit(d.get("journal_chart_limit"), ORDER_CHART_LIMIT)
|
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 = {
|
marker_payload = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"entry_price": d.get("entry_price_hint"),
|
||||||
"exit_price": d.get("exit_price_hint"),
|
"exit_price": d.get("exit_price_hint"),
|
||||||
"stop_loss_price": d.get("stop_loss_hint"),
|
"stop_loss_price": d.get("stop_loss_hint"),
|
||||||
|
"chart_anchor": chart_anchor,
|
||||||
|
"now_ts_ms": int(app_now().timestamp() * 1000),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
chart_fname = f"journal_{entry_id}.png"
|
chart_fname = f"journal_{entry_id}.png"
|
||||||
|
|||||||
@@ -721,8 +721,13 @@
|
|||||||
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
|
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<label style="font-size:.82rem;color:#9aa">K线截止</label>
|
||||||
|
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
|
||||||
|
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
|
||||||
|
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;以平仓时间为锚点向前取 K 线;标注开仓、平仓与止损位</div>
|
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位</div>
|
||||||
<div class="form-row" style="margin-top:8px">
|
<div class="form-row" style="margin-top:8px">
|
||||||
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ JOURNAL_CHART_DEFAULT_TF2 = "1h"
|
|||||||
JOURNAL_CHART_DEFAULT_LIMIT = 300
|
JOURNAL_CHART_DEFAULT_LIMIT = 300
|
||||||
JOURNAL_CHART_LIMIT_MIN = 50
|
JOURNAL_CHART_LIMIT_MIN = 50
|
||||||
JOURNAL_CHART_LIMIT_MAX = 500
|
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):
|
def _load_font(size):
|
||||||
@@ -90,6 +93,13 @@ def parse_positive_price(raw):
|
|||||||
return None
|
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):
|
def parse_journal_chart_limit(raw, fallback=None):
|
||||||
fb = int(fallback if fallback is not None else JOURNAL_CHART_DEFAULT_LIMIT)
|
fb = int(fallback if fallback is not None else JOURNAL_CHART_DEFAULT_LIMIT)
|
||||||
try:
|
try:
|
||||||
@@ -106,6 +116,113 @@ def normalize_chart_timeframe(raw):
|
|||||||
return ""
|
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):
|
def parse_journal_chart_timeframes(tf1, tf2, fallback_tfs=None):
|
||||||
"""复盘表单:最多两个周期,去重保序。"""
|
"""复盘表单:最多两个周期,去重保序。"""
|
||||||
out = []
|
out = []
|
||||||
|
|||||||
Reference in New Issue
Block a user