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