Speed up journal save with AJAX and async K-line charts.
Defer exchange chart generation to a background thread and save via XHR so fill-from-trade no longer blocks on full page reload. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9352,29 +9352,28 @@ def del_order(id):
|
|||||||
@app.route("/add_journal", methods=["POST"])
|
@app.route("/add_journal", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def add_journal():
|
def add_journal():
|
||||||
|
from lib.instance.journal_chart_async_lib import journal_ajax_or_flash_error
|
||||||
|
|
||||||
d = request.form
|
d = request.form
|
||||||
order_type_norm = normalize_journal_order_type(d.get("order_type"))
|
order_type_norm = normalize_journal_order_type(d.get("order_type"))
|
||||||
if not order_type_norm:
|
if not order_type_norm:
|
||||||
flash("请选择下单类型")
|
return journal_ajax_or_flash_error(request, "请选择下单类型", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint"))
|
direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint"))
|
||||||
if not direction_norm:
|
if not direction_norm:
|
||||||
flash("请选择方向")
|
return journal_ajax_or_flash_error(request, "请选择方向", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
entry_reason_norm = normalize_journal_entry_reason(
|
entry_reason_norm = normalize_journal_entry_reason(
|
||||||
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
|
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
|
||||||
)
|
)
|
||||||
if not entry_reason_norm:
|
if not entry_reason_norm:
|
||||||
flash("请选择开仓类型")
|
return journal_ajax_or_flash_error(request, "请选择开仓类型", redirect_fn=_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("请选择离场触发")
|
return journal_ajax_or_flash_error(request, "请选择离场触发", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
||||||
flash("手工平仓必须填写补充说明")
|
return journal_ajax_or_flash_error(
|
||||||
return _redirect_records()
|
request, "手工平仓必须填写补充说明", redirect_fn=_redirect_records
|
||||||
|
)
|
||||||
if early_exit_trigger != "手动平仓":
|
if early_exit_trigger != "手动平仓":
|
||||||
early_exit_note = ""
|
early_exit_note = ""
|
||||||
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
||||||
@@ -9407,14 +9406,14 @@ def add_journal():
|
|||||||
|
|
||||||
want_exchange_chart = (
|
want_exchange_chart = (
|
||||||
not has_manual_uploads
|
not has_manual_uploads
|
||||||
|
and ORDER_CHART_ENABLED
|
||||||
and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes")
|
and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes")
|
||||||
)
|
)
|
||||||
chart_msg = None
|
chart_job = None
|
||||||
if want_exchange_chart and ORDER_CHART_ENABLED:
|
if want_exchange_chart:
|
||||||
coin = (d.get("coin") or "").strip().upper()
|
coin = (d.get("coin") or "").strip().upper()
|
||||||
symbol_guess = normalize_symbol_input(coin) or coin
|
symbol_guess = normalize_symbol_input(coin) or coin
|
||||||
exchange_symbol = normalize_exchange_symbol(symbol_guess)
|
exchange_symbol = normalize_exchange_symbol(symbol_guess)
|
||||||
title_prefix = f"{symbol_guess} journal {entry_id[:8]}"
|
|
||||||
journal_tfs = parse_journal_chart_timeframes(
|
journal_tfs = parse_journal_chart_timeframes(
|
||||||
d.get("journal_chart_tf1"),
|
d.get("journal_chart_tf1"),
|
||||||
d.get("journal_chart_tf2"),
|
d.get("journal_chart_tf2"),
|
||||||
@@ -9422,36 +9421,21 @@ def add_journal():
|
|||||||
)
|
)
|
||||||
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"))
|
chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor"))
|
||||||
marker_payload = {
|
chart_job = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"exchange_symbol": exchange_symbol,
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"title_prefix": f"{symbol_guess} journal {entry_id[:8]}",
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"journal_tfs": journal_tfs,
|
||||||
"exit_price": d.get("exit_price_hint"),
|
"journal_limit": journal_limit,
|
||||||
"stop_loss_price": d.get("stop_loss_hint"),
|
"marker_payload": {
|
||||||
"chart_anchor": chart_anchor,
|
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
||||||
"now_ts_ms": int(app_now().timestamp() * 1000),
|
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
||||||
|
"entry_price": d.get("entry_price_hint"),
|
||||||
|
"exit_price": d.get("exit_price_hint"),
|
||||||
|
"stop_loss_price": d.get("stop_loss_hint"),
|
||||||
|
"chart_anchor": chart_anchor,
|
||||||
|
"now_ts_ms": int(app_now().timestamp() * 1000),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
chart_fname = f"journal_{entry_id}.png"
|
|
||||||
saved = generate_multi_timeframe_chart_png(
|
|
||||||
exchange_symbol,
|
|
||||||
title_prefix,
|
|
||||||
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 journal_tfs},
|
|
||||||
layout="vertical",
|
|
||||||
)
|
|
||||||
if saved:
|
|
||||||
image_filename = saved
|
|
||||||
chart_msg = f"已生成复盘K线图({'/'.join(journal_tfs)} 各{journal_limit}根):/static/images/{saved}"
|
|
||||||
else:
|
|
||||||
chart_msg = "已勾选自动生成K线图,但生成失败(返回空).请检查 Pillow 是否安装,Binance 网络/代理是否正常."
|
|
||||||
except Exception as e:
|
|
||||||
chart_msg = f"自动生成K线图失败:{str(e)}"
|
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -9488,10 +9472,38 @@ def add_journal():
|
|||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
if chart_msg:
|
if chart_job:
|
||||||
flash(f"交易复盘记录已保存.{chart_msg}")
|
from lib.instance.journal_chart_async_lib import schedule_journal_exchange_chart
|
||||||
|
|
||||||
|
schedule_journal_exchange_chart(
|
||||||
|
entry_id=entry_id,
|
||||||
|
exchange_symbol=chart_job["exchange_symbol"],
|
||||||
|
title_prefix=chart_job["title_prefix"],
|
||||||
|
journal_tfs=chart_job["journal_tfs"],
|
||||||
|
journal_limit=chart_job["journal_limit"],
|
||||||
|
marker_payload=chart_job["marker_payload"],
|
||||||
|
upload_folder=app.config["UPLOAD_FOLDER"],
|
||||||
|
generate_chart_fn=generate_multi_timeframe_chart_png,
|
||||||
|
get_db_fn=get_db,
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
"交易复盘记录已保存.K 线图后台生成中"
|
||||||
|
f"({'/'.join(chart_job['journal_tfs'])} 各{chart_job['journal_limit']}根),稍后刷新列表可见."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
flash("交易复盘记录已保存")
|
msg = "交易复盘记录已保存"
|
||||||
|
from lib.instance.journal_chart_async_lib import request_wants_journal_ajax
|
||||||
|
|
||||||
|
if request_wants_journal_ajax(request):
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"id": entry_id,
|
||||||
|
"msg": msg,
|
||||||
|
"chart_pending": bool(chart_job),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
flash(msg)
|
||||||
return _redirect_records()
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+57
-45
@@ -9190,29 +9190,28 @@ def del_order(id):
|
|||||||
@app.route("/add_journal", methods=["POST"])
|
@app.route("/add_journal", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def add_journal():
|
def add_journal():
|
||||||
|
from lib.instance.journal_chart_async_lib import journal_ajax_or_flash_error
|
||||||
|
|
||||||
d = request.form
|
d = request.form
|
||||||
order_type_norm = normalize_journal_order_type(d.get("order_type"))
|
order_type_norm = normalize_journal_order_type(d.get("order_type"))
|
||||||
if not order_type_norm:
|
if not order_type_norm:
|
||||||
flash("请选择下单类型")
|
return journal_ajax_or_flash_error(request, "请选择下单类型", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint"))
|
direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint"))
|
||||||
if not direction_norm:
|
if not direction_norm:
|
||||||
flash("请选择方向")
|
return journal_ajax_or_flash_error(request, "请选择方向", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
entry_reason_norm = normalize_journal_entry_reason(
|
entry_reason_norm = normalize_journal_entry_reason(
|
||||||
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
|
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
|
||||||
)
|
)
|
||||||
if not entry_reason_norm:
|
if not entry_reason_norm:
|
||||||
flash("请选择开仓类型")
|
return journal_ajax_or_flash_error(request, "请选择开仓类型", redirect_fn=_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("请选择离场触发")
|
return journal_ajax_or_flash_error(request, "请选择离场触发", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
||||||
flash("手工平仓必须填写补充说明")
|
return journal_ajax_or_flash_error(
|
||||||
return _redirect_records()
|
request, "手工平仓必须填写补充说明", redirect_fn=_redirect_records
|
||||||
|
)
|
||||||
if early_exit_trigger != "手动平仓":
|
if early_exit_trigger != "手动平仓":
|
||||||
early_exit_note = ""
|
early_exit_note = ""
|
||||||
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
||||||
@@ -9245,14 +9244,14 @@ def add_journal():
|
|||||||
|
|
||||||
want_exchange_chart = (
|
want_exchange_chart = (
|
||||||
not has_manual_uploads
|
not has_manual_uploads
|
||||||
|
and ORDER_CHART_ENABLED
|
||||||
and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes")
|
and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes")
|
||||||
)
|
)
|
||||||
chart_msg = None
|
chart_job = None
|
||||||
if want_exchange_chart and ORDER_CHART_ENABLED:
|
if want_exchange_chart:
|
||||||
coin = (d.get("coin") or "").strip().upper()
|
coin = (d.get("coin") or "").strip().upper()
|
||||||
symbol_guess = normalize_symbol_input(coin) or coin
|
symbol_guess = normalize_symbol_input(coin) or coin
|
||||||
exchange_symbol = normalize_exchange_symbol(symbol_guess)
|
exchange_symbol = normalize_exchange_symbol(symbol_guess)
|
||||||
title_prefix = f"{symbol_guess} journal {entry_id[:8]}"
|
|
||||||
journal_tfs = parse_journal_chart_timeframes(
|
journal_tfs = parse_journal_chart_timeframes(
|
||||||
d.get("journal_chart_tf1"),
|
d.get("journal_chart_tf1"),
|
||||||
d.get("journal_chart_tf2"),
|
d.get("journal_chart_tf2"),
|
||||||
@@ -9260,36 +9259,21 @@ def add_journal():
|
|||||||
)
|
)
|
||||||
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"))
|
chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor"))
|
||||||
marker_payload = {
|
chart_job = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"exchange_symbol": exchange_symbol,
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"title_prefix": f"{symbol_guess} journal {entry_id[:8]}",
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"journal_tfs": journal_tfs,
|
||||||
"exit_price": d.get("exit_price_hint"),
|
"journal_limit": journal_limit,
|
||||||
"stop_loss_price": d.get("stop_loss_hint"),
|
"marker_payload": {
|
||||||
"chart_anchor": chart_anchor,
|
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
||||||
"now_ts_ms": int(app_now().timestamp() * 1000),
|
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
||||||
|
"entry_price": d.get("entry_price_hint"),
|
||||||
|
"exit_price": d.get("exit_price_hint"),
|
||||||
|
"stop_loss_price": d.get("stop_loss_hint"),
|
||||||
|
"chart_anchor": chart_anchor,
|
||||||
|
"now_ts_ms": int(app_now().timestamp() * 1000),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
chart_fname = f"journal_{entry_id}.png"
|
|
||||||
saved = generate_multi_timeframe_chart_png(
|
|
||||||
exchange_symbol,
|
|
||||||
title_prefix,
|
|
||||||
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 journal_tfs},
|
|
||||||
layout="vertical",
|
|
||||||
)
|
|
||||||
if saved:
|
|
||||||
image_filename = saved
|
|
||||||
chart_msg = f"已生成复盘K线图({'/'.join(journal_tfs)} 各{journal_limit}根):/static/images/{saved}"
|
|
||||||
else:
|
|
||||||
chart_msg = "已勾选自动生成K线图,但生成失败(返回空).请检查 Pillow 是否安装,Gate 网络/代理是否正常."
|
|
||||||
except Exception as e:
|
|
||||||
chart_msg = f"自动生成K线图失败:{str(e)}"
|
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -9326,10 +9310,38 @@ def add_journal():
|
|||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
if chart_msg:
|
if chart_job:
|
||||||
flash(f"交易复盘记录已保存.{chart_msg}")
|
from lib.instance.journal_chart_async_lib import schedule_journal_exchange_chart
|
||||||
|
|
||||||
|
schedule_journal_exchange_chart(
|
||||||
|
entry_id=entry_id,
|
||||||
|
exchange_symbol=chart_job["exchange_symbol"],
|
||||||
|
title_prefix=chart_job["title_prefix"],
|
||||||
|
journal_tfs=chart_job["journal_tfs"],
|
||||||
|
journal_limit=chart_job["journal_limit"],
|
||||||
|
marker_payload=chart_job["marker_payload"],
|
||||||
|
upload_folder=app.config["UPLOAD_FOLDER"],
|
||||||
|
generate_chart_fn=generate_multi_timeframe_chart_png,
|
||||||
|
get_db_fn=get_db,
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
"交易复盘记录已保存.K 线图后台生成中"
|
||||||
|
f"({'/'.join(chart_job['journal_tfs'])} 各{chart_job['journal_limit']}根),稍后刷新列表可见."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
flash("交易复盘记录已保存")
|
msg = "交易复盘记录已保存"
|
||||||
|
from lib.instance.journal_chart_async_lib import request_wants_journal_ajax
|
||||||
|
|
||||||
|
if request_wants_journal_ajax(request):
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"id": entry_id,
|
||||||
|
"msg": msg,
|
||||||
|
"chart_pending": bool(chart_job),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
flash(msg)
|
||||||
return _redirect_records()
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+57
-45
@@ -8857,29 +8857,28 @@ def del_order(id):
|
|||||||
@app.route("/add_journal", methods=["POST"])
|
@app.route("/add_journal", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def add_journal():
|
def add_journal():
|
||||||
|
from lib.instance.journal_chart_async_lib import journal_ajax_or_flash_error
|
||||||
|
|
||||||
d = request.form
|
d = request.form
|
||||||
order_type_norm = normalize_journal_order_type(d.get("order_type"))
|
order_type_norm = normalize_journal_order_type(d.get("order_type"))
|
||||||
if not order_type_norm:
|
if not order_type_norm:
|
||||||
flash("请选择下单类型")
|
return journal_ajax_or_flash_error(request, "请选择下单类型", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint"))
|
direction_norm = normalize_journal_direction(d.get("direction") or d.get("direction_hint"))
|
||||||
if not direction_norm:
|
if not direction_norm:
|
||||||
flash("请选择方向")
|
return journal_ajax_or_flash_error(request, "请选择方向", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
entry_reason_norm = normalize_journal_entry_reason(
|
entry_reason_norm = normalize_journal_entry_reason(
|
||||||
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
|
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
|
||||||
)
|
)
|
||||||
if not entry_reason_norm:
|
if not entry_reason_norm:
|
||||||
flash("请选择开仓类型")
|
return journal_ajax_or_flash_error(request, "请选择开仓类型", redirect_fn=_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("请选择离场触发")
|
return journal_ajax_or_flash_error(request, "请选择离场触发", redirect_fn=_redirect_records)
|
||||||
return _redirect_records()
|
|
||||||
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
||||||
flash("手工平仓必须填写补充说明")
|
return journal_ajax_or_flash_error(
|
||||||
return _redirect_records()
|
request, "手工平仓必须填写补充说明", redirect_fn=_redirect_records
|
||||||
|
)
|
||||||
if early_exit_trigger != "手动平仓":
|
if early_exit_trigger != "手动平仓":
|
||||||
early_exit_note = ""
|
early_exit_note = ""
|
||||||
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
||||||
@@ -8912,14 +8911,14 @@ def add_journal():
|
|||||||
|
|
||||||
want_exchange_chart = (
|
want_exchange_chart = (
|
||||||
not has_manual_uploads
|
not has_manual_uploads
|
||||||
|
and ORDER_CHART_ENABLED
|
||||||
and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes")
|
and d.get("journal_exchange_chart", "").lower() in ("1", "true", "on", "yes")
|
||||||
)
|
)
|
||||||
chart_msg = None
|
chart_job = None
|
||||||
if want_exchange_chart and ORDER_CHART_ENABLED:
|
if want_exchange_chart:
|
||||||
coin = (d.get("coin") or "").strip().upper()
|
coin = (d.get("coin") or "").strip().upper()
|
||||||
symbol_guess = normalize_symbol_input(coin) or coin
|
symbol_guess = normalize_symbol_input(coin) or coin
|
||||||
exchange_symbol = normalize_okx_symbol(symbol_guess)
|
exchange_symbol = normalize_okx_symbol(symbol_guess)
|
||||||
title_prefix = f"{symbol_guess} journal {entry_id[:8]}"
|
|
||||||
journal_tfs = parse_journal_chart_timeframes(
|
journal_tfs = parse_journal_chart_timeframes(
|
||||||
d.get("journal_chart_tf1"),
|
d.get("journal_chart_tf1"),
|
||||||
d.get("journal_chart_tf2"),
|
d.get("journal_chart_tf2"),
|
||||||
@@ -8927,36 +8926,21 @@ def add_journal():
|
|||||||
)
|
)
|
||||||
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"))
|
chart_anchor = parse_journal_chart_anchor(d.get("journal_chart_anchor"))
|
||||||
marker_payload = {
|
chart_job = {
|
||||||
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
"exchange_symbol": exchange_symbol,
|
||||||
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
"title_prefix": f"{symbol_guess} journal {entry_id[:8]}",
|
||||||
"entry_price": d.get("entry_price_hint"),
|
"journal_tfs": journal_tfs,
|
||||||
"exit_price": d.get("exit_price_hint"),
|
"journal_limit": journal_limit,
|
||||||
"stop_loss_price": d.get("stop_loss_hint"),
|
"marker_payload": {
|
||||||
"chart_anchor": chart_anchor,
|
"entry_ts_ms": _local_input_datetime_to_ms(d.get("open_datetime")),
|
||||||
"now_ts_ms": int(app_now().timestamp() * 1000),
|
"exit_ts_ms": _local_input_datetime_to_ms(d.get("close_datetime")),
|
||||||
|
"entry_price": d.get("entry_price_hint"),
|
||||||
|
"exit_price": d.get("exit_price_hint"),
|
||||||
|
"stop_loss_price": d.get("stop_loss_hint"),
|
||||||
|
"chart_anchor": chart_anchor,
|
||||||
|
"now_ts_ms": int(app_now().timestamp() * 1000),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
chart_fname = f"journal_{entry_id}.png"
|
|
||||||
saved = generate_multi_timeframe_chart_png(
|
|
||||||
exchange_symbol,
|
|
||||||
title_prefix,
|
|
||||||
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 journal_tfs},
|
|
||||||
layout="vertical",
|
|
||||||
)
|
|
||||||
if saved:
|
|
||||||
image_filename = saved
|
|
||||||
chart_msg = f"已生成复盘K线图({'/'.join(journal_tfs)} 各{journal_limit}根):/static/images/{saved}"
|
|
||||||
else:
|
|
||||||
chart_msg = "已勾选自动生成K线图,但生成失败(返回空).请检查 Pillow 是否安装,OKX 网络/代理是否正常."
|
|
||||||
except Exception as e:
|
|
||||||
chart_msg = f"自动生成K线图失败:{str(e)}"
|
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -8993,10 +8977,38 @@ def add_journal():
|
|||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
if chart_msg:
|
if chart_job:
|
||||||
flash(f"交易复盘记录已保存.{chart_msg}")
|
from lib.instance.journal_chart_async_lib import schedule_journal_exchange_chart
|
||||||
|
|
||||||
|
schedule_journal_exchange_chart(
|
||||||
|
entry_id=entry_id,
|
||||||
|
exchange_symbol=chart_job["exchange_symbol"],
|
||||||
|
title_prefix=chart_job["title_prefix"],
|
||||||
|
journal_tfs=chart_job["journal_tfs"],
|
||||||
|
journal_limit=chart_job["journal_limit"],
|
||||||
|
marker_payload=chart_job["marker_payload"],
|
||||||
|
upload_folder=app.config["UPLOAD_FOLDER"],
|
||||||
|
generate_chart_fn=generate_multi_timeframe_chart_png,
|
||||||
|
get_db_fn=get_db,
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
"交易复盘记录已保存.K 线图后台生成中"
|
||||||
|
f"({'/'.join(chart_job['journal_tfs'])} 各{chart_job['journal_limit']}根),稍后刷新列表可见."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
flash("交易复盘记录已保存")
|
msg = "交易复盘记录已保存"
|
||||||
|
from lib.instance.journal_chart_async_lib import request_wants_journal_ajax
|
||||||
|
|
||||||
|
if request_wants_journal_ajax(request):
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"id": entry_id,
|
||||||
|
"msg": msg,
|
||||||
|
"chart_pending": bool(chart_job),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
flash(msg)
|
||||||
return _redirect_records()
|
return _redirect_records()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* 复盘表单 AJAX 保存:避免整页刷新卡顿;配合 FormSubmitGuard 即时反馈.
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function $(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast(msg) {
|
||||||
|
if (!msg) return;
|
||||||
|
try {
|
||||||
|
if (global.InstanceTheme && typeof InstanceTheme.toast === "function") {
|
||||||
|
InstanceTheme.toast(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
alert(msg);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLists() {
|
||||||
|
if (typeof global.loadJournals === "function") {
|
||||||
|
try {
|
||||||
|
global.loadJournals();
|
||||||
|
} catch (_) {}
|
||||||
|
} else if (global.RecordsReviewPage && typeof RecordsReviewPage.loadJournals === "function") {
|
||||||
|
try {
|
||||||
|
RecordsReviewPage.loadJournals();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetJournalForm(form) {
|
||||||
|
if (!form) return;
|
||||||
|
form.reset();
|
||||||
|
["risk-amount-hint", "entry-price-hint", "stop-loss-hint", "exit-price-hint", "direction-hint"].forEach(
|
||||||
|
function (id) {
|
||||||
|
var el = $(id);
|
||||||
|
if (el) el.value = "";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (global.JournalUploadSlots && typeof JournalUploadSlots.reset === "function") {
|
||||||
|
JournalUploadSlots.reset(form);
|
||||||
|
}
|
||||||
|
if (typeof global.syncEarlyExitNoteRequired === "function") {
|
||||||
|
try {
|
||||||
|
global.syncEarlyExitNoteRequired();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
if (global.RecordsReviewPage && typeof RecordsReviewPage.hideJournalCard === "function") {
|
||||||
|
try {
|
||||||
|
RecordsReviewPage.hideJournalCard();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSuccess(form, data) {
|
||||||
|
var msg = (data && data.msg) || "交易复盘记录已保存";
|
||||||
|
toast(msg);
|
||||||
|
resetJournalForm(form);
|
||||||
|
refreshLists();
|
||||||
|
if (data && data.chart_pending) {
|
||||||
|
// K 线后台生成,数秒后再刷一次列表拿缩略图
|
||||||
|
setTimeout(refreshLists, 4000);
|
||||||
|
setTimeout(refreshLists, 12000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind(form) {
|
||||||
|
if (!form || form.dataset.journalAjaxBound === "1") return;
|
||||||
|
form.dataset.journalAjaxBound = "1";
|
||||||
|
form.addEventListener("submit", function (ev) {
|
||||||
|
if (typeof global.validateJournalEntryReason === "function") {
|
||||||
|
if (!global.validateJournalEntryReason()) {
|
||||||
|
ev.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ev.preventDefault();
|
||||||
|
if (global.FormSubmitGuard && FormSubmitGuard.isLocked(form)) return;
|
||||||
|
if (global.FormSubmitGuard) FormSubmitGuard.lock(form, "保存中…");
|
||||||
|
|
||||||
|
var fd = new FormData(form);
|
||||||
|
fd.set("ajax", "1");
|
||||||
|
var action = form.getAttribute("action") || "/add_journal";
|
||||||
|
|
||||||
|
fetch(action, {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
var ct = (res.headers.get("content-type") || "").toLowerCase();
|
||||||
|
if (ct.indexOf("application/json") >= 0) {
|
||||||
|
return res.json().then(function (data) {
|
||||||
|
return { ok: res.ok, data: data, redirected: false };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 旧后端未返回 JSON 时走整页逻辑
|
||||||
|
if (res.redirected || res.ok) {
|
||||||
|
global.location.reload();
|
||||||
|
return { ok: true, data: null, redirected: true };
|
||||||
|
}
|
||||||
|
throw new Error("save failed");
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
if (result.redirected) return;
|
||||||
|
if (!result.ok || !result.data || result.data.ok === false) {
|
||||||
|
var err =
|
||||||
|
(result.data && (result.data.msg || result.data.error)) || "保存失败";
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
onSuccess(form, result.data);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
toast((err && err.message) || "保存失败,请稍后重试");
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
if (global.FormSubmitGuard) FormSubmitGuard.unlock(form);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
bind($("journal-form"));
|
||||||
|
}
|
||||||
|
|
||||||
|
global.JournalFormSave = { init: init, bind: bind };
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
@@ -66,6 +66,7 @@ def install_instance_theme_static(app) -> None:
|
|||||||
"manual_order_rr_preview.js": "application/javascript; charset=utf-8",
|
"manual_order_rr_preview.js": "application/javascript; charset=utf-8",
|
||||||
"symbol_live_price.js": "application/javascript; charset=utf-8",
|
"symbol_live_price.js": "application/javascript; charset=utf-8",
|
||||||
"journal_upload_slots.js": "application/javascript; charset=utf-8",
|
"journal_upload_slots.js": "application/javascript; charset=utf-8",
|
||||||
|
"journal_form_save.js": "application/javascript; charset=utf-8",
|
||||||
"strategy_roll.js": "application/javascript; charset=utf-8",
|
"strategy_roll.js": "application/javascript; charset=utf-8",
|
||||||
"instance_page.css": "text/css; charset=utf-8",
|
"instance_page.css": "text/css; charset=utf-8",
|
||||||
"instance_embed.js": "application/javascript; charset=utf-8",
|
"instance_embed.js": "application/javascript; charset=utf-8",
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""复盘自动 K 线:后台线程生成,避免 /add_journal 同步卡住."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_journal_exchange_chart(
|
||||||
|
*,
|
||||||
|
entry_id: str,
|
||||||
|
exchange_symbol: str,
|
||||||
|
title_prefix: str,
|
||||||
|
journal_tfs: list[str],
|
||||||
|
journal_limit: int,
|
||||||
|
marker_payload: dict[str, Any],
|
||||||
|
upload_folder: str,
|
||||||
|
generate_chart_fn: Callable[..., str | None],
|
||||||
|
get_db_fn: Callable[[], Any],
|
||||||
|
) -> None:
|
||||||
|
"""提交后立即返回;线程内画图并写回 journal_entries.image."""
|
||||||
|
entry_id = str(entry_id or "").strip()
|
||||||
|
if not entry_id or not callable(generate_chart_fn) or not callable(get_db_fn):
|
||||||
|
return
|
||||||
|
|
||||||
|
tfs = [str(x).strip() for x in (journal_tfs or []) if str(x).strip()]
|
||||||
|
if not tfs:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _run() -> None:
|
||||||
|
try:
|
||||||
|
chart_fname = f"journal_{entry_id}.png"
|
||||||
|
saved = generate_chart_fn(
|
||||||
|
exchange_symbol,
|
||||||
|
title_prefix,
|
||||||
|
timeframes=tfs,
|
||||||
|
limit=journal_limit,
|
||||||
|
out_dir=upload_folder,
|
||||||
|
filename=chart_fname,
|
||||||
|
filename_prefix="journal",
|
||||||
|
marker_payload=marker_payload,
|
||||||
|
marker_timeframes={x.lower() for x in tfs},
|
||||||
|
layout="vertical",
|
||||||
|
)
|
||||||
|
if not saved:
|
||||||
|
logger.warning("journal chart async empty entry_id=%s", entry_id)
|
||||||
|
return
|
||||||
|
conn = get_db_fn()
|
||||||
|
try:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE journal_entries SET image=? WHERE id=?",
|
||||||
|
(saved, entry_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("journal chart async failed entry_id=%s", entry_id)
|
||||||
|
|
||||||
|
threading.Thread(
|
||||||
|
target=_run,
|
||||||
|
name=f"journal-chart-{entry_id[:8]}",
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
|
||||||
|
def request_wants_journal_ajax(request: Any) -> bool:
|
||||||
|
"""XHR / Accept:json / form ajax=1 → 返回 JSON,避免整页刷新."""
|
||||||
|
xrw = str(getattr(request, "headers", {}).get("X-Requested-With") or "").lower()
|
||||||
|
if xrw == "xmlhttprequest":
|
||||||
|
return True
|
||||||
|
form = getattr(request, "form", None)
|
||||||
|
if form is not None:
|
||||||
|
raw = str(form.get("ajax") or "").strip().lower()
|
||||||
|
if raw in ("1", "true", "yes", "on"):
|
||||||
|
return True
|
||||||
|
accept = str(getattr(request, "headers", {}).get("Accept") or "").lower()
|
||||||
|
if "application/json" in accept and accept.strip().startswith("application/json"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def journal_ajax_or_flash_error(request: Any, msg: str, *, redirect_fn: Callable[[], Any]):
|
||||||
|
"""校验失败:AJAX 返回 JSON,否则 flash + 跳转."""
|
||||||
|
from flask import flash, jsonify
|
||||||
|
|
||||||
|
if request_wants_journal_ajax(request):
|
||||||
|
return jsonify({"ok": False, "msg": msg}), 400
|
||||||
|
flash(msg)
|
||||||
|
return redirect_fn()
|
||||||
@@ -1318,13 +1318,6 @@ if(window.ManualOrderRrPreview){
|
|||||||
|
|
||||||
refreshAccountSnapshot();
|
refreshAccountSnapshot();
|
||||||
if (window.AccountRiskBadge) AccountRiskBadge.startTicker();
|
if (window.AccountRiskBadge) AccountRiskBadge.startTicker();
|
||||||
const _journalFormEl = document.getElementById("journal-form");
|
|
||||||
if(_journalFormEl){
|
|
||||||
_journalFormEl.addEventListener("submit", function(ev){
|
|
||||||
if(!validateJournalEntryReason()) ev.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const addOrderForm = document.getElementById("add-order-form");
|
const addOrderForm = document.getElementById("add-order-form");
|
||||||
if(addOrderForm){
|
if(addOrderForm){
|
||||||
addOrderForm.addEventListener("submit", function(ev){
|
addOrderForm.addEventListener("submit", function(ev){
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
<script src="/static/time_close_ui.js?v=3"></script>
|
<script src="/static/time_close_ui.js?v=3"></script>
|
||||||
<script src="/static/ai_review_render.js?v=2"></script>
|
<script src="/static/ai_review_render.js?v=2"></script>
|
||||||
<script src="/static/form_submit_guard.js?v=2"></script>
|
<script src="/static/form_submit_guard.js?v=2"></script>
|
||||||
|
<script src="/static/journal_form_save.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
|
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
|
||||||
const ORDER_ENTRY_MODEL_CATEGORIES = {{ entry_model_categories | tojson }};
|
const ORDER_ENTRY_MODEL_CATEGORIES = {{ entry_model_categories | tojson }};
|
||||||
|
|||||||
@@ -456,6 +456,7 @@
|
|||||||
<script src="/static/time_close_ui.js?v=3"></script>
|
<script src="/static/time_close_ui.js?v=3"></script>
|
||||||
<script src="/static/ai_review_render.js?v=2"></script>
|
<script src="/static/ai_review_render.js?v=2"></script>
|
||||||
<script src="/static/form_submit_guard.js?v=2"></script>
|
<script src="/static/form_submit_guard.js?v=2"></script>
|
||||||
|
<script src="/static/journal_form_save.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
|
const ORDER_ENTRY_MODEL_TRADE_STYLE = {{ entry_model_trade_style_map | tojson }};
|
||||||
const ORDER_ENTRY_MODEL_CATEGORIES = {{ entry_model_categories | tojson }};
|
const ORDER_ENTRY_MODEL_CATEGORIES = {{ entry_model_categories | tojson }};
|
||||||
@@ -1823,13 +1824,6 @@ if(window.ManualOrderRrPreview){
|
|||||||
|
|
||||||
refreshAccountSnapshot();
|
refreshAccountSnapshot();
|
||||||
if (window.AccountRiskBadge) AccountRiskBadge.startTicker();
|
if (window.AccountRiskBadge) AccountRiskBadge.startTicker();
|
||||||
const _journalFormEl = document.getElementById("journal-form");
|
|
||||||
if(_journalFormEl){
|
|
||||||
_journalFormEl.addEventListener("submit", function(ev){
|
|
||||||
if(!validateJournalEntryReason()) ev.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const addOrderForm = document.getElementById("add-order-form");
|
const addOrderForm = document.getElementById("add-order-form");
|
||||||
if(addOrderForm){
|
if(addOrderForm){
|
||||||
addOrderForm.addEventListener("submit", function(ev){
|
addOrderForm.addEventListener("submit", function(ev){
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
|
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
|
||||||
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
||||||
<input type="checkbox" name="journal_exchange_chart" value="true">
|
<input type="checkbox" name="journal_exchange_chart" value="true">
|
||||||
保存时自动生成 K 线图并作为截图
|
保存后后台生成 K 线图并作为截图(不阻塞保存)
|
||||||
</label>
|
</label>
|
||||||
<label style="font-size:.82rem;color:#9aa">周期1</label>
|
<label style="font-size:.82rem;color:#9aa">周期1</label>
|
||||||
<select name="journal_chart_tf1" style="min-width:72px">
|
<select name="journal_chart_tf1" style="min-width:72px">
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from lib.instance.journal_chart_async_lib import (
|
||||||
|
request_wants_journal_ajax,
|
||||||
|
schedule_journal_exchange_chart,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeHeaders(dict):
|
||||||
|
def get(self, key, default=None):
|
||||||
|
for k, v in self.items():
|
||||||
|
if str(k).lower() == str(key).lower():
|
||||||
|
return v
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
class JournalChartAsyncLibTests(TestCase):
|
||||||
|
def test_request_wants_journal_ajax_xhr(self):
|
||||||
|
req = MagicMock()
|
||||||
|
req.headers = _FakeHeaders({"X-Requested-With": "XMLHttpRequest"})
|
||||||
|
req.form = {}
|
||||||
|
self.assertTrue(request_wants_journal_ajax(req))
|
||||||
|
|
||||||
|
def test_request_wants_journal_ajax_form(self):
|
||||||
|
req = MagicMock()
|
||||||
|
req.headers = _FakeHeaders({})
|
||||||
|
req.form = {"ajax": "1"}
|
||||||
|
self.assertTrue(request_wants_journal_ajax(req))
|
||||||
|
|
||||||
|
def test_request_wants_journal_ajax_plain(self):
|
||||||
|
req = MagicMock()
|
||||||
|
req.headers = _FakeHeaders({"Accept": "text/html"})
|
||||||
|
req.form = {}
|
||||||
|
self.assertFalse(request_wants_journal_ajax(req))
|
||||||
|
|
||||||
|
@patch("lib.instance.journal_chart_async_lib.threading.Thread")
|
||||||
|
def test_schedule_starts_daemon_thread(self, mock_thread_cls):
|
||||||
|
thread = MagicMock()
|
||||||
|
mock_thread_cls.return_value = thread
|
||||||
|
schedule_journal_exchange_chart(
|
||||||
|
entry_id="abc123",
|
||||||
|
exchange_symbol="BTC/USDT:USDT",
|
||||||
|
title_prefix="t",
|
||||||
|
journal_tfs=["5m", "15m"],
|
||||||
|
journal_limit=100,
|
||||||
|
marker_payload={},
|
||||||
|
upload_folder="/tmp",
|
||||||
|
generate_chart_fn=MagicMock(),
|
||||||
|
get_db_fn=MagicMock(),
|
||||||
|
)
|
||||||
|
mock_thread_cls.assert_called_once()
|
||||||
|
kwargs = mock_thread_cls.call_args.kwargs
|
||||||
|
self.assertTrue(kwargs.get("daemon"))
|
||||||
|
thread.start.assert_called_once()
|
||||||
Reference in New Issue
Block a user