import logging as log from dataclasses import dataclass from datetime import datetime, timedelta from threading import Lock @dataclass(slots=True) class _Entry: last_close: float expires_at: datetime class DipWatch: """观察价格低点,并在价格达到指定反弹幅度时触发。""" def __init__( self, expire_seconds: float = 300, rebound_threshold: float = 0.61, ) -> None: self.expire_seconds = expire_seconds self.rebound_threshold = rebound_threshold self.data: dict[str, _Entry] = {} self.lock = Lock() def triggered( self, tag: str, code: str, price: float, now: datetime | None = None, ) -> bool: """更新观察价格;达到反弹阈值时返回 ``True``。""" if price <= 0: log.warning("[%s观察] %s 价格无效:%.2f", tag, code, price) return False current = now or datetime.now() with self.lock: watch = self.data.get(code) if watch is None: self._start(code, price, current) log.info( "[%s观察] %s 开始观察,收盘价=%.2f,反弹阈值=%.2f%%", tag, code, price, self.rebound_threshold, ) return False if current >= watch.expires_at: self._start(code, price, current) log.info("[%s观察] %s 观察已过期,重新观察,收盘价=%.2f", tag, code, price) return False if price < watch.last_close: old_price = watch.last_close self._start(code, price, current) log.info( "[%s观察] %s 刷新低点,原收盘价=%.2f,新收盘价=%.2f", tag, code, old_price, price, ) return False rebound = (price - watch.last_close) / watch.last_close * 100 if rebound < self.rebound_threshold: log.debug( "[%s观察] %s 等待反弹,收盘价=%.2f,现价=%.2f,反弹=%.2f%%,阈值=%.2f%%", tag, code, watch.last_close, price, rebound, self.rebound_threshold, ) return False del self.data[code] log.info( "[%s观察] %s 反弹触发,收盘价=%.2f,现价=%.2f,反弹=%.2f%%", tag, code, watch.last_close, price, rebound, ) return True def forget(self, code: str) -> None: """清除指定股票的价格观察状态。""" with self.lock: removed = self.data.pop(code, None) if removed is not None: log.info("[价格观察] %s 已清除观察状态", code) def _start(self, code: str, price: float, now: datetime) -> None: self.data[code] = _Entry( last_close=price, expires_at=now + timedelta(seconds=self.expire_seconds), )