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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user