feat: add timed position close (1h/2h/4h) for key levels and live orders

Program monitors open positions and market-closes at deadline; UI shows label and countdown on instance and hub boards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 19:30:16 +08:00
parent 879ea5e228
commit 959593cdab
17 changed files with 1152 additions and 69 deletions
+79 -20
View File
@@ -100,6 +100,17 @@ from key_sl_tp_lib import (
sl_tp_mode_label,
sl_tp_plan_summary_text,
)
from time_close_lib import (
TIME_CLOSE_RESULT,
apply_time_close_to_payload,
ensure_time_close_schema,
parse_time_close_enabled_form,
parse_time_close_hours_form,
should_trigger_time_close,
time_close_insert_values,
time_close_label,
time_close_settings_from_row,
)
from manual_sltp_lib import (
normalize_open_sltp_mode,
resolve_entrust_sltp_prices,
@@ -1426,6 +1437,7 @@ def init_db():
c.execute(ddl)
except Exception:
pass
ensure_time_close_schema(c)
try:
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
except Exception:
@@ -4493,6 +4505,8 @@ def _market_open_for_key_monitor(
take_profit,
key_signal_type=None,
breakeven_enabled=0,
time_close_enabled=0,
time_close_hours=None,
):
"""
与手动实盘下单对齐的市价开仓与 order_monitors 写入
@@ -4609,14 +4623,18 @@ def _market_open_for_key_monitor(
breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
be_enabled = 1 if int(breakeven_enabled or 0) != 0 else 0
tc_en, tc_h, tc_at = time_close_insert_values(
time_close_enabled, time_close_hours, opened_at_ms
)
conn.execute(
"INSERT INTO order_monitors "
"(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
"margin_capital, leverage, trade_style, risk_percent, risk_amount, "
"breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, "
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type, "
"time_close_enabled, time_close_hours, time_close_at_ms) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol,
exchange_symbol,
@@ -4646,6 +4664,9 @@ def _market_open_for_key_monitor(
trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO,
stored_key_signal_type(key_signal_type),
tc_en,
tc_h,
tc_at,
),
)
new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
@@ -4835,13 +4856,16 @@ def _insert_order_monitor_from_fib_fill(
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
opened_at_bj = app_now_str()
opened_at_ms = _to_ms_with_fallback(None, opened_at_bj)
tc_en, tc_h, _ = time_close_settings_from_row(row)
tc_en, tc_h, tc_at = time_close_insert_values(tc_en, tc_h, opened_at_ms)
conn.execute(
"INSERT INTO order_monitors "
"(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, "
"margin_capital, leverage, trade_style, risk_percent, risk_amount, "
"breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, "
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, key_signal_type, "
"time_close_enabled, time_close_hours, time_close_at_ms) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol,
exchange_symbol,
@@ -4871,6 +4895,9 @@ def _insert_order_monitor_from_fib_fill(
trading_day,
ORDER_MONITOR_TYPE_KEY_AUTO,
stored_key_signal_type(typ),
tc_en,
tc_h,
tc_at,
),
)
new_order_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
@@ -5040,6 +5067,7 @@ def _false_breakout_exists_for_symbol(conn, symbol):
def _add_false_breakout_key_monitor(
conn, symbol, direction_sel, upper_px, lower_px, key_px, breakeven_enabled=0,
time_close_enabled=0, time_close_hours=None,
):
if _false_breakout_exists_for_symbol(conn, symbol):
return False, f"{symbol} 已有假突破监控(同币仅允许一条)"
@@ -5096,21 +5124,25 @@ def _add_false_breakout_key_monitor(
except Exception as e:
return False, friendly_exchange_error(e, available_usdt=available_usdt)
be_flag = 1 if int(breakeven_enabled or 0) != 0 else 0
tc_en, tc_h, _ = time_close_insert_values(time_close_enabled, time_close_hours, None)
conn.execute(
"INSERT INTO key_monitors "
"(symbol, monitor_type, direction, upper, lower, "
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled, time_close_enabled, time_close_hours) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, FALSE_BREAKOUT_MONITOR_TYPE, direction_sel, upper_px, lower_px,
oid, entry, sl, tp, float(amount), margin_capital, leverage, be_flag,
oid, entry, sl, tp, float(amount), margin_capital, leverage, be_flag, tc_en, tc_h,
),
)
return True, None
def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=0):
def _add_fib_key_monitor(
conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=0,
time_close_enabled=0, time_close_hours=None,
):
if _fib_key_exists_for_symbol(conn, symbol):
return False, f"{symbol} 已有斐波监控(同币仅允许一条 0.618/0.786"
ratio = fib_ratio_from_type(mt)
@@ -5171,15 +5203,16 @@ def _add_fib_key_monitor(conn, symbol, direction_sel, mt, upper_px, lower_px, br
except Exception as e:
return False, friendly_exchange_error(e, available_usdt=available_usdt)
be_flag = 1 if int(breakeven_enabled or 0) != 0 else 0
tc_en, tc_h, _ = time_close_insert_values(time_close_enabled, time_close_hours, None)
conn.execute(
"INSERT INTO key_monitors "
"(symbol, monitor_type, direction, upper, lower, "
"fib_limit_order_id, fib_entry_price, fib_stop_loss, fib_take_profit, "
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
"fib_order_amount, fib_margin_capital, fib_leverage, breakeven_enabled, time_close_enabled, time_close_hours) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, mt, direction_sel, upper_px, lower_px,
oid, entry, sl, tp, float(amount), margin_capital, leverage, be_flag,
oid, entry, sl, tp, float(amount), margin_capital, leverage, be_flag, tc_en, tc_h,
),
)
return True, None
@@ -5290,6 +5323,7 @@ def check_key_monitors():
key_sig = typ if typ in KEY_MONITOR_AUTO_TYPES else None
be_on = breakeven_enabled_from_row(r, 0)
tc_en, tc_h, _ = time_close_settings_from_row(r)
ok_trade, trade_err, det = _market_open_for_key_monitor(
conn,
sym,
@@ -5299,6 +5333,8 @@ def check_key_monitors():
tp_raw,
key_signal_type=key_sig,
breakeven_enabled=1 if be_on else 0,
time_close_enabled=tc_en,
time_close_hours=tc_h,
)
planned_rr_txt = (
format_wechat_scalar_2dp(planned_rr) if planned_rr is not None else "-"
@@ -5487,12 +5523,14 @@ def check_order_monitors():
send_wechat_msg(be_msg)
res = None
if should_trigger_time_close(r):
res = TIME_CLOSE_RESULT
# 做多
if direction == "long":
if not res and direction == "long":
if p >= take_profit: res = "止盈"
elif p <= stop_loss: res = "止损"
# 做空
elif direction == "short":
elif not res and direction == "short":
if p <= take_profit: res = "止盈"
elif p >= stop_loss: res = "止损"
@@ -6414,7 +6452,8 @@ def api_price_snapshot():
"SELECT id,symbol,monitor_type,direction,upper,lower,fib_entry_price,fib_limit_order_id,created_at FROM key_monitors"
).fetchall()
order_rows = conn.execute(
"SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'"
"SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,"
"time_close_enabled,time_close_hours,time_close_at_ms,opened_at_ms FROM order_monitors WHERE status='active'"
).fetchall()
try:
@@ -6623,6 +6662,7 @@ def api_price_snapshot():
format_price_fn=format_price_for_symbol,
symbol=r["symbol"],
)
apply_time_close_to_payload(payload, r)
new_sl, new_tp, changed = order_monitor_tpsl_needs_sync(
r["stop_loss"], r["take_profit"], exchange_tpsl
)
@@ -7120,6 +7160,10 @@ def add_key():
except Exception:
pass
be_flag = parse_breakeven_enabled_form(d.get("breakeven_enabled"))
tc_en = parse_time_close_enabled_form(d.get("time_close_enabled"))
tc_h = parse_time_close_hours_form(d.get("time_close_hours")) if tc_en else None
if tc_en and not tc_h:
tc_en = 0
if is_false_breakout_key_monitor_type(mt):
fb_sym = normalize_false_breakout_symbol(symbol)
if not fb_sym:
@@ -7154,6 +7198,7 @@ def add_key():
return redirect("/key_monitor")
ok_fb, err_fb = _add_false_breakout_key_monitor(
conn, symbol, direction_sel, upper_px, lower_px, key_px, breakeven_enabled=be_flag,
time_close_enabled=tc_en, time_close_hours=tc_h,
)
conn.commit()
conn.close()
@@ -7164,6 +7209,7 @@ def add_key():
flash(
f"假突破监控已添加,限价单已挂出({symbol}"
f"|有效期 {FALSE_BREAKOUT_VALIDITY_HOURS}h|移动保本:{'' if be_flag else ''}"
+ (f"{time_close_label(tc_h)}" if tc_en else "")
)
return redirect("/key_monitor")
try:
@@ -7184,6 +7230,7 @@ def add_key():
if is_fib_key_monitor_type(mt):
ok_fib, err_fib = _add_fib_key_monitor(
conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=be_flag,
time_close_enabled=tc_en, time_close_hours=tc_h,
)
conn.commit()
conn.close()
@@ -7194,6 +7241,7 @@ def add_key():
flash(
f"斐波监控已添加,限价单已挂出({symbol} 日成交量排名 {rank}/{total}"
f"|移动保本:{'' if be_flag else ''}"
+ (f"{time_close_label(tc_h)}" if tc_en else "")
)
return redirect("/key_monitor")
sl_tp_mode = "standard"
@@ -7227,8 +7275,8 @@ def add_key():
conn.execute(
"INSERT INTO key_monitors "
"(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled,"
"max_notify,notify_interval_min) "
"VALUES (?,?,?,?,?,?,?,?,?,?)",
"max_notify,notify_interval_min,time_close_enabled,time_close_hours) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol,
mt,
@@ -7240,14 +7288,17 @@ def add_key():
be_flag,
KEY_ALERT_MAX_TIMES,
KEY_ALERT_INTERVAL_MINUTES,
tc_en,
tc_h,
),
)
else:
conn.execute(
"INSERT INTO key_monitors "
"(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled) "
"VALUES (?,?,?,?,?,?,?,?)",
(symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag),
"(symbol,monitor_type,direction,upper,lower,sl_tp_mode,manual_take_profit,breakeven_enabled,"
"time_close_enabled,time_close_hours) "
"VALUES (?,?,?,?,?,?,?,?,?,?)",
(symbol, mt, direction_sel, upper_px, lower_px, sl_tp_mode, manual_tp, be_flag, tc_en, tc_h),
)
conn.commit()
conn.close()
@@ -7263,6 +7314,8 @@ def add_key():
extra = ""
if mt in KEY_MONITOR_AUTO_TYPES:
extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'' if be_flag else ''}"
if tc_en:
extra += f"{time_close_label(tc_h)}"
if mt in KEY_MONITOR_RS_TYPES:
flash(
f"添加成功({symbol} 日成交量排名 {rank}/{total})|阻力/支撑:双向监控上/下沿,"
@@ -7478,14 +7531,20 @@ def add_order():
breakeven_raw = float(trigger_price) * (1 + breakeven_offset_pct / 100.0)
breakeven_price = round_price_to_exchange(exchange_symbol, breakeven_raw)
breakeven_enabled = 1 if (d.get("breakeven_enabled") or "").strip() in ("1", "true", "on", "yes") else 0
tc_en = parse_time_close_enabled_form(d.get("time_close_enabled"))
tc_h = parse_time_close_hours_form(d.get("time_close_hours")) if tc_en else None
if tc_en and not tc_h:
tc_en = 0
tc_en, tc_h, tc_at = time_close_insert_values(tc_en, tc_h, opened_at_ms)
conn.execute(
"INSERT INTO order_monitors (symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, margin_capital, leverage, trade_style, risk_percent, risk_amount, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"INSERT INTO order_monitors (symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, margin_capital, leverage, trade_style, risk_percent, risk_amount, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, time_close_enabled, time_close_hours, time_close_at_ms) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, exchange_symbol, direction, trigger_price, stop_loss, stop_loss, take_profit,
margin_capital, leverage, trade_style, risk_percent_db, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,
breakeven_enabled,
notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day,
ORDER_MONITOR_TYPE_MANUAL,
tc_en, tc_h, tc_at,
)
)
conn.commit()
+22
View File
@@ -356,6 +356,14 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<label id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
<select name="time_close_hours" id="order-time-close-hours" disabled>
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
@@ -397,6 +405,12 @@
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
{% if not o.time_close_enabled %}style="display:none"{% endif %}
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· 倒计时 <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
@@ -765,6 +779,7 @@
</div>
<script src="/static/instance_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=1"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script>
@@ -1520,6 +1535,7 @@ function syncKeyMonitorFormFields(){
manualTp.required = !!trend;
}
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if(window.TimeCloseUI) TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
if(upperEl){
upperEl.style.display = showFb ? "none" : "";
upperEl.required = !showFb;
@@ -1544,6 +1560,10 @@ if(keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if(window.TimeCloseUI){
TimeCloseUI.bindTimeCloseForm("key-time-close-cb", "key-time-close-hours", "key-time-close-wrap");
TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap");
}
const keyForm = document.getElementById("key-form");
if(keyForm){
@@ -1877,6 +1897,7 @@ function refreshPriceSnapshot(){
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
paintPlanTpslDisplay(o.id, o);
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o);
});
}).catch(()=>{});
}
@@ -2135,6 +2156,7 @@ function refreshPriceSnapshotConditional(){
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
paintPlanTpslDisplay(o.id, o);
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o);
});
}
}).catch(()=>{});