"""持仓时间平仓:开仓后按 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