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
+150
View File
@@ -0,0 +1,150 @@
"""持仓时间平仓:开仓后按 1h/2h/4h 定时市价平仓。"""
from __future__ import annotations
import time
from typing import Any, Optional
ALLOWED_TIME_CLOSE_HOURS = (1, 2, 4)
TIME_CLOSE_RESULT = "时间平仓"
def parse_time_close_enabled_form(form_value: Any) -> int:
return 1 if str(form_value or "").strip().lower() in ("1", "true", "on", "yes") else 0
def parse_time_close_hours_form(form_value: Any, *, default: int = 4) -> Optional[int]:
raw = str(form_value or "").strip().lower().rstrip("h")
if not raw:
return None
try:
h = int(float(raw))
except (TypeError, ValueError):
return None
if h in ALLOWED_TIME_CLOSE_HOURS:
return h
return None
def normalize_time_close_hours(value: Any) -> Optional[int]:
try:
h = int(value)
except (TypeError, ValueError):
return None
return h if h in ALLOWED_TIME_CLOSE_HOURS else None
def _row_val(row: Any, key: str, default=None):
if row is None:
return default
try:
if hasattr(row, "keys") and key in row.keys():
return row[key]
except Exception:
pass
if isinstance(row, dict):
return row.get(key, default)
return default
def time_close_settings_from_row(row: Any) -> tuple[int, Optional[int], Optional[int]]:
"""返回 (enabled, hours, close_at_ms)。"""
enabled = int(_row_val(row, "time_close_enabled", 0) or 0) != 0
hours = normalize_time_close_hours(_row_val(row, "time_close_hours"))
close_at = _row_val(row, "time_close_at_ms")
try:
close_at_ms = int(close_at) if close_at not in (None, "") else None
except (TypeError, ValueError):
close_at_ms = None
if enabled and hours and not close_at_ms:
opened_ms = _row_val(row, "opened_at_ms")
try:
opened_ms = int(opened_ms) if opened_ms not in (None, "") else None
except (TypeError, ValueError):
opened_ms = None
close_at_ms = compute_close_at_ms(opened_ms, hours)
return (1 if enabled and hours else 0, hours, close_at_ms)
def compute_close_at_ms(opened_at_ms: Any, hours: Any) -> Optional[int]:
h = normalize_time_close_hours(hours)
try:
opened = int(opened_at_ms)
except (TypeError, ValueError):
return None
if not h or opened <= 0:
return None
return opened + h * 3600 * 1000
def should_trigger_time_close(row: Any, *, now_ms: Optional[int] = None) -> bool:
enabled, hours, close_at_ms = time_close_settings_from_row(row)
if not enabled or not close_at_ms:
return False
now = int(now_ms if now_ms is not None else time.time() * 1000)
return now >= int(close_at_ms)
def time_close_remaining_seconds(close_at_ms: Any, *, now_ms: Optional[int] = None) -> Optional[int]:
try:
close_at = int(close_at_ms)
except (TypeError, ValueError):
return None
now = int(now_ms if now_ms is not None else time.time() * 1000)
return max(0, int((close_at - now) / 1000))
def format_time_close_countdown(seconds: Any) -> str:
try:
sec = max(0, int(seconds))
except (TypeError, ValueError):
return "--:--:--"
h = sec // 3600
m = (sec % 3600) // 60
s = sec % 60
return f"{h:02d}:{m:02d}:{s:02d}"
def time_close_label(hours: Any) -> str:
h = normalize_time_close_hours(hours)
return f"时间平仓 {h}h" if h else "时间平仓"
def apply_time_close_to_payload(payload: dict[str, Any], row: Any, *, now_ms: Optional[int] = None) -> None:
enabled, hours, close_at_ms = time_close_settings_from_row(row)
payload["time_close_enabled"] = bool(enabled)
payload["time_close_hours"] = hours
payload["time_close_at_ms"] = close_at_ms
payload["time_close_label"] = time_close_label(hours) if enabled else ""
if enabled and close_at_ms:
rem = time_close_remaining_seconds(close_at_ms, now_ms=now_ms)
payload["time_close_remaining_sec"] = rem
payload["time_close_countdown"] = format_time_close_countdown(rem)
else:
payload["time_close_remaining_sec"] = None
payload["time_close_countdown"] = ""
def ensure_time_close_schema(cursor) -> None:
ddl_list = (
"ALTER TABLE order_monitors ADD COLUMN time_close_enabled INTEGER DEFAULT 0",
"ALTER TABLE order_monitors ADD COLUMN time_close_hours INTEGER",
"ALTER TABLE order_monitors ADD COLUMN time_close_at_ms INTEGER",
"ALTER TABLE key_monitors ADD COLUMN time_close_enabled INTEGER DEFAULT 0",
"ALTER TABLE key_monitors ADD COLUMN time_close_hours INTEGER",
)
for ddl in ddl_list:
try:
cursor.execute(ddl)
except Exception:
pass
def time_close_insert_values(
enabled: int,
hours: Optional[int],
opened_at_ms: Optional[int],
) -> tuple[int, Optional[int], Optional[int]]:
en = 1 if int(enabled or 0) != 0 and hours else 0
h = normalize_time_close_hours(hours) if en else None
close_at = compute_close_at_ms(opened_at_ms, h) if en else None
return en, h, close_at