35 lines
1.3 KiB
Python
35 lines
1.3 KiB
Python
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
from threading import Lock
|
|
import logging
|
|
|
|
|
|
@dataclass
|
|
class _Entry:
|
|
last_close: float
|
|
expires_at: datetime
|
|
|
|
|
|
class DipWatch:
|
|
def __init__(self, expire_seconds: float = 300, rebound_threshold: float = 0.61):
|
|
self.expire_seconds, self.rebound_threshold = expire_seconds, rebound_threshold
|
|
self.data: dict[str, _Entry] = {}; self.lock = Lock()
|
|
|
|
def triggered(self, tag: str, code: str, price: float, now: datetime | None = None) -> bool:
|
|
if price <= 0: return False
|
|
now = now or datetime.now()
|
|
with self.lock:
|
|
watch = self.data.get(code)
|
|
if watch is None or now >= watch.expires_at:
|
|
self.data[code] = _Entry(price, now + timedelta(seconds=self.expire_seconds)); return False
|
|
if price < watch.last_close:
|
|
self.data[code] = _Entry(price, now + timedelta(seconds=self.expire_seconds)); return False
|
|
rebound = (price - watch.last_close) / watch.last_close * 100
|
|
if rebound <= 0 or rebound < self.rebound_threshold: return False
|
|
del self.data[code]
|
|
logging.info("[%s-触发] %s 反弹=%.2f%%", tag, code, rebound)
|
|
return True
|
|
|
|
def forget(self, code):
|
|
with self.lock: self.data.pop(code, None)
|