修复一些bug
This commit is contained in:
@@ -58,6 +58,8 @@ from history_window_lib import (
|
|||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
PRESET_UTC_LAST7D,
|
PRESET_UTC_LAST7D,
|
||||||
PRESET_UTC_TODAY,
|
PRESET_UTC_TODAY,
|
||||||
|
list_window_redirect_query,
|
||||||
|
resolve_list_window,
|
||||||
resolve_window,
|
resolve_window,
|
||||||
utc_window_to_bj_sql_strings,
|
utc_window_to_bj_sql_strings,
|
||||||
)
|
)
|
||||||
@@ -785,19 +787,36 @@ def _timeframe_period_ms(tf):
|
|||||||
return 300000
|
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):
|
def _fetch_ohlcv_ending_at(exchange_symbol, timeframe, limit, end_ts_ms):
|
||||||
"""以 end_ts_ms 为终点向前取 K 线(无 end 则拉最近 limit 根)。"""
|
"""以 end_ts_ms 为终点向前取 K 线(无 end 则拉最近 limit 根)。"""
|
||||||
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
||||||
if not end_ts_ms:
|
try:
|
||||||
return exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
|
if not end_ts_ms:
|
||||||
period = _timeframe_period_ms(timeframe)
|
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
|
||||||
since = int(end_ts_ms) - period * (lim + 5)
|
else:
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, since=max(0, since), limit=lim + 10)
|
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)
|
rows = _ohlcv_to_rows(ohlcv)
|
||||||
filtered = [r for r in rows if int(r[0]) <= int(end_ts_ms)]
|
if not rows:
|
||||||
if len(filtered) >= lim:
|
return []
|
||||||
return [[r[0], r[1], r[2], r[3], r[4]] for r in filtered[-lim:]]
|
if not end_ts_ms:
|
||||||
return ohlcv[-lim:] if ohlcv else []
|
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 generate_multi_timeframe_chart_png(
|
def generate_multi_timeframe_chart_png(
|
||||||
@@ -837,6 +856,8 @@ def generate_multi_timeframe_chart_png(
|
|||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
try:
|
try:
|
||||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
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:
|
except Exception:
|
||||||
ohlcv = []
|
ohlcv = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
||||||
@@ -1497,7 +1518,12 @@ def _count_opens_between(conn, start_td, end_td):
|
|||||||
|
|
||||||
|
|
||||||
def _list_window_from_request():
|
def _list_window_from_request():
|
||||||
return resolve_window(request.args, default_preset=PRESET_UTC_TODAY)
|
return resolve_list_window(request.args, session, default_preset=PRESET_UTC_TODAY)
|
||||||
|
|
||||||
|
|
||||||
|
def _redirect_records():
|
||||||
|
qs = list_window_redirect_query(session)
|
||||||
|
return redirect(f"/records?{qs}" if qs else "/records")
|
||||||
|
|
||||||
|
|
||||||
def _pnl_row_matches_segment(row, segment_key):
|
def _pnl_row_matches_segment(row, segment_key):
|
||||||
@@ -7066,7 +7092,7 @@ def add_miss():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("已记录错过机会")
|
flash("已记录错过机会")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/add_journal", methods=["POST"])
|
@app.route("/add_journal", methods=["POST"])
|
||||||
@@ -7076,15 +7102,15 @@ def add_journal():
|
|||||||
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
||||||
if not entry_reason_norm:
|
if not entry_reason_norm:
|
||||||
flash("请选择开仓类型;若选「其他」请在下方填写自定义说明")
|
flash("请选择开仓类型;若选「其他」请在下方填写自定义说明")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
|
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
|
||||||
early_exit_note = str(d.get("early_exit_note") or "").strip()
|
early_exit_note = str(d.get("early_exit_note") or "").strip()
|
||||||
if not early_exit_trigger:
|
if not early_exit_trigger:
|
||||||
flash("请选择离场触发")
|
flash("请选择离场触发")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
||||||
flash("手工平仓必须填写补充说明")
|
flash("手工平仓必须填写补充说明")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
if early_exit_trigger != "手动平仓":
|
if early_exit_trigger != "手动平仓":
|
||||||
early_exit_note = ""
|
early_exit_note = ""
|
||||||
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
||||||
@@ -7183,7 +7209,7 @@ def add_journal():
|
|||||||
flash(f"交易复盘记录已保存。{chart_msg}")
|
flash(f"交易复盘记录已保存。{chart_msg}")
|
||||||
else:
|
else:
|
||||||
flash("交易复盘记录已保存")
|
flash("交易复盘记录已保存")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/journals")
|
@app.route("/api/journals")
|
||||||
|
|||||||
+42
-16
@@ -59,6 +59,8 @@ from history_window_lib import (
|
|||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
PRESET_UTC_LAST7D,
|
PRESET_UTC_LAST7D,
|
||||||
PRESET_UTC_TODAY,
|
PRESET_UTC_TODAY,
|
||||||
|
list_window_redirect_query,
|
||||||
|
resolve_list_window,
|
||||||
resolve_window,
|
resolve_window,
|
||||||
utc_window_to_bj_sql_strings,
|
utc_window_to_bj_sql_strings,
|
||||||
)
|
)
|
||||||
@@ -779,19 +781,36 @@ def _timeframe_period_ms(tf):
|
|||||||
return 300000
|
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):
|
def _fetch_ohlcv_ending_at(exchange_symbol, timeframe, limit, end_ts_ms):
|
||||||
"""以 end_ts_ms 为终点向前取 K 线(无 end 则拉最近 limit 根)。"""
|
"""以 end_ts_ms 为终点向前取 K 线(无 end 则拉最近 limit 根)。"""
|
||||||
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
||||||
if not end_ts_ms:
|
try:
|
||||||
return exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
|
if not end_ts_ms:
|
||||||
period = _timeframe_period_ms(timeframe)
|
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
|
||||||
since = int(end_ts_ms) - period * (lim + 5)
|
else:
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, since=max(0, since), limit=lim + 10)
|
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)
|
rows = _ohlcv_to_rows(ohlcv)
|
||||||
filtered = [r for r in rows if int(r[0]) <= int(end_ts_ms)]
|
if not rows:
|
||||||
if len(filtered) >= lim:
|
return []
|
||||||
return [[r[0], r[1], r[2], r[3], r[4]] for r in filtered[-lim:]]
|
if not end_ts_ms:
|
||||||
return ohlcv[-lim:] if ohlcv else []
|
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 generate_multi_timeframe_chart_png(
|
def generate_multi_timeframe_chart_png(
|
||||||
@@ -831,6 +850,8 @@ def generate_multi_timeframe_chart_png(
|
|||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
try:
|
try:
|
||||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
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:
|
except Exception:
|
||||||
ohlcv = []
|
ohlcv = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
||||||
@@ -1498,7 +1519,12 @@ def _count_opens_between(conn, start_td, end_td):
|
|||||||
|
|
||||||
|
|
||||||
def _list_window_from_request():
|
def _list_window_from_request():
|
||||||
return resolve_window(request.args, default_preset=PRESET_UTC_TODAY)
|
return resolve_list_window(request.args, session, default_preset=PRESET_UTC_TODAY)
|
||||||
|
|
||||||
|
|
||||||
|
def _redirect_records():
|
||||||
|
qs = list_window_redirect_query(session)
|
||||||
|
return redirect(f"/records?{qs}" if qs else "/records")
|
||||||
|
|
||||||
|
|
||||||
def _pnl_row_matches_segment(row, segment_key):
|
def _pnl_row_matches_segment(row, segment_key):
|
||||||
@@ -7074,7 +7100,7 @@ def add_miss():
|
|||||||
tgt_px = round_price_to_exchange(ex_sym, float(d["tgt"]))
|
tgt_px = round_price_to_exchange(ex_sym, float(d["tgt"]))
|
||||||
except Exception:
|
except Exception:
|
||||||
flash("价格格式错误")
|
flash("价格格式错误")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
insert_trade_record(
|
insert_trade_record(
|
||||||
conn,
|
conn,
|
||||||
@@ -7092,7 +7118,7 @@ def add_miss():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("已记录错过机会")
|
flash("已记录错过机会")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/add_journal", methods=["POST"])
|
@app.route("/add_journal", methods=["POST"])
|
||||||
@@ -7102,15 +7128,15 @@ def add_journal():
|
|||||||
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
||||||
if not entry_reason_norm:
|
if not entry_reason_norm:
|
||||||
flash("请选择开仓类型;若选「其他」请在下方填写自定义说明")
|
flash("请选择开仓类型;若选「其他」请在下方填写自定义说明")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
|
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
|
||||||
early_exit_note = str(d.get("early_exit_note") or "").strip()
|
early_exit_note = str(d.get("early_exit_note") or "").strip()
|
||||||
if not early_exit_trigger:
|
if not early_exit_trigger:
|
||||||
flash("请选择离场触发")
|
flash("请选择离场触发")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
||||||
flash("手工平仓必须填写补充说明")
|
flash("手工平仓必须填写补充说明")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
if early_exit_trigger != "手动平仓":
|
if early_exit_trigger != "手动平仓":
|
||||||
early_exit_note = ""
|
early_exit_note = ""
|
||||||
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
||||||
@@ -7209,7 +7235,7 @@ def add_journal():
|
|||||||
flash(f"交易复盘记录已保存。{chart_msg}")
|
flash(f"交易复盘记录已保存。{chart_msg}")
|
||||||
else:
|
else:
|
||||||
flash("交易复盘记录已保存")
|
flash("交易复盘记录已保存")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/journals")
|
@app.route("/api/journals")
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ from history_window_lib import (
|
|||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
PRESET_UTC_LAST7D,
|
PRESET_UTC_LAST7D,
|
||||||
PRESET_UTC_TODAY,
|
PRESET_UTC_TODAY,
|
||||||
|
list_window_redirect_query,
|
||||||
|
resolve_list_window,
|
||||||
resolve_window,
|
resolve_window,
|
||||||
utc_window_to_bj_sql_strings,
|
utc_window_to_bj_sql_strings,
|
||||||
)
|
)
|
||||||
@@ -884,6 +886,7 @@ ENTRY_REASON_OPTIONS = (
|
|||||||
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
|
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
|
||||||
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
|
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
|
||||||
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
|
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
|
||||||
|
"趋势回调",
|
||||||
)
|
)
|
||||||
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom)
|
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom)
|
||||||
ENTRY_REASON_OTHER = "__OTHER__"
|
ENTRY_REASON_OTHER = "__OTHER__"
|
||||||
@@ -898,7 +901,7 @@ def normalize_entry_reason(raw, custom_text=None):
|
|||||||
|
|
||||||
|
|
||||||
def entry_reason_valid_for_storage(s):
|
def entry_reason_valid_for_storage(s):
|
||||||
"""允许五种固定整句、或自定义短文本(不含未解析的 __OTHER__ 占位)。"""
|
"""允许固定开仓类型选项、或自定义短文本(不含未解析的 __OTHER__ 占位)。"""
|
||||||
t = str(s or "").strip()
|
t = str(s or "").strip()
|
||||||
if not t:
|
if not t:
|
||||||
return True
|
return True
|
||||||
@@ -946,6 +949,7 @@ def ai_extract_journal_from_image(image_b64):
|
|||||||
- 趋势多头:小分歧低吸入场(左侧),确认条件:二次探底
|
- 趋势多头:小分歧低吸入场(左侧),确认条件:二次探底
|
||||||
- 趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶
|
- 趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶
|
||||||
- 波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20
|
- 波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20
|
||||||
|
- 趋势回调
|
||||||
6) early_exit_trigger 只能从下列取值中选一个(无法识别则填空字符串):保本止盈、移动止盈、手动平仓、止损、其他。
|
6) early_exit_trigger 只能从下列取值中选一个(无法识别则填空字符串):保本止盈、移动止盈、手动平仓、止损、其他。
|
||||||
7) 若触发为「手动平仓」,early_exit_note 必须写出图中可见的补充说明;其他触发类型 early_exit_note 留空。
|
7) 若触发为「手动平仓」,early_exit_note 必须写出图中可见的补充说明;其他触发类型 early_exit_note 留空。
|
||||||
8) 若图中有无法归类的离场说明原文,可放进 early_exit_note,early_exit_trigger 填「其他」或留空。
|
8) 若图中有无法归类的离场说明原文,可放进 early_exit_note,early_exit_trigger 填「其他」或留空。
|
||||||
@@ -3567,7 +3571,12 @@ def trend_plan_history_status_label(status):
|
|||||||
|
|
||||||
|
|
||||||
def _list_window_from_request():
|
def _list_window_from_request():
|
||||||
return resolve_window(request.args, default_preset=PRESET_UTC_TODAY)
|
return resolve_list_window(request.args, session, default_preset=PRESET_UTC_TODAY)
|
||||||
|
|
||||||
|
|
||||||
|
def _redirect_records():
|
||||||
|
qs = list_window_redirect_query(session)
|
||||||
|
return redirect(f"/records?{qs}" if qs else "/records")
|
||||||
|
|
||||||
|
|
||||||
def calc_trend_manual_breakeven_stop(direction, entry_price, offset_pct=None):
|
def calc_trend_manual_breakeven_stop(direction, entry_price, offset_pct=None):
|
||||||
@@ -6700,7 +6709,7 @@ def add_miss():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("已记录错过机会")
|
flash("已记录错过机会")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/add_journal", methods=["POST"])
|
@app.route("/add_journal", methods=["POST"])
|
||||||
@@ -6710,15 +6719,15 @@ def add_journal():
|
|||||||
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
||||||
if not entry_reason_norm:
|
if not entry_reason_norm:
|
||||||
flash("请选择开仓类型;若选「其他」请在下方填写自定义说明")
|
flash("请选择开仓类型;若选「其他」请在下方填写自定义说明")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
|
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
|
||||||
early_exit_note = str(d.get("early_exit_note") or "").strip()
|
early_exit_note = str(d.get("early_exit_note") or "").strip()
|
||||||
if not early_exit_trigger:
|
if not early_exit_trigger:
|
||||||
flash("请选择离场触发")
|
flash("请选择离场触发")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
||||||
flash("手工平仓必须填写补充说明")
|
flash("手工平仓必须填写补充说明")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
if early_exit_trigger != "手动平仓":
|
if early_exit_trigger != "手动平仓":
|
||||||
early_exit_note = ""
|
early_exit_note = ""
|
||||||
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
||||||
@@ -6816,14 +6825,20 @@ def add_journal():
|
|||||||
flash(f"交易复盘记录已保存。{chart_msg}")
|
flash(f"交易复盘记录已保存。{chart_msg}")
|
||||||
else:
|
else:
|
||||||
flash("交易复盘记录已保存")
|
flash("交易复盘记录已保存")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/journals")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
def api_journals():
|
def api_journals():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute("SELECT * FROM journal_entries ORDER BY created_at DESC").fetchall()
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM journal_entries WHERE COALESCE(close_datetime, created_at, open_datetime) >= ? "
|
||||||
|
"AND COALESCE(close_datetime, created_at, open_datetime) <= ? ORDER BY created_at DESC LIMIT 500",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
result = []
|
result = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
@@ -6871,8 +6886,13 @@ def delete_journal(jid):
|
|||||||
@app.route("/api/reviews")
|
@app.route("/api/reviews")
|
||||||
@login_required
|
@login_required
|
||||||
def api_reviews():
|
def api_reviews():
|
||||||
|
win = _list_window_from_request()
|
||||||
|
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute("SELECT * FROM ai_reviews ORDER BY created_at DESC").fetchall()
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM ai_reviews WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT 200",
|
||||||
|
(start_bj, end_bj),
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify([row_to_dict(r) for r in rows])
|
return jsonify([row_to_dict(r) for r in rows])
|
||||||
|
|
||||||
|
|||||||
@@ -380,45 +380,6 @@
|
|||||||
<button type="submit" {% if not can_trade %}disabled style="opacity:.5;cursor:not-allowed"{% endif %}>生成预览</button>
|
<button type="submit" {% if not can_trade %}disabled style="opacity:.5;cursor:not-allowed"{% endif %}>生成预览</button>
|
||||||
</form>
|
</form>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
function listWindowQueryString(){
|
|
||||||
const presetEl = document.getElementById("win-preset-select");
|
|
||||||
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_today";
|
|
||||||
const q = new URLSearchParams(window.location.search);
|
|
||||||
q.set("win_preset", preset);
|
|
||||||
if(preset === "custom"){
|
|
||||||
const fromEl = document.getElementById("win-from-utc");
|
|
||||||
const toEl = document.getElementById("win-to-utc");
|
|
||||||
if(fromEl && fromEl.value) q.set("from_utc", fromEl.value.replace("T", " ") + ":00");
|
|
||||||
else q.delete("from_utc");
|
|
||||||
if(toEl && toEl.value) q.set("to_utc", toEl.value.replace("T", " ") + ":00");
|
|
||||||
else q.delete("to_utc");
|
|
||||||
} else {
|
|
||||||
q.delete("from_utc");
|
|
||||||
q.delete("to_utc");
|
|
||||||
}
|
|
||||||
return q.toString();
|
|
||||||
}
|
|
||||||
function toggleListWindowCustom(){
|
|
||||||
const preset = document.getElementById("win-preset-select");
|
|
||||||
const box = document.getElementById("win-custom-range");
|
|
||||||
if(!preset || !box) return;
|
|
||||||
box.style.display = preset.value === "custom" ? "" : "none";
|
|
||||||
}
|
|
||||||
function applyListWindow(){
|
|
||||||
const qs = listWindowQueryString();
|
|
||||||
const path = window.location.pathname || "/records";
|
|
||||||
window.location.href = qs ? (path + "?" + qs) : path;
|
|
||||||
}
|
|
||||||
function attachListWindowToExports(){
|
|
||||||
const qs = listWindowQueryString();
|
|
||||||
if(!qs) return;
|
|
||||||
document.querySelectorAll('.export-bar a[href^="/export/trade_records"]').forEach(a=>{
|
|
||||||
const base = a.getAttribute("href").split("?")[0];
|
|
||||||
a.setAttribute("href", base + "?" + qs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(function(){
|
(function(){
|
||||||
const dirSel = document.getElementById("trend-direction");
|
const dirSel = document.getElementById("trend-direction");
|
||||||
const addInp = document.getElementById("trend-add-upper");
|
const addInp = document.getElementById("trend-add-upper");
|
||||||
@@ -584,7 +545,13 @@ function attachListWindowToExports(){
|
|||||||
|
|
||||||
{% if page == 'records' %}
|
{% if page == 'records' %}
|
||||||
<div class="card full records-card">
|
<div class="card full records-card">
|
||||||
<h2>交易记录</h2>
|
<h2>交易记录 & 错过机会</h2>
|
||||||
|
<div class="form-row" style="margin-bottom:10px;gap:8px">
|
||||||
|
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
||||||
|
<input id="review-mode-toggle" type="checkbox">
|
||||||
|
修改/核对开关(开启后可编辑关键字段)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓(展示)</th><th>平仓(展示)</th><th>盈亏U(展示)</th><th>结果</th><th>操作</th></tr>
|
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓(展示)</th><th>平仓(展示)</th><th>盈亏U(展示)</th><th>结果</th><th>操作</th></tr>
|
||||||
@@ -612,6 +579,41 @@ function attachListWindowToExports(){
|
|||||||
{% else %}<span class="badge miss">{{ effective_result }}</span>{% endif %}
|
{% else %}<span class="badge miss">{{ effective_result }}</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="table-del"
|
||||||
|
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
|
||||||
|
onclick='fillJournalFromTrade({{ {
|
||||||
|
"symbol": r.symbol,
|
||||||
|
"monitor_type": r.monitor_type,
|
||||||
|
"direction": r.direction,
|
||||||
|
"trigger_price": r.trigger_price,
|
||||||
|
"stop_loss": stop_show,
|
||||||
|
"take_profit": tp_show,
|
||||||
|
"opened_at": r.effective_opened_at,
|
||||||
|
"closed_at": r.effective_closed_at,
|
||||||
|
"pnl_amount": r.effective_pnl_amount,
|
||||||
|
"result": r.effective_result,
|
||||||
|
"risk_amount": r.risk_amount
|
||||||
|
}|tojson|safe }})'
|
||||||
|
>填入复盘</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="table-del review-edit-btn"
|
||||||
|
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
|
||||||
|
onclick='editTradeRecordReview({{ {
|
||||||
|
"id": r.id,
|
||||||
|
"opened_at": r.effective_opened_at,
|
||||||
|
"closed_at": r.effective_closed_at,
|
||||||
|
"stop_loss": stop_show,
|
||||||
|
"take_profit": tp_show,
|
||||||
|
"pnl_amount": r.effective_pnl_amount,
|
||||||
|
"result": r.effective_result,
|
||||||
|
"miss_reason": r.effective_miss_reason,
|
||||||
|
"effective_entry_reason": r.effective_entry_reason or ""
|
||||||
|
}|tojson|safe }})'
|
||||||
|
disabled
|
||||||
|
>核对修改</button>
|
||||||
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
|
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -641,6 +643,90 @@ function attachListWindowToExports(){
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card journal-card">
|
||||||
|
<h2>交易复盘记录上传(含截图)</h2>
|
||||||
|
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
|
||||||
|
<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="direction_hint" id="direction-hint">
|
||||||
|
<div class="form-grid">
|
||||||
|
<input type="datetime-local" name="open_datetime" required>
|
||||||
|
<input type="datetime-local" name="close_datetime" required>
|
||||||
|
<input name="coin" placeholder="BTC" required>
|
||||||
|
<input name="tf" placeholder="5m" required>
|
||||||
|
<input name="pnl" placeholder="盈亏(U)" required>
|
||||||
|
<select name="entry_reason" id="journal-entry-reason" required title="固定选项或选其他手写">
|
||||||
|
<option value="">开仓类型(必选)</option>
|
||||||
|
{% for er in entry_reason_options %}
|
||||||
|
<option value="{{ er }}">{{ er }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
<option value="{{ entry_reason_other_value }}">其他(自定义,见下方说明框)</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="entry_reason_custom" id="journal-entry-reason-custom" maxlength="2000" placeholder="选「其他」时在此填写开仓类型说明" autocomplete="off" style="display:none">
|
||||||
|
<input name="expect_rr" placeholder="预期RR">
|
||||||
|
<input name="real_rr" placeholder="实际RR">
|
||||||
|
<select name="early_exit_trigger" required title="平仓如何触发">
|
||||||
|
<option value="">离场触发(必选)</option>
|
||||||
|
<option value="止盈">止盈</option>
|
||||||
|
<option value="保本止盈">保本止盈</option>
|
||||||
|
<option value="移动止盈">移动止盈</option>
|
||||||
|
<option value="手动平仓">手动平仓</option>
|
||||||
|
<option value="止损">止损</option>
|
||||||
|
<option value="其他">其他</option>
|
||||||
|
</select>
|
||||||
|
<input name="early_exit_note" id="early-exit-note" placeholder="离场补充(仅手工平仓必填)">
|
||||||
|
<select name="post_breakeven_stare"><option value="否">保本后盯盘:否</option><option value="是">保本后盯盘:是</option></select>
|
||||||
|
<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">
|
||||||
|
<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)并作为截图
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin-top:8px">
|
||||||
|
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
||||||
|
</div>
|
||||||
|
<div class="mood-grid" style="margin-top:8px">
|
||||||
|
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
|
||||||
|
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
|
||||||
|
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label>
|
||||||
|
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label>
|
||||||
|
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label>
|
||||||
|
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label>
|
||||||
|
</div>
|
||||||
|
<textarea name="note" rows="2" style="width:100%;margin-top:8px" placeholder="备注"></textarea>
|
||||||
|
<button type="submit" style="margin-top:8px">保存复盘记录</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card full review-card">
|
||||||
|
<h2>AI复盘(按交易记录)</h2>
|
||||||
|
<div class="form-row">
|
||||||
|
<input type="date" id="day_date">
|
||||||
|
<button type="button" onclick="genDaily()">生成日复盘</button>
|
||||||
|
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button>
|
||||||
|
<input type="date" id="week_start">
|
||||||
|
<input type="date" id="week_end">
|
||||||
|
<button type="button" onclick="genWeekly()">生成周复盘</button>
|
||||||
|
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button>
|
||||||
|
</div>
|
||||||
|
<div id="daily_result" class="ai-result" style="display:none"></div>
|
||||||
|
<div id="weekly_result" class="ai-result" style="display:none"></div>
|
||||||
|
<div class="panel-list" style="margin-top:10px">
|
||||||
|
<div class="panel-item">
|
||||||
|
<strong>交易复盘记录</strong>
|
||||||
|
<div id="journal-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-item">
|
||||||
|
<strong>AI历史复盘</strong>
|
||||||
|
<div id="review-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -926,7 +1012,7 @@ function editTradeRecordReview(t){
|
|||||||
const result = prompt("结果(止盈/止损/保本止盈/移动止盈/手动平仓)", String(t.result || ""));
|
const result = prompt("结果(止盈/止损/保本止盈/移动止盈/手动平仓)", String(t.result || ""));
|
||||||
if(result === null) return;
|
if(result === null) return;
|
||||||
const note = prompt("备注(可空)", String(t.miss_reason || "")) ?? "";
|
const note = prompt("备注(可空)", String(t.miss_reason || "")) ?? "";
|
||||||
const entryHint = "开仓类型:五种固定整句、或自定义说明(2000字内;与复盘表单一致;留空=本次不改该项)";
|
const entryHint = "开仓类型:固定选项整句、或自定义说明(2000字内;与复盘表单一致;留空=本次不改该项)";
|
||||||
const entryIn = prompt(entryHint, String(t.effective_entry_reason || ""));
|
const entryIn = prompt(entryHint, String(t.effective_entry_reason || ""));
|
||||||
if(entryIn === null) return;
|
if(entryIn === null) return;
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -980,7 +1066,8 @@ function deleteKeyHistory(id){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadJournals(){
|
function loadJournals(){
|
||||||
fetch("/api/journals").then(r=>r.json()).then(data=>{
|
const qs = listWindowQueryString();
|
||||||
|
fetch("/api/journals" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
|
||||||
Object.keys(journalCache).forEach(k=>delete journalCache[k]);
|
Object.keys(journalCache).forEach(k=>delete journalCache[k]);
|
||||||
let html="";
|
let html="";
|
||||||
data.forEach(o=>{
|
data.forEach(o=>{
|
||||||
@@ -1002,7 +1089,8 @@ function loadJournals(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadReviews(){
|
function loadReviews(){
|
||||||
fetch("/api/reviews").then(r=>r.json()).then(data=>{
|
const qs = listWindowQueryString();
|
||||||
|
fetch("/api/reviews" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
|
||||||
Object.keys(reviewCache).forEach(k=>delete reviewCache[k]);
|
Object.keys(reviewCache).forEach(k=>delete reviewCache[k]);
|
||||||
let html="";
|
let html="";
|
||||||
data.forEach(r=>{
|
data.forEach(r=>{
|
||||||
@@ -1167,6 +1255,10 @@ function fillJournalFromTrade(t){
|
|||||||
setJournalField("entry_reason", "");
|
setJournalField("entry_reason", "");
|
||||||
setJournalField("entry_reason_custom", "");
|
setJournalField("entry_reason_custom", "");
|
||||||
syncJournalEntryReasonOtherUi();
|
syncJournalEntryReasonOtherUi();
|
||||||
|
if(String(t.monitor_type || "").trim() === "趋势回调" && JOURNAL_ENTRY_REASON_OPTIONS.includes("趋势回调")){
|
||||||
|
setJournalField("entry_reason", "趋势回调");
|
||||||
|
syncJournalEntryReasonOtherUi();
|
||||||
|
}
|
||||||
const er = String(t.result || "").trim();
|
const er = String(t.result || "").trim();
|
||||||
const exitTrigMap = { 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
|
const exitTrigMap = { 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
|
||||||
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
|
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
|
||||||
@@ -1259,6 +1351,44 @@ function toggleStatsCard(){
|
|||||||
btn.innerText = collapsed ? "展开" : "折叠";
|
btn.innerText = collapsed ? "展开" : "折叠";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listWindowQueryString(){
|
||||||
|
const presetEl = document.getElementById("win-preset-select");
|
||||||
|
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_today";
|
||||||
|
const q = new URLSearchParams(window.location.search);
|
||||||
|
q.set("win_preset", preset);
|
||||||
|
if(preset === "custom"){
|
||||||
|
const fromEl = document.getElementById("win-from-utc");
|
||||||
|
const toEl = document.getElementById("win-to-utc");
|
||||||
|
if(fromEl && fromEl.value) q.set("from_utc", fromEl.value.replace("T", " ") + ":00");
|
||||||
|
else q.delete("from_utc");
|
||||||
|
if(toEl && toEl.value) q.set("to_utc", toEl.value.replace("T", " ") + ":00");
|
||||||
|
else q.delete("to_utc");
|
||||||
|
} else {
|
||||||
|
q.delete("from_utc");
|
||||||
|
q.delete("to_utc");
|
||||||
|
}
|
||||||
|
return q.toString();
|
||||||
|
}
|
||||||
|
function toggleListWindowCustom(){
|
||||||
|
const preset = document.getElementById("win-preset-select");
|
||||||
|
const box = document.getElementById("win-custom-range");
|
||||||
|
if(!preset || !box) return;
|
||||||
|
box.style.display = preset.value === "custom" ? "" : "none";
|
||||||
|
}
|
||||||
|
function applyListWindow(){
|
||||||
|
const qs = listWindowQueryString();
|
||||||
|
const path = window.location.pathname || "/records";
|
||||||
|
window.location.href = qs ? (path + "?" + qs) : path;
|
||||||
|
}
|
||||||
|
function attachListWindowToExports(){
|
||||||
|
const qs = listWindowQueryString();
|
||||||
|
if(!qs) return;
|
||||||
|
document.querySelectorAll('.export-bar a[href^="/export/trade_records"]').forEach(a=>{
|
||||||
|
const base = a.getAttribute("href").split("?")[0];
|
||||||
|
a.setAttribute("href", base + "?" + qs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
attachListWindowToExports();
|
attachListWindowToExports();
|
||||||
toggleListWindowCustom();
|
toggleListWindowCustom();
|
||||||
if(document.getElementById("journal-list")) loadJournals();
|
if(document.getElementById("journal-list")) loadJournals();
|
||||||
|
|||||||
+41
-15
@@ -58,6 +58,8 @@ from history_window_lib import (
|
|||||||
PRESET_UTC_LAST24H,
|
PRESET_UTC_LAST24H,
|
||||||
PRESET_UTC_LAST7D,
|
PRESET_UTC_LAST7D,
|
||||||
PRESET_UTC_TODAY,
|
PRESET_UTC_TODAY,
|
||||||
|
list_window_redirect_query,
|
||||||
|
resolve_list_window,
|
||||||
resolve_window,
|
resolve_window,
|
||||||
utc_window_to_bj_sql_strings,
|
utc_window_to_bj_sql_strings,
|
||||||
)
|
)
|
||||||
@@ -683,18 +685,35 @@ def _timeframe_period_ms(tf):
|
|||||||
return 300000
|
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):
|
def _fetch_ohlcv_ending_at(exchange_symbol, timeframe, limit, end_ts_ms):
|
||||||
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
lim = max(2, int(limit or ORDER_CHART_LIMIT))
|
||||||
if not end_ts_ms:
|
try:
|
||||||
return exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
|
if not end_ts_ms:
|
||||||
period = _timeframe_period_ms(timeframe)
|
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, limit=lim)
|
||||||
since = int(end_ts_ms) - period * (lim + 5)
|
else:
|
||||||
ohlcv = exchange.fetch_ohlcv(exchange_symbol, timeframe=timeframe, since=max(0, since), limit=lim + 10)
|
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)
|
rows = _ohlcv_to_rows(ohlcv)
|
||||||
filtered = [r for r in rows if int(r[0]) <= int(end_ts_ms)]
|
if not rows:
|
||||||
if len(filtered) >= lim:
|
return []
|
||||||
return [[r[0], r[1], r[2], r[3], r[4]] for r in filtered[-lim:]]
|
if not end_ts_ms:
|
||||||
return ohlcv[-lim:] if ohlcv else []
|
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 generate_multi_timeframe_chart_png(
|
def generate_multi_timeframe_chart_png(
|
||||||
@@ -734,6 +753,8 @@ def generate_multi_timeframe_chart_png(
|
|||||||
for tf in timeframes:
|
for tf in timeframes:
|
||||||
try:
|
try:
|
||||||
ohlcv = _fetch_ohlcv_ending_at(exchange_symbol, tf, limit, end_ts_ms)
|
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:
|
except Exception:
|
||||||
ohlcv = []
|
ohlcv = []
|
||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
||||||
@@ -1361,7 +1382,12 @@ def _count_opens_between(conn, start_td, end_td):
|
|||||||
|
|
||||||
|
|
||||||
def _list_window_from_request():
|
def _list_window_from_request():
|
||||||
return resolve_window(request.args, default_preset=PRESET_UTC_TODAY)
|
return resolve_list_window(request.args, session, default_preset=PRESET_UTC_TODAY)
|
||||||
|
|
||||||
|
|
||||||
|
def _redirect_records():
|
||||||
|
qs = list_window_redirect_query(session)
|
||||||
|
return redirect(f"/records?{qs}" if qs else "/records")
|
||||||
|
|
||||||
|
|
||||||
def _pnl_row_matches_segment(row, segment_key):
|
def _pnl_row_matches_segment(row, segment_key):
|
||||||
@@ -5328,7 +5354,7 @@ def add_miss():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("已记录错过机会")
|
flash("已记录错过机会")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/add_journal", methods=["POST"])
|
@app.route("/add_journal", methods=["POST"])
|
||||||
@@ -5338,15 +5364,15 @@ def add_journal():
|
|||||||
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
||||||
if not entry_reason_norm:
|
if not entry_reason_norm:
|
||||||
flash("请选择开仓类型")
|
flash("请选择开仓类型")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
|
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
|
||||||
early_exit_note = str(d.get("early_exit_note") or "").strip()
|
early_exit_note = str(d.get("early_exit_note") or "").strip()
|
||||||
if not early_exit_trigger:
|
if not early_exit_trigger:
|
||||||
flash("请选择离场触发")
|
flash("请选择离场触发")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
||||||
flash("手工平仓必须填写补充说明")
|
flash("手工平仓必须填写补充说明")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
if early_exit_trigger != "手动平仓":
|
if early_exit_trigger != "手动平仓":
|
||||||
early_exit_note = ""
|
early_exit_note = ""
|
||||||
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
||||||
@@ -5445,7 +5471,7 @@ def add_journal():
|
|||||||
flash(f"交易复盘记录已保存。{chart_msg}")
|
flash(f"交易复盘记录已保存。{chart_msg}")
|
||||||
else:
|
else:
|
||||||
flash("交易复盘记录已保存")
|
flash("交易复盘记录已保存")
|
||||||
return redirect("/records")
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/journals")
|
@app.route("/api/journals")
|
||||||
|
|||||||
@@ -73,3 +73,55 @@ def utc_window_to_bj_sql_strings(start_utc, end_utc, app_tz):
|
|||||||
start_bj = start_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
|
start_bj = start_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
end_bj = end_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
|
end_bj = end_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
return start_bj, end_bj
|
return start_bj, end_bj
|
||||||
|
|
||||||
|
|
||||||
|
SESSION_KEY_LIST_WIN = "list_win_filter"
|
||||||
|
|
||||||
|
|
||||||
|
def query_mapping_from_session(session_store):
|
||||||
|
"""从 Flask session 恢复 win_preset / from_utc / to_utc。"""
|
||||||
|
if not session_store:
|
||||||
|
return {}
|
||||||
|
block = session_store.get(SESSION_KEY_LIST_WIN)
|
||||||
|
if not isinstance(block, dict):
|
||||||
|
return {}
|
||||||
|
preset = (block.get("preset") or "").strip()
|
||||||
|
if not preset:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"win_preset": preset,
|
||||||
|
"from_utc": (block.get("from_utc") or "").strip(),
|
||||||
|
"to_utc": (block.get("to_utc") or "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_list_window(query_mapping, session_store=None, default_preset=PRESET_UTC_TODAY):
|
||||||
|
"""
|
||||||
|
URL 带 win_preset 时解析并写入 session;无参数时用 session 中上次「应用」的预设。
|
||||||
|
"""
|
||||||
|
qm = query_mapping or {}
|
||||||
|
preset_in_q = (qm.get("win_preset") or "").strip()
|
||||||
|
if preset_in_q:
|
||||||
|
win = resolve_window(qm, default_preset=default_preset)
|
||||||
|
if session_store is not None:
|
||||||
|
session_store[SESSION_KEY_LIST_WIN] = {
|
||||||
|
"preset": win["preset"],
|
||||||
|
"from_utc": (qm.get("from_utc") or "").strip(),
|
||||||
|
"to_utc": (qm.get("to_utc") or "").strip(),
|
||||||
|
}
|
||||||
|
return win
|
||||||
|
stored = query_mapping_from_session(session_store)
|
||||||
|
if stored.get("win_preset"):
|
||||||
|
return resolve_window(stored, default_preset=default_preset)
|
||||||
|
return resolve_window(qm, default_preset=default_preset)
|
||||||
|
|
||||||
|
|
||||||
|
def list_window_redirect_query(session_store):
|
||||||
|
"""复盘/表单 POST 后重定向时附带列表筛选 query。"""
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
stored = query_mapping_from_session(session_store)
|
||||||
|
if not stored.get("win_preset"):
|
||||||
|
return ""
|
||||||
|
params = {k: v for k, v in stored.items() if v}
|
||||||
|
return urlencode(params)
|
||||||
|
|||||||
Reference in New Issue
Block a user