edc50e8809
- 数据层: build_aggregates_sql 新增 2h/6h 聚合视图,默认起始时间调整为 2017-05 - 模型层: KlineInterval 类型扩展 2h/6h,DataService 新增对应表名和毫秒映射 - 指标层: 新增 incremental.py 增量指标模块 (EmaInc/AtrInc/RsiInc/BbInc),O(1) per bar - 策略重构: long_short.py 和 regime_all.py 从批量 ema/atr 迁移至增量指标,避免每 bar 重复全量计算 - regime 探测器: RegimeDetector3 改为增量 EMA200,detect() 接口简化 - 回测扩展: regime_timeframe_comparison 从 4h/1d 扩展至 2h/4h/6h/1d - 新增示例: multi_strategy_report, vol_break_compare/periods, intraday_explore, top3_trades 等分析脚本
245 lines
7.2 KiB
Python
245 lines
7.2 KiB
Python
"""
|
||
增量指标 — O(1) 每 bar 更新,避免每次从头重算整条序列
|
||
|
||
策略在 on_kline 中对每根 bar 调用 update(),内部只计算增量值,
|
||
对外暴露 values 属性(完整序列,支持索引回溯),兼顾性能与易用性。
|
||
|
||
用法:
|
||
from engine.indicators.incremental import EmaInc, AtrInc
|
||
|
||
e200 = EmaInc(200)
|
||
for price in prices:
|
||
e200.update(price)
|
||
print(e200[-1]) # 最新 EMA 值
|
||
print(e200[-20]) # 20 根前的 EMA 值(斜率计算用)
|
||
"""
|
||
|
||
from typing import Optional
|
||
|
||
|
||
class EmaInc:
|
||
"""增量 EMA
|
||
|
||
内部维护完整序列,update() 为 O(1),values 为 list[float] 可直接索引。
|
||
"""
|
||
|
||
def __init__(self, period: int):
|
||
self.period = period
|
||
self.k = 2.0 / (period + 1)
|
||
self._values: list[float] = []
|
||
self._warm: list[float] = []
|
||
self._ready = False
|
||
|
||
def update(self, price: float) -> float:
|
||
"""输入新价格,返回最新 EMA 值(不足周期时返回 0)"""
|
||
if not self._ready:
|
||
self._warm.append(price)
|
||
self._values.append(0.0)
|
||
if len(self._warm) == self.period:
|
||
self._values[-1] = sum(self._warm) / self.period
|
||
self._warm.clear()
|
||
self._ready = True
|
||
return self._values[-1]
|
||
return 0.0
|
||
val = price * self.k + self._values[-1] * (1 - self.k)
|
||
self._values.append(val)
|
||
return val
|
||
|
||
@property
|
||
def values(self) -> list[float]:
|
||
return self._values
|
||
|
||
@property
|
||
def current(self) -> float:
|
||
return self._values[-1] if self._values else 0.0
|
||
|
||
def __getitem__(self, idx: int) -> float:
|
||
return self._values[idx]
|
||
|
||
def __len__(self) -> int:
|
||
return len(self._values)
|
||
|
||
|
||
class AtrInc:
|
||
"""增量 ATR(Wilder 平滑)
|
||
|
||
内部维护完整序列,update() 为 O(1),values 为 list[float] 可直接索引。
|
||
"""
|
||
|
||
def __init__(self, period: int = 14):
|
||
self.period = period
|
||
self._values: list[float] = []
|
||
self._tr_buffer: list[float] = []
|
||
self._prev_close: Optional[float] = None
|
||
self._ready = False
|
||
|
||
def update(self, high: float, low: float, close: float) -> float:
|
||
"""输入新 bar 的 HLC,返回最新 ATR 值(不足周期时返回 0)"""
|
||
# 第一根 bar:记录收盘价,无法计算 TR
|
||
if self._prev_close is None:
|
||
self._prev_close = close
|
||
self._values.append(0.0)
|
||
return 0.0
|
||
|
||
tr = max(high - low, abs(high - self._prev_close), abs(low - self._prev_close))
|
||
self._prev_close = close
|
||
|
||
if not self._ready:
|
||
self._tr_buffer.append(tr)
|
||
self._values.append(0.0)
|
||
if len(self._tr_buffer) == self.period:
|
||
atr_val = sum(self._tr_buffer) / self.period
|
||
self._values[-1] = atr_val
|
||
self._tr_buffer.clear()
|
||
self._ready = True
|
||
return atr_val
|
||
return 0.0
|
||
|
||
# Wilder 平滑
|
||
atr_val = (self._values[-1] * (self.period - 1) + tr) / self.period
|
||
self._values.append(atr_val)
|
||
return atr_val
|
||
|
||
@property
|
||
def values(self) -> list[float]:
|
||
return self._values
|
||
|
||
@property
|
||
def current(self) -> float:
|
||
return self._values[-1] if self._values else 0.0
|
||
|
||
def __getitem__(self, idx: int) -> float:
|
||
return self._values[idx]
|
||
|
||
def __len__(self) -> int:
|
||
return len(self._values)
|
||
|
||
|
||
class RsiInc:
|
||
"""增量 RSI(Wilder 平滑)
|
||
|
||
内部维护完整序列,update() 为 O(1)。
|
||
"""
|
||
|
||
def __init__(self, period: int = 14):
|
||
self.period = period
|
||
self._values: list[float] = []
|
||
self._prev_price: Optional[float] = None
|
||
self._avg_gain: float = 0.0
|
||
self._avg_loss: float = 0.0
|
||
self._changes: list[float] = []
|
||
self._ready = False
|
||
|
||
def update(self, price: float) -> float:
|
||
if self._prev_price is None:
|
||
self._prev_price = price
|
||
self._values.append(0.0)
|
||
return 0.0
|
||
|
||
change = price - self._prev_price
|
||
self._prev_price = price
|
||
|
||
if not self._ready:
|
||
self._changes.append(change)
|
||
self._values.append(0.0)
|
||
if len(self._changes) == self.period:
|
||
gains = [max(c, 0.0) for c in self._changes]
|
||
losses = [abs(min(c, 0.0)) for c in self._changes]
|
||
self._avg_gain = sum(gains) / self.period
|
||
self._avg_loss = sum(losses) / self.period
|
||
self._changes.clear()
|
||
self._ready = True
|
||
rs = self._avg_gain / self._avg_loss if self._avg_loss > 0 else float("inf")
|
||
rsi = 100.0 - (100.0 / (1.0 + rs)) if self._avg_loss > 0 else 100.0
|
||
self._values[-1] = rsi
|
||
return rsi
|
||
return 0.0
|
||
|
||
gain = max(change, 0.0)
|
||
loss = abs(min(change, 0.0))
|
||
self._avg_gain = (self._avg_gain * (self.period - 1) + gain) / self.period
|
||
self._avg_loss = (self._avg_loss * (self.period - 1) + loss) / self.period
|
||
|
||
if self._avg_loss == 0:
|
||
rsi = 100.0
|
||
else:
|
||
rs = self._avg_gain / self._avg_loss
|
||
rsi = 100.0 - (100.0 / (1.0 + rs))
|
||
|
||
self._values.append(rsi)
|
||
return rsi
|
||
|
||
@property
|
||
def values(self) -> list[float]:
|
||
return self._values
|
||
|
||
@property
|
||
def current(self) -> float:
|
||
return self._values[-1] if self._values else 0.0
|
||
|
||
def __getitem__(self, idx: int) -> float:
|
||
return self._values[idx]
|
||
|
||
def __len__(self) -> int:
|
||
return len(self._values)
|
||
|
||
|
||
class BbInc:
|
||
"""增量布林带
|
||
|
||
内部维护完整序列,update() 返回 (upper, mid, lower) 三元组。
|
||
"""
|
||
|
||
def __init__(self, period: int = 20, std: float = 2.0):
|
||
self.period = period
|
||
self.std = std
|
||
self._upper: list[float] = []
|
||
self._mid: list[float] = []
|
||
self._lower: list[float] = []
|
||
self._window: list[float] = []
|
||
self._window_sum: float = 0.0
|
||
self._window_sum_sq: float = 0.0
|
||
|
||
def update(self, price: float) -> tuple[float, float, float]:
|
||
self._window.append(price)
|
||
self._window_sum += price
|
||
self._window_sum_sq += price * price
|
||
|
||
if len(self._window) < self.period:
|
||
self._upper.append(0.0)
|
||
self._mid.append(0.0)
|
||
self._lower.append(0.0)
|
||
return 0.0, 0.0, 0.0
|
||
|
||
if len(self._window) > self.period:
|
||
old = self._window.pop(0)
|
||
self._window_sum -= old
|
||
self._window_sum_sq -= old * old
|
||
|
||
mean = self._window_sum / self.period
|
||
variance = (self._window_sum_sq / self.period) - (mean * mean)
|
||
stdev = max(variance, 0.0) ** 0.5
|
||
|
||
upper = mean + self.std * stdev
|
||
lower = mean - self.std * stdev
|
||
|
||
self._upper.append(upper)
|
||
self._mid.append(mean)
|
||
self._lower.append(lower)
|
||
return upper, mean, lower
|
||
|
||
@property
|
||
def upper(self) -> list[float]:
|
||
return self._upper
|
||
|
||
@property
|
||
def mid(self) -> list[float]:
|
||
return self._mid
|
||
|
||
@property
|
||
def lower(self) -> list[float]:
|
||
return self._lower
|
||
|
||
def __len__(self) -> int:
|
||
return len(self._mid)
|