Shorten force-close window to 5m and grey-out open during blocks.
Unify Gate/OKX/Binance: disable the open button with a side note during force-close, cooloff, and daily freeze, and enforce the same gate server-side. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -190,6 +190,8 @@ AUTO_TRANSFER_BJ_HOUR=8
|
||||
FORCE_CLOSE_BJ_HOUR=0
|
||||
# 是否启用强制清仓(默认关闭,true 才会在整点执行)
|
||||
FORCE_CLOSE_ENABLED=false
|
||||
# 强制清仓执行窗口(分钟,默认 5;该窗口内禁止开仓)
|
||||
FORCE_CLOSE_GRACE_MINUTES=5
|
||||
|
||||
# 推送与AI超时(秒)
|
||||
WECHAT_TIMEOUT_SECONDS=10
|
||||
|
||||
@@ -3307,6 +3307,7 @@ def resolve_capital_base_for_key_open(conn, trading_day, live_capital):
|
||||
def precheck_risk(conn, symbol, direction):
|
||||
now = app_now()
|
||||
from lib.trade.account_risk_lib import account_risk_blocks_trading
|
||||
from lib.trade.force_close_lib import force_close_blocks_new_open
|
||||
|
||||
ok_risk, risk_reason = account_risk_blocks_trading(
|
||||
conn,
|
||||
@@ -3316,6 +3317,13 @@ def precheck_risk(conn, symbol, direction):
|
||||
)
|
||||
if not ok_risk:
|
||||
return False, risk_reason
|
||||
fc_block, fc_note = force_close_blocks_new_open(
|
||||
FORCE_CLOSE_ENABLED,
|
||||
FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
)
|
||||
if fc_block:
|
||||
return False, fc_note or "强制清仓窗口内暂不可开仓"
|
||||
if not trading_day_reset_allows_new_open(now):
|
||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||
from lib.trade.account_risk_lib import position_limit_reached
|
||||
@@ -6930,8 +6938,10 @@ def force_close_before_reset():
|
||||
if not FORCE_CLOSE_ENABLED:
|
||||
return
|
||||
now = app_now()
|
||||
# 每天北京时间指定整点小时内执行一次性兜底清仓(默认 00:xx)
|
||||
if now.hour != FORCE_CLOSE_BJ_HOUR:
|
||||
# 每天北京时间指定整点起 FORCE_CLOSE_GRACE_MINUTES 分钟内执行兜底清仓
|
||||
from lib.trade.force_close_lib import is_force_close_executing
|
||||
|
||||
if not is_force_close_executing(FORCE_CLOSE_BJ_HOUR, now_ms=int(now.timestamp() * 1000)):
|
||||
return
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
|
||||
@@ -7303,14 +7313,22 @@ def render_main_page(page="trade", embed_mode=None):
|
||||
position_limit_count = count_position_limit_active_monitors(conn)
|
||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||
risk_status = hub_account_risk_status(conn)
|
||||
can_trade = can_trade_new_open(
|
||||
from lib.trade.open_trade_gate_lib import resolve_manual_open_gate
|
||||
|
||||
_open_gate = resolve_manual_open_gate(
|
||||
time_allows=trading_day_reset_allows_new_open(now),
|
||||
active_count=position_limit_count,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
opens_today=opens_today,
|
||||
hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
extra_blocks=not risk_status.get("can_trade", True),
|
||||
risk_status=risk_status,
|
||||
force_close_enabled=FORCE_CLOSE_ENABLED,
|
||||
force_close_bj_hour=FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
reset_hour=TRADING_DAY_RESET_HOUR,
|
||||
)
|
||||
can_trade = _open_gate["can_trade"]
|
||||
open_block_note = _open_gate["open_block_note"]
|
||||
key_rule_ctx = key_monitor_rule_template_context(
|
||||
kline_timeframe=KLINE_TIMEFRAME,
|
||||
key_breakout_amp_min_pct=KEY_BREAKOUT_AMP_MIN_PCT,
|
||||
@@ -7377,6 +7395,7 @@ def render_main_page(page="trade", embed_mode=None):
|
||||
price_refresh_seconds=PRICE_REFRESH_SECONDS,
|
||||
active_count=position_limit_count,
|
||||
can_trade=can_trade,
|
||||
open_block_note=open_block_note,
|
||||
opens_today=opens_today,
|
||||
daily_open_hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
daily_open_alert_threshold=DAILY_OPEN_ALERT_THRESHOLD,
|
||||
@@ -7564,14 +7583,22 @@ def api_account_snapshot():
|
||||
|
||||
header_trade_stats = header_trade_stats_for_window(conn, _list_window_from_request(), APP_TZ)
|
||||
conn.close()
|
||||
can_trade = can_trade_new_open(
|
||||
from lib.trade.open_trade_gate_lib import resolve_manual_open_gate
|
||||
|
||||
_open_gate = resolve_manual_open_gate(
|
||||
time_allows=trading_day_reset_allows_new_open(now),
|
||||
active_count=position_limit_count,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
opens_today=opens_today,
|
||||
hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
extra_blocks=not risk_status.get("can_trade", True),
|
||||
risk_status=risk_status,
|
||||
force_close_enabled=FORCE_CLOSE_ENABLED,
|
||||
force_close_bj_hour=FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
reset_hour=TRADING_DAY_RESET_HOUR,
|
||||
)
|
||||
can_trade = _open_gate["can_trade"]
|
||||
open_block_note = _open_gate["open_block_note"]
|
||||
available_trading_usdt = get_available_trading_usdt()
|
||||
|
||||
unrealized_pnl = None
|
||||
@@ -7597,6 +7624,7 @@ def api_account_snapshot():
|
||||
"active_count": position_limit_count,
|
||||
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
||||
"can_trade": can_trade,
|
||||
"open_block_note": open_block_note,
|
||||
"opens_today": opens_today,
|
||||
"daily_open_hard_limit": DAILY_OPEN_HARD_LIMIT,
|
||||
"daily_open_alert_threshold": DAILY_OPEN_ALERT_THRESHOLD,
|
||||
|
||||
@@ -192,6 +192,8 @@ AUTO_TRANSFER_BJ_HOUR=8
|
||||
FORCE_CLOSE_BJ_HOUR=0
|
||||
# 是否启用强制清仓(默认关闭,true 才会在整点执行)
|
||||
FORCE_CLOSE_ENABLED=false
|
||||
# 强制清仓执行窗口(分钟,默认 5;该窗口内禁止开仓)
|
||||
FORCE_CLOSE_GRACE_MINUTES=5
|
||||
|
||||
# 推送与AI超时(秒)
|
||||
WECHAT_TIMEOUT_SECONDS=10
|
||||
|
||||
@@ -2971,6 +2971,7 @@ def resolve_capital_base_for_key_open(conn, trading_day, live_capital):
|
||||
def precheck_risk(conn, symbol, direction):
|
||||
now = app_now()
|
||||
from lib.trade.account_risk_lib import account_risk_blocks_trading
|
||||
from lib.trade.force_close_lib import force_close_blocks_new_open
|
||||
|
||||
ok_risk, risk_reason = account_risk_blocks_trading(
|
||||
conn,
|
||||
@@ -2980,6 +2981,13 @@ def precheck_risk(conn, symbol, direction):
|
||||
)
|
||||
if not ok_risk:
|
||||
return False, risk_reason
|
||||
fc_block, fc_note = force_close_blocks_new_open(
|
||||
FORCE_CLOSE_ENABLED,
|
||||
FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
)
|
||||
if fc_block:
|
||||
return False, fc_note or "强制清仓窗口内暂不可开仓"
|
||||
if not trading_day_reset_allows_new_open(now):
|
||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||
from lib.trade.account_risk_lib import position_limit_reached
|
||||
@@ -6553,8 +6561,10 @@ def force_close_before_reset():
|
||||
if not FORCE_CLOSE_ENABLED:
|
||||
return
|
||||
now = app_now()
|
||||
# 每天北京时间指定整点小时内执行一次性兜底清仓(默认 00:xx)
|
||||
if now.hour != FORCE_CLOSE_BJ_HOUR:
|
||||
# 每天北京时间指定整点起 FORCE_CLOSE_GRACE_MINUTES 分钟内执行兜底清仓
|
||||
from lib.trade.force_close_lib import is_force_close_executing
|
||||
|
||||
if not is_force_close_executing(FORCE_CLOSE_BJ_HOUR, now_ms=int(now.timestamp() * 1000)):
|
||||
return
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
|
||||
@@ -7075,14 +7085,22 @@ def render_main_page(page="trade", embed_mode=None):
|
||||
position_limit_count = count_position_limit_active_monitors(conn)
|
||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||
risk_status = hub_account_risk_status(conn)
|
||||
can_trade = can_trade_new_open(
|
||||
from lib.trade.open_trade_gate_lib import resolve_manual_open_gate
|
||||
|
||||
_open_gate = resolve_manual_open_gate(
|
||||
time_allows=trading_day_reset_allows_new_open(now),
|
||||
active_count=position_limit_count,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
opens_today=opens_today,
|
||||
hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
extra_blocks=not risk_status.get("can_trade", True),
|
||||
risk_status=risk_status,
|
||||
force_close_enabled=FORCE_CLOSE_ENABLED,
|
||||
force_close_bj_hour=FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
reset_hour=TRADING_DAY_RESET_HOUR,
|
||||
)
|
||||
can_trade = _open_gate["can_trade"]
|
||||
open_block_note = _open_gate["open_block_note"]
|
||||
key_rule_ctx = key_monitor_rule_template_context(
|
||||
kline_timeframe=KLINE_TIMEFRAME,
|
||||
key_breakout_amp_min_pct=KEY_BREAKOUT_AMP_MIN_PCT,
|
||||
@@ -7146,6 +7164,7 @@ def render_main_page(page="trade", embed_mode=None):
|
||||
price_refresh_seconds=PRICE_REFRESH_SECONDS,
|
||||
active_count=position_limit_count,
|
||||
can_trade=can_trade,
|
||||
open_block_note=open_block_note,
|
||||
opens_today=opens_today,
|
||||
daily_open_hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
daily_open_alert_threshold=DAILY_OPEN_ALERT_THRESHOLD,
|
||||
@@ -7354,14 +7373,22 @@ def api_account_snapshot():
|
||||
|
||||
header_trade_stats = header_trade_stats_for_window(conn, _list_window_from_request(), APP_TZ)
|
||||
conn.close()
|
||||
can_trade = can_trade_new_open(
|
||||
from lib.trade.open_trade_gate_lib import resolve_manual_open_gate
|
||||
|
||||
_open_gate = resolve_manual_open_gate(
|
||||
time_allows=trading_day_reset_allows_new_open(now),
|
||||
active_count=position_limit_count,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
opens_today=opens_today,
|
||||
hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
extra_blocks=not risk_status.get("can_trade", True),
|
||||
risk_status=risk_status,
|
||||
force_close_enabled=FORCE_CLOSE_ENABLED,
|
||||
force_close_bj_hour=FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
reset_hour=TRADING_DAY_RESET_HOUR,
|
||||
)
|
||||
can_trade = _open_gate["can_trade"]
|
||||
open_block_note = _open_gate["open_block_note"]
|
||||
available_trading_usdt = get_available_trading_usdt()
|
||||
|
||||
unrealized_pnl = None
|
||||
@@ -7390,6 +7417,7 @@ def api_account_snapshot():
|
||||
"active_count": position_limit_count,
|
||||
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
||||
"can_trade": can_trade,
|
||||
"open_block_note": open_block_note,
|
||||
"opens_today": opens_today,
|
||||
"daily_open_hard_limit": DAILY_OPEN_HARD_LIMIT,
|
||||
"daily_open_alert_threshold": DAILY_OPEN_ALERT_THRESHOLD,
|
||||
|
||||
@@ -262,6 +262,8 @@ AUTO_TRANSFER_BJ_HOUR=8
|
||||
FORCE_CLOSE_BJ_HOUR=0
|
||||
# 是否启用强制清仓(默认关闭,true 才会在整点执行)
|
||||
FORCE_CLOSE_ENABLED=false
|
||||
# 强制清仓执行窗口(分钟,默认 5;该窗口内禁止开仓)
|
||||
FORCE_CLOSE_GRACE_MINUTES=5
|
||||
|
||||
# 推送与AI超时(秒)
|
||||
WECHAT_TIMEOUT_SECONDS=10
|
||||
|
||||
@@ -2729,6 +2729,7 @@ def trading_day_reset_allows_new_open(now, conn=None):
|
||||
def precheck_risk(conn, symbol, direction):
|
||||
now = app_now()
|
||||
from lib.trade.account_risk_lib import account_risk_blocks_trading
|
||||
from lib.trade.force_close_lib import force_close_blocks_new_open
|
||||
|
||||
ok_risk, risk_reason = account_risk_blocks_trading(
|
||||
conn,
|
||||
@@ -2738,6 +2739,13 @@ def precheck_risk(conn, symbol, direction):
|
||||
)
|
||||
if not ok_risk:
|
||||
return False, risk_reason
|
||||
fc_block, fc_note = force_close_blocks_new_open(
|
||||
FORCE_CLOSE_ENABLED,
|
||||
FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
)
|
||||
if fc_block:
|
||||
return False, fc_note or "强制清仓窗口内暂不可开仓"
|
||||
if not trading_day_reset_allows_new_open(now):
|
||||
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
|
||||
from lib.trade.account_risk_lib import position_limit_reached
|
||||
@@ -6399,8 +6407,10 @@ def force_close_before_reset():
|
||||
if not FORCE_CLOSE_ENABLED:
|
||||
return
|
||||
now = app_now()
|
||||
# 每天北京时间指定整点小时内执行一次性兜底清仓(默认 00:xx)
|
||||
if now.hour != FORCE_CLOSE_BJ_HOUR:
|
||||
# 每天北京时间指定整点起 FORCE_CLOSE_GRACE_MINUTES 分钟内执行兜底清仓
|
||||
from lib.trade.force_close_lib import is_force_close_executing
|
||||
|
||||
if not is_force_close_executing(FORCE_CLOSE_BJ_HOUR, now_ms=int(now.timestamp() * 1000)):
|
||||
return
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall()
|
||||
@@ -6700,14 +6710,22 @@ def render_main_page(page="trade", embed_mode=None):
|
||||
open_guard_blocks_now = open_guard_enabled and now.hour < TRADING_DAY_RESET_HOUR
|
||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||
risk_status = hub_account_risk_status(conn)
|
||||
can_trade = can_trade_new_open(
|
||||
from lib.trade.open_trade_gate_lib import resolve_manual_open_gate
|
||||
|
||||
_open_gate = resolve_manual_open_gate(
|
||||
time_allows=trading_day_reset_allows_new_open(now, conn),
|
||||
active_count=position_limit_count,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
opens_today=opens_today,
|
||||
hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
extra_blocks=not risk_status.get("can_trade", True),
|
||||
risk_status=risk_status,
|
||||
force_close_enabled=FORCE_CLOSE_ENABLED,
|
||||
force_close_bj_hour=FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
reset_hour=TRADING_DAY_RESET_HOUR,
|
||||
)
|
||||
can_trade = _open_gate["can_trade"]
|
||||
open_block_note = _open_gate["open_block_note"]
|
||||
key_rule_ctx = {}
|
||||
if page in ("key_monitor", "trade") or page in (
|
||||
"strategy",
|
||||
@@ -6794,6 +6812,7 @@ def render_main_page(page="trade", embed_mode=None):
|
||||
price_refresh_seconds=PRICE_REFRESH_SECONDS,
|
||||
active_count=position_limit_count,
|
||||
can_trade=can_trade,
|
||||
open_block_note=open_block_note,
|
||||
opens_today=opens_today,
|
||||
daily_open_hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
daily_open_alert_threshold=DAILY_OPEN_ALERT_THRESHOLD,
|
||||
@@ -7050,14 +7069,22 @@ def api_account_snapshot():
|
||||
header_trade_stats = header_trade_stats_for_window(conn, _list_window_from_request(), APP_TZ)
|
||||
conn.close()
|
||||
open_guard_blocks_now = open_guard_enabled and now.hour < TRADING_DAY_RESET_HOUR
|
||||
can_trade = can_trade_new_open(
|
||||
from lib.trade.open_trade_gate_lib import resolve_manual_open_gate
|
||||
|
||||
_open_gate = resolve_manual_open_gate(
|
||||
time_allows=trading_day_reset_allows_new_open(now),
|
||||
active_count=position_limit_count,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
opens_today=opens_today,
|
||||
hard_limit=DAILY_OPEN_HARD_LIMIT,
|
||||
extra_blocks=not risk_status.get("can_trade", True),
|
||||
risk_status=risk_status,
|
||||
force_close_enabled=FORCE_CLOSE_ENABLED,
|
||||
force_close_bj_hour=FORCE_CLOSE_BJ_HOUR,
|
||||
now_ms=int(now.timestamp() * 1000),
|
||||
reset_hour=TRADING_DAY_RESET_HOUR,
|
||||
)
|
||||
can_trade = _open_gate["can_trade"]
|
||||
open_block_note = _open_gate["open_block_note"]
|
||||
available_trading_usdt = get_available_trading_usdt()
|
||||
|
||||
unrealized_pnl = None
|
||||
@@ -7120,6 +7147,7 @@ def api_account_snapshot():
|
||||
"active_count": position_limit_count,
|
||||
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
||||
"can_trade": can_trade,
|
||||
"open_block_note": open_block_note,
|
||||
"opens_today": opens_today,
|
||||
"daily_open_hard_limit": DAILY_OPEN_HARD_LIMIT,
|
||||
"daily_open_alert_threshold": DAILY_OPEN_ALERT_THRESHOLD,
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
delete form.dataset.submitGuard;
|
||||
form.classList.remove("is-form-submitting");
|
||||
submitButtons(form).forEach(function (btn) {
|
||||
btn.disabled = false;
|
||||
// 风控灰显(开仓门禁)保持禁用
|
||||
btn.disabled = btn.classList.contains("is-blocked");
|
||||
var orig = btn.dataset.submitGuardOrig;
|
||||
if (orig !== undefined) {
|
||||
if (btn.tagName === "BUTTON") btn.textContent = orig;
|
||||
|
||||
@@ -36,8 +36,21 @@
|
||||
.order-monitor-form .om-check{display:inline-flex;align-items:center;gap:5px;font-size:.82rem;color:#cfd3ef;cursor:pointer;user-select:none}
|
||||
.order-monitor-form .om-time-close{display:inline-flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef}
|
||||
.order-monitor-form .om-time-close select{width:auto;min-width:4.2rem;max-width:5.5rem;padding:6px 8px}
|
||||
.order-monitor-form .om-row-action{padding-top:2px}
|
||||
.order-monitor-form .om-row-action{padding-top:2px;display:flex;flex-wrap:wrap;align-items:center;gap:10px 14px}
|
||||
.order-monitor-form .om-submit{min-width:11rem;padding:10px 18px;font-weight:600}
|
||||
.order-monitor-form .om-submit.is-blocked,
|
||||
.order-monitor-form .om-submit:disabled{
|
||||
opacity:.45;
|
||||
cursor:not-allowed;
|
||||
filter:grayscale(.35);
|
||||
pointer-events:none;
|
||||
}
|
||||
.order-monitor-form .om-open-block-note{
|
||||
color:var(--danger,#ff7b7b);
|
||||
font-size:13px;
|
||||
line-height:1.4;
|
||||
max-width:min(28rem,100%);
|
||||
}
|
||||
.order-plan-preview{display:flex;gap:18px;flex-wrap:wrap;align-items:center;margin:4px 0 10px;padding:10px 12px;background:#151a28;border:1px solid #2a3150;border-radius:8px;font-size:.85rem}
|
||||
#add-order-form #sltp-mode{min-width:12.5rem;max-width:16rem;width:auto}
|
||||
.order-preview-risk{color:#ff6b6b}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 实盘下单监控:开仓按钮灰显 + 旁注(强制清仓/冷静期/日冻结等).
|
||||
*/
|
||||
(function (global) {
|
||||
function apply(data) {
|
||||
const d = data || {};
|
||||
const btn =
|
||||
document.getElementById("om-submit-btn") ||
|
||||
document.querySelector("#add-order-form button.om-submit");
|
||||
const noteEl = document.getElementById("om-open-block-note");
|
||||
if (!btn && !noteEl) return;
|
||||
|
||||
const canTrade = d.can_trade !== false;
|
||||
let note = (d.open_block_note || "").trim();
|
||||
const fc = d.force_close || {};
|
||||
const rs = d.risk_status || {};
|
||||
if (!note && fc.enabled && fc.executing) {
|
||||
const grace = fc.grace_minutes != null ? fc.grace_minutes : 5;
|
||||
note =
|
||||
"强制清仓窗口内(北京时间 " +
|
||||
(fc.hour_label || "--:--") +
|
||||
" 起 " +
|
||||
grace +
|
||||
" 分钟),暂不可开仓";
|
||||
}
|
||||
if (!note && rs.can_trade === false && rs.reason) {
|
||||
note = String(rs.reason);
|
||||
}
|
||||
if (!note && !canTrade) {
|
||||
note = "当前不可开仓";
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = !canTrade;
|
||||
btn.classList.toggle("is-blocked", !canTrade);
|
||||
btn.setAttribute("aria-disabled", canTrade ? "false" : "true");
|
||||
if (!canTrade) {
|
||||
btn.title = note || "当前不可开仓";
|
||||
} else {
|
||||
btn.removeAttribute("title");
|
||||
}
|
||||
}
|
||||
if (noteEl) {
|
||||
if (!canTrade && note) {
|
||||
noteEl.hidden = false;
|
||||
noteEl.textContent = note;
|
||||
} else {
|
||||
noteEl.hidden = true;
|
||||
noteEl.textContent = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.OpenSubmitGate = { apply: apply };
|
||||
})(window);
|
||||
Vendored
+1
@@ -76,6 +76,7 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"TRANSFER_CCY",
|
||||
"FORCE_CLOSE_ENABLED",
|
||||
"FORCE_CLOSE_BJ_HOUR",
|
||||
"FORCE_CLOSE_GRACE_MINUTES",
|
||||
"BTC_LEVERAGE",
|
||||
"ALT_LEVERAGE",
|
||||
"DAILY_START_CAPITAL",
|
||||
|
||||
Vendored
+1
@@ -83,6 +83,7 @@ _SHARED_SECTIONS: list[dict[str, Any]] = [
|
||||
("KEY_AUTO_MIN_PLANNED_RR", "关键位最低盈亏比", "自动单计划 RR 须严格大于该值,默认 1.5"),
|
||||
("FORCE_CLOSE_ENABLED", "强制清仓开关", ""),
|
||||
("FORCE_CLOSE_BJ_HOUR", "强制清仓整点(北京)", ""),
|
||||
("FORCE_CLOSE_GRACE_MINUTES", "强制清仓窗口(分钟)", "默认 5;整点起该分钟内执行并禁止开仓"),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -152,7 +152,10 @@ def build_instance_settings_view(
|
||||
"title": "整点强制清仓",
|
||||
"rows": [
|
||||
_row("强制清仓", _on_off(force_close_on)),
|
||||
_row("执行时刻", f"北京时间 {force_close_hour}:00 起 15 分钟内"),
|
||||
_row(
|
||||
"执行时刻",
|
||||
f"北京时间 {force_close_hour}:00 起 {_env_int('FORCE_CLOSE_GRACE_MINUTES', 5)} 分钟内",
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1230,11 +1230,15 @@ function applyAccountSnapshot(data){
|
||||
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
|
||||
TimeCloseUI.paintForceCloseHeader(data.force_close);
|
||||
}
|
||||
if(window.OpenSubmitGate) OpenSubmitGate.apply(data);
|
||||
let canTradeText = "可开仓";
|
||||
if(!data.can_trade){
|
||||
const parts = [];
|
||||
if(data.open_block_note) parts.push(data.open_block_note);
|
||||
if(data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason){
|
||||
parts.push(data.risk_status.reason);
|
||||
if(!data.open_block_note || data.open_block_note.indexOf(data.risk_status.reason) < 0){
|
||||
parts.push(data.risk_status.reason);
|
||||
}
|
||||
}
|
||||
const ac = Number(data.active_count || 0);
|
||||
const max = Number(data.max_active_positions || {{ max_active_positions }});
|
||||
@@ -1242,9 +1246,8 @@ function applyAccountSnapshot(data){
|
||||
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
const opens = Number(data.opens_today);
|
||||
if(hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
|
||||
if(!parts.length) parts.push(`未到北京时间 {{ reset_hour }}:00`);
|
||||
else parts.push(`或未到北京时间 {{ reset_hour }}:00`);
|
||||
canTradeText = `不可开仓(${parts.join(";")})`;
|
||||
if(data.open_guard_blocks_now) parts.push(`未到北京时间 ${data.reset_hour||{{ reset_hour }}}:00`);
|
||||
canTradeText = parts.length ? `不可开仓(${parts.join(";")})` : "不可开仓";
|
||||
}
|
||||
const opensToday = Number(data.opens_today);
|
||||
const hardLim = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=13">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=111">
|
||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||
<script src="/static/open_submit_gate.js?v=1"></script>
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<title>{{ pwa_app_name }}</title>
|
||||
</head>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||
<script src="/static/open_submit_gate.js?v=1"></script>
|
||||
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<meta name="apple-mobile-web-app-title" content="{{ pwa_app_name }}">
|
||||
@@ -1707,11 +1708,15 @@ function applyAccountSnapshot(data){
|
||||
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
|
||||
TimeCloseUI.paintForceCloseHeader(data.force_close);
|
||||
}
|
||||
if(window.OpenSubmitGate) OpenSubmitGate.apply(data);
|
||||
let canTradeText = "可开仓";
|
||||
if(!data.can_trade){
|
||||
const parts = [];
|
||||
if(data.open_block_note) parts.push(data.open_block_note);
|
||||
if(data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason){
|
||||
parts.push(data.risk_status.reason);
|
||||
if(!data.open_block_note || data.open_block_note.indexOf(data.risk_status.reason) < 0){
|
||||
parts.push(data.risk_status.reason);
|
||||
}
|
||||
}
|
||||
if((data.active_count||0) >= (data.max_active_positions||{{ max_active_positions }})) parts.push(`持仓 ${data.active_count}/${data.max_active_positions}`);
|
||||
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
</div>
|
||||
|
||||
<div class="om-row om-row-action">
|
||||
<button type="submit" class="om-submit">{{ open_position_button_label }}</button>
|
||||
<button type="submit" class="om-submit{% if not can_trade %} is-blocked{% endif %}" id="om-submit-btn"{% if not can_trade %} disabled aria-disabled="true"{% endif %}>{{ open_position_button_label }}</button>
|
||||
<span id="om-open-block-note" class="om-open-block-note"{% if can_trade or not (open_block_note|default('')) %} hidden{% endif %}>{% if not can_trade and (open_block_note|default('')) %}{{ open_block_note }}{% endif %}</span>
|
||||
</div>
|
||||
</form>
|
||||
{% include 'order_plan_preview_bar.html' %}
|
||||
|
||||
@@ -7,6 +7,23 @@
|
||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
||||
</div>
|
||||
</details>
|
||||
<details class="tip-collapse order-exit-collapse">
|
||||
<summary class="tip-collapse-summary">平仓 / 委托 / 强制清仓</summary>
|
||||
<div class="tip-collapse-body rule-tip">
|
||||
<ul style="margin:0;padding-left:1.1em;line-height:1.55">
|
||||
<li><strong>止盈止损委托</strong>:开仓后挂交易所条件止盈/止损;持仓卡可查看状态,「委托」可重挂,「撤单」只撤对应条件单、不平仓。</li>
|
||||
<li><strong>手动平仓</strong>:持仓卡「平仓」会市价平仓并撤销该合约条件单,交易记录记为「手动平仓」。</li>
|
||||
<li><strong>强制清仓</strong>:
|
||||
{% if force_close is defined and force_close.enabled %}
|
||||
已开启 · 北京时间 <strong>{{ force_close.hour_label }}</strong> 整点起 <strong>{{ force_close.grace_minutes|default(5) }}</strong> 分钟内,本地状态为 active 的下单监控会被市价清仓,结果记「强制清仓」;该窗口内开仓按钮灰显不可点。
|
||||
{% else %}
|
||||
当前<strong>已关闭</strong>(<code>FORCE_CLOSE_ENABLED=false</code>);开启后按 <code>FORCE_CLOSE_BJ_HOUR</code> 在北京时间该整点起 <code>FORCE_CLOSE_GRACE_MINUTES</code>(默认 5)分钟内清掉 active 监控仓。
|
||||
{% endif %}
|
||||
冷静期/日冻结期间开仓按钮同样灰显不可点。仅影响本系统监控中的仓。
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
<details class="tip-collapse order-sizing-collapse">
|
||||
<summary class="tip-collapse-summary">计仓与保本说明</summary>
|
||||
<div class="tip-collapse-body rule-tip">
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
<li><strong>手动平仓</strong>:持仓卡「平仓」会市价平仓并撤销该合约条件单,交易记录记为「手动平仓」。</li>
|
||||
<li><strong>强制清仓</strong>:
|
||||
{% if force_close is defined and force_close.enabled %}
|
||||
已开启 · 北京时间 <strong>{{ force_close.hour_label }}</strong> 整点起该小时内,本地状态为 active 的下单监控会被市价清仓,结果记「强制清仓」。
|
||||
已开启 · 北京时间 <strong>{{ force_close.hour_label }}</strong> 整点起 <strong>{{ force_close.grace_minutes|default(5) }}</strong> 分钟内,本地状态为 active 的下单监控会被市价清仓,结果记「强制清仓」;该窗口内开仓按钮灰显不可点。
|
||||
{% else %}
|
||||
当前<strong>已关闭</strong>(<code>FORCE_CLOSE_ENABLED=false</code>);开启后按 <code>FORCE_CLOSE_BJ_HOUR</code> 在北京时间该整点小时内清掉 active 监控仓。
|
||||
当前<strong>已关闭</strong>(<code>FORCE_CLOSE_ENABLED=false</code>);开启后按 <code>FORCE_CLOSE_BJ_HOUR</code> 在北京时间该整点起 <code>FORCE_CLOSE_GRACE_MINUTES</code>(默认 5)分钟内清掉 active 监控仓。
|
||||
{% endif %}
|
||||
仅影响本系统监控中的仓;交易所裸仓且无本地监控时不会被此逻辑平掉。
|
||||
冷静期/日冻结期间开仓按钮同样灰显不可点。仅影响本系统监控中的仓;交易所裸仓且无本地监控时不会被此逻辑平掉。
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,23 @@
|
||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
||||
</div>
|
||||
</details>
|
||||
<details class="tip-collapse order-exit-collapse">
|
||||
<summary class="tip-collapse-summary">平仓 / 委托 / 强制清仓</summary>
|
||||
<div class="tip-collapse-body rule-tip">
|
||||
<ul style="margin:0;padding-left:1.1em;line-height:1.55">
|
||||
<li><strong>止盈止损委托</strong>:开仓后挂交易所条件止盈/止损;持仓卡可查看状态,「委托」可重挂,「撤单」只撤对应条件单、不平仓。</li>
|
||||
<li><strong>手动平仓</strong>:持仓卡「平仓」会市价平仓并撤销该合约条件单,交易记录记为「手动平仓」。</li>
|
||||
<li><strong>强制清仓</strong>:
|
||||
{% if force_close is defined and force_close.enabled %}
|
||||
已开启 · 北京时间 <strong>{{ force_close.hour_label }}</strong> 整点起 <strong>{{ force_close.grace_minutes|default(5) }}</strong> 分钟内,本地状态为 active 的下单监控会被市价清仓,结果记「强制清仓」;该窗口内开仓按钮灰显不可点。
|
||||
{% else %}
|
||||
当前<strong>已关闭</strong>(<code>FORCE_CLOSE_ENABLED=false</code>);开启后按 <code>FORCE_CLOSE_BJ_HOUR</code> 在北京时间该整点起 <code>FORCE_CLOSE_GRACE_MINUTES</code>(默认 5)分钟内清掉 active 监控仓。
|
||||
{% endif %}
|
||||
冷静期/日冻结期间开仓按钮同样灰显不可点。仅影响本系统监控中的仓。
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
<details class="tip-collapse order-sizing-collapse">
|
||||
<summary class="tip-collapse-summary">计仓与保本说明</summary>
|
||||
<div class="tip-collapse-body rule-tip">
|
||||
|
||||
@@ -8,7 +8,22 @@ from typing import Any, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
FORCE_CLOSE_RESULT = "强制清仓"
|
||||
FORCE_CLOSE_GRACE_MINUTES = 15
|
||||
# 默认宽限(分钟);运行时优先读 FORCE_CLOSE_GRACE_MINUTES
|
||||
FORCE_CLOSE_GRACE_MINUTES = 5
|
||||
|
||||
|
||||
def force_close_grace_minutes(override: Any = None) -> int:
|
||||
"""整点强制清仓执行窗口长度(分钟)."""
|
||||
if override is not None and str(override).strip() != "":
|
||||
raw = override
|
||||
else:
|
||||
raw = os.getenv("FORCE_CLOSE_GRACE_MINUTES")
|
||||
if raw is None or str(raw).strip() == "":
|
||||
raw = FORCE_CLOSE_GRACE_MINUTES
|
||||
try:
|
||||
return max(1, int(raw))
|
||||
except (TypeError, ValueError):
|
||||
return max(1, int(FORCE_CLOSE_GRACE_MINUTES))
|
||||
|
||||
|
||||
def app_timezone_name() -> str:
|
||||
@@ -43,7 +58,7 @@ def is_force_close_active_hour(
|
||||
*,
|
||||
now_ms: Optional[int] = None,
|
||||
tz_name: Optional[str] = None,
|
||||
grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES,
|
||||
grace_minutes: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""当前是否处于整点强制清仓执行窗口(整点起 grace 分钟内)."""
|
||||
return is_force_close_executing(
|
||||
@@ -59,17 +74,41 @@ def is_force_close_executing(
|
||||
*,
|
||||
now_ms: Optional[int] = None,
|
||||
tz_name: Optional[str] = None,
|
||||
grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES,
|
||||
grace_minutes: Optional[int] = None,
|
||||
) -> bool:
|
||||
hour = normalize_force_close_bj_hour(bj_hour)
|
||||
now = _now_dt(now_ms=now_ms, tz_name=tz_name)
|
||||
target = now.replace(hour=hour, minute=0, second=0, microsecond=0)
|
||||
if now < target:
|
||||
return False
|
||||
end = target + timedelta(minutes=max(1, int(grace_minutes)))
|
||||
grace = force_close_grace_minutes(grace_minutes)
|
||||
end = target + timedelta(minutes=grace)
|
||||
return now < end
|
||||
|
||||
|
||||
def force_close_blocks_new_open(
|
||||
enabled: bool,
|
||||
bj_hour: Any,
|
||||
*,
|
||||
now_ms: Optional[int] = None,
|
||||
tz_name: Optional[str] = None,
|
||||
grace_minutes: Optional[int] = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""强制清仓执行窗口内禁止新开仓.返回 (是否拦截, 说明文案)."""
|
||||
if not enabled:
|
||||
return False, ""
|
||||
grace = force_close_grace_minutes(grace_minutes)
|
||||
if not is_force_close_executing(
|
||||
bj_hour, now_ms=now_ms, tz_name=tz_name, grace_minutes=grace
|
||||
):
|
||||
return False, ""
|
||||
label = force_close_hour_label(bj_hour)
|
||||
return (
|
||||
True,
|
||||
f"强制清仓窗口内(北京时间 {label} 起 {grace} 分钟),暂不可开仓",
|
||||
)
|
||||
|
||||
|
||||
def parse_closed_at_dt(
|
||||
closed_at: Any,
|
||||
*,
|
||||
@@ -93,7 +132,7 @@ def is_close_at_force_close_window(
|
||||
closed_at: Any,
|
||||
bj_hour: Any,
|
||||
*,
|
||||
grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES,
|
||||
grace_minutes: Optional[int] = None,
|
||||
tz_name: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""平仓时刻是否落在北京时间整点强制清仓窗口内."""
|
||||
@@ -103,7 +142,7 @@ def is_close_at_force_close_window(
|
||||
hour = normalize_force_close_bj_hour(bj_hour)
|
||||
if dt.hour != hour:
|
||||
return False
|
||||
return dt.minute < max(1, int(grace_minutes))
|
||||
return dt.minute < force_close_grace_minutes(grace_minutes)
|
||||
|
||||
|
||||
def infer_force_close_result(
|
||||
@@ -111,7 +150,7 @@ def infer_force_close_result(
|
||||
*,
|
||||
enabled: bool,
|
||||
bj_hour: Any,
|
||||
grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES,
|
||||
grace_minutes: Optional[int] = None,
|
||||
tz_name: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
if not enabled:
|
||||
@@ -130,7 +169,7 @@ def coerce_force_close_result(
|
||||
enabled: bool,
|
||||
bj_hour: Any,
|
||||
miss_reason: Optional[str] = None,
|
||||
grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES,
|
||||
grace_minutes: Optional[int] = None,
|
||||
tz_name: Optional[str] = None,
|
||||
) -> tuple[str, str]:
|
||||
"""同步平仓归类:整点窗口内优先记为强制清仓."""
|
||||
@@ -158,7 +197,7 @@ def apply_force_close_display_result(
|
||||
*,
|
||||
enabled: bool,
|
||||
bj_hour: Any,
|
||||
grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES,
|
||||
grace_minutes: Optional[int] = None,
|
||||
tz_name: Optional[str] = None,
|
||||
) -> str:
|
||||
"""展示层:外部平仓/手动平仓若落在整点窗口,显示为强制清仓."""
|
||||
@@ -227,19 +266,24 @@ def build_force_close_state(
|
||||
has_active_positions: Optional[bool] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""实例级强制清仓状态(模板 / API 共用)."""
|
||||
grace = force_close_grace_minutes()
|
||||
if not enabled:
|
||||
return {
|
||||
"enabled": False,
|
||||
"bj_hour": normalize_force_close_bj_hour(bj_hour),
|
||||
"hour_label": force_close_hour_label(bj_hour),
|
||||
"label": force_close_label(bj_hour),
|
||||
"grace_minutes": grace,
|
||||
"next_at_ms": None,
|
||||
"remaining_sec": None,
|
||||
"countdown": "",
|
||||
"active": False,
|
||||
"executing": False,
|
||||
}
|
||||
hour = normalize_force_close_bj_hour(bj_hour)
|
||||
executing = is_force_close_executing(hour, now_ms=now_ms, tz_name=tz_name)
|
||||
executing = is_force_close_executing(
|
||||
hour, now_ms=now_ms, tz_name=tz_name, grace_minutes=grace
|
||||
)
|
||||
active = executing and (has_active_positions is not False)
|
||||
next_at_ms = compute_next_force_close_at_ms(bj_hour=hour, now_ms=now_ms, tz_name=tz_name)
|
||||
rem = force_close_remaining_seconds(next_at_ms, now_ms=now_ms) if next_at_ms else None
|
||||
@@ -248,10 +292,12 @@ def build_force_close_state(
|
||||
"bj_hour": hour,
|
||||
"hour_label": force_close_hour_label(hour),
|
||||
"label": force_close_label(hour),
|
||||
"grace_minutes": grace,
|
||||
"next_at_ms": next_at_ms,
|
||||
"remaining_sec": rem,
|
||||
"countdown": format_force_close_countdown(rem, active=active),
|
||||
"active": active,
|
||||
"executing": executing,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""三所开仓门禁:账户风控 + 强制清仓窗口 + 仓位/日开仓上限."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from lib.trade.daily_open_limit_lib import can_trade_new_open
|
||||
from lib.trade.force_close_lib import force_close_blocks_new_open
|
||||
|
||||
|
||||
def resolve_manual_open_gate(
|
||||
*,
|
||||
time_allows: bool,
|
||||
active_count: int,
|
||||
max_active_positions: int,
|
||||
opens_today: int,
|
||||
hard_limit: int,
|
||||
risk_status: Optional[dict[str, Any]],
|
||||
force_close_enabled: bool,
|
||||
force_close_bj_hour: Any,
|
||||
now_ms: Optional[int] = None,
|
||||
reset_hour: int = 8,
|
||||
) -> dict[str, Any]:
|
||||
"""汇总是否可开仓及按钮旁说明文案."""
|
||||
rs = risk_status if isinstance(risk_status, dict) else {}
|
||||
risk_can = bool(rs.get("can_trade", True))
|
||||
fc_block, fc_note = force_close_blocks_new_open(
|
||||
bool(force_close_enabled),
|
||||
force_close_bj_hour,
|
||||
now_ms=now_ms,
|
||||
)
|
||||
can_trade = can_trade_new_open(
|
||||
time_allows=time_allows,
|
||||
active_count=active_count,
|
||||
max_active_positions=max_active_positions,
|
||||
opens_today=opens_today,
|
||||
hard_limit=hard_limit,
|
||||
extra_blocks=(not risk_can) or fc_block,
|
||||
)
|
||||
note = ""
|
||||
if fc_block and fc_note:
|
||||
note = fc_note
|
||||
elif not risk_can:
|
||||
note = str(rs.get("reason") or "账户冷静期/日冻结中,暂不可开仓")
|
||||
elif not time_allows:
|
||||
note = f"未到北京时间 {int(reset_hour)}:00,暂不可开仓"
|
||||
elif int(active_count) >= int(max_active_positions):
|
||||
note = f"已达最大持仓数({int(active_count)}/{int(max_active_positions)}),暂不可开仓"
|
||||
elif int(hard_limit) > 0 and int(opens_today) >= int(hard_limit):
|
||||
note = (
|
||||
f"本交易日开仓已达上限({int(opens_today)}/{int(hard_limit)}),"
|
||||
f"次日北京时间 {int(reset_hour)}:00 后恢复"
|
||||
)
|
||||
return {
|
||||
"can_trade": can_trade,
|
||||
"open_block_note": note if not can_trade else "",
|
||||
"force_close_blocks": fc_block,
|
||||
}
|
||||
@@ -49,7 +49,8 @@ def test_next_force_close_after_trigger_same_day():
|
||||
|
||||
|
||||
def test_executing_window_and_countdown():
|
||||
now = _ms(2026, 7, 6, 0, 14)
|
||||
# 默认宽限 5 分钟:00:04 仍在执行窗
|
||||
now = _ms(2026, 7, 6, 0, 4)
|
||||
assert is_force_close_executing(0, now_ms=now, tz_name="Asia/Shanghai")
|
||||
assert is_force_close_active_hour(0, now_ms=now, tz_name="Asia/Shanghai")
|
||||
state = build_force_close_state(
|
||||
@@ -57,17 +58,21 @@ def test_executing_window_and_countdown():
|
||||
)
|
||||
assert state["enabled"] is True
|
||||
assert state["active"] is True
|
||||
assert state["executing"] is True
|
||||
assert state["grace_minutes"] == 5
|
||||
assert state["countdown"] == "执行中"
|
||||
assert state["next_at_ms"] == _ms(2026, 7, 7, 0, 0)
|
||||
|
||||
|
||||
def test_not_executing_after_grace_without_positions():
|
||||
now = _ms(2026, 7, 7, 0, 18)
|
||||
# 默认宽限 5 分钟:00:05 起已离开执行窗
|
||||
now = _ms(2026, 7, 7, 0, 5)
|
||||
assert not is_force_close_executing(0, now_ms=now, tz_name="Asia/Shanghai")
|
||||
state = build_force_close_state(
|
||||
True, 0, now_ms=now, tz_name="Asia/Shanghai", has_active_positions=False
|
||||
)
|
||||
assert state["active"] is False
|
||||
assert state["executing"] is False
|
||||
assert state["countdown"] != "执行中"
|
||||
assert state["next_at_ms"] == _ms(2026, 7, 8, 0, 0)
|
||||
|
||||
@@ -85,6 +90,8 @@ def test_format_countdown():
|
||||
|
||||
def test_infer_force_close_from_closed_at():
|
||||
assert is_close_at_force_close_window("2026-07-07 00:00", 0)
|
||||
assert is_close_at_force_close_window("2026-07-07 00:04", 0)
|
||||
assert not is_close_at_force_close_window("2026-07-07 00:05", 0)
|
||||
assert infer_force_close_result("2026-07-07 00:00", enabled=True, bj_hour=0) == "强制清仓"
|
||||
assert infer_force_close_result("2026-07-07 00:20", enabled=True, bj_hour=0) is None
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from lib.trade.open_trade_gate_lib import resolve_manual_open_gate
|
||||
|
||||
|
||||
def _base(**kwargs):
|
||||
cfg = dict(
|
||||
time_allows=True,
|
||||
active_count=0,
|
||||
max_active_positions=1,
|
||||
opens_today=0,
|
||||
hard_limit=10,
|
||||
risk_status={"can_trade": True, "reason": ""},
|
||||
force_close_enabled=False,
|
||||
force_close_bj_hour=0,
|
||||
now_ms=None,
|
||||
reset_hour=8,
|
||||
)
|
||||
cfg.update(kwargs)
|
||||
return resolve_manual_open_gate(**cfg)
|
||||
|
||||
|
||||
def test_open_when_clear():
|
||||
g = _base()
|
||||
assert g["can_trade"] is True
|
||||
assert g["open_block_note"] == ""
|
||||
|
||||
|
||||
def test_block_cooloff_note():
|
||||
g = _base(risk_status={"can_trade": False, "reason": "账户冷静期中"})
|
||||
assert g["can_trade"] is False
|
||||
assert "冷静期" in g["open_block_note"]
|
||||
|
||||
|
||||
def test_block_force_close_window():
|
||||
# 2026-07-07 00:02 Asia/Shanghai
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
now_ms = int(datetime(2026, 7, 7, 0, 2, tzinfo=ZoneInfo("Asia/Shanghai")).timestamp() * 1000)
|
||||
g = _base(force_close_enabled=True, force_close_bj_hour=0, now_ms=now_ms)
|
||||
assert g["can_trade"] is False
|
||||
assert g["force_close_blocks"] is True
|
||||
assert "强制清仓" in g["open_block_note"]
|
||||
assert "5 分钟" in g["open_block_note"]
|
||||
Reference in New Issue
Block a user