106 lines
3.3 KiB
Python
106 lines
3.3 KiB
Python
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),
|
|
)
|