卡尔曼策略新增md文件

This commit is contained in:
2025-11-07 16:37:16 +08:00
parent 2eec6452ee
commit 2ae9f2db9e
89 changed files with 39954 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,196 @@
import numpy as np
import talib
from typing import Optional, Any, List
from src.core_data import Bar, Order
from src.indicators.base_indicators import Indicator
from src.indicators.indicators import Empty
from src.strategies.base_strategy import Strategy
class AreaReversalStrategy(Strategy):
"""
面积反转策略(含跟踪止损出场)
逻辑:
- 面积扩张 + 强度达标 + 局部见顶 + 面积收缩 → 等待反向突破开仓
- 出场:跟踪止损(回调出场)
"""
def __init__(
self,
context: Any,
main_symbol: str,
enable_log: bool,
trade_volume: int,
ma_period: int = 14,
area_window: int = 14,
strength_window: int = 50,
breakout_window: int = 20,
quantile_threshold: float = 0.5,
top_k: int = 3,
trailing_points: float = 100.0, # 跟踪止损点数
trailing_percent: float = None, # 或用百分比(如 0.01 = 1%
order_direction: Optional[List[str]] = None,
indicators: Optional[List[Indicator]] = None,
):
super().__init__(context, main_symbol, enable_log)
if order_direction is None:
order_direction = ["BUY", "SELL"]
if indicators is None:
indicators = [Empty(), Empty()]
self.trade_volume = trade_volume
self.ma_period = ma_period
self.area_window = area_window
self.strength_window = strength_window
self.breakout_window = breakout_window
self.quantile_threshold = quantile_threshold
self.top_k = top_k
self.trailing_points = trailing_points
self.trailing_percent = trailing_percent
self.order_direction = order_direction
self.indicators = indicators
# 跟踪止损状态
self.entry_price = None
self.highest_high = None # 多头持仓期间最高价
self.lowest_low = None # 空头持仓期间最低价
self.order_id_counter = 0
self.min_bars_needed = max(
ma_period,
area_window * 3,
strength_window,
breakout_window
) + 10
self.log("AreaReversalStrategy with Trailing Stop Initialized")
def _calculate_areas(self, closes: np.array, ma: np.array) -> np.array:
diffs = np.abs(closes - ma)
areas = talib.SUM(diffs, self.area_window)
return areas
def on_open_bar(self, open_price: float, symbol: str):
self.symbol = symbol
bar_history = self.get_bar_history()
if len(bar_history) < self.min_bars_needed or not self.trading:
return
position = self.get_current_positions().get(self.symbol, 0)
current_bar = bar_history[-1]
# === 计算指标 ===
closes = np.array([b.close for b in bar_history], dtype=float)
ma = talib.SMA(closes, self.ma_period)
areas = self._calculate_areas(closes, ma)
A1 = areas[-1]
A2 = areas[-2] if len(areas) >= 2 else 0
# 强度评估窗口
historical_areas = areas[-(self.strength_window + 1):-1]
if len(historical_areas) < self.strength_window:
return
# === 面积信号条件 ===
area_contracting = (A1 < A2) and (A2 > 0)
threshold = np.nanpercentile(historical_areas, self.quantile_threshold * 100)
strength_satisfied = (A2 >= threshold)
top_k_values = np.partition(historical_areas, -self.top_k)[-self.top_k:]
local_peak = (A2 >= np.min(top_k_values))
area_signal = area_contracting and strength_satisfied and local_peak
# === 突破判断 ===
recent_bars = bar_history[-self.breakout_window:]
highest = max(b.high for b in recent_bars)
lowest = min(b.low for b in recent_bars)
# =============== 开仓逻辑 ===============
if position == 0 and area_signal:
if "BUY" in self.order_direction and current_bar.high >= highest:
self.send_market_order("BUY", self.trade_volume, "OPEN")
self.entry_price = current_bar.close
self.highest_high = current_bar.high
self.lowest_low = None
self.log(f"🚀 Long Entry | A2={A2:.4f}")
elif "SELL" in self.order_direction and current_bar.low <= lowest:
self.send_market_order("SELL", self.trade_volume, "OPEN")
self.entry_price = current_bar.close
self.lowest_low = current_bar.low
self.highest_high = None
self.log(f"⬇️ Short Entry | A2={A2:.4f}")
# =============== 跟踪止损出场逻辑 ===============
elif position != 0 and self.entry_price is not None:
if position > 0:
# 更新最高价
if self.highest_high is None or current_bar.high > self.highest_high:
self.highest_high = current_bar.high
# 计算止损价
if self.trailing_percent is not None:
trailing_offset = self.highest_high * self.trailing_percent
else:
trailing_offset = self.trailing_points
stop_loss_price = self.highest_high - trailing_offset
if current_bar.low <= stop_loss_price:
self.close_position("CLOSE_LONG", position)
self._reset_state()
self.log(f"CloseOperation (Long Trailing Stop) @ {stop_loss_price:.5f}")
else: # position < 0
# 更新最低价
if self.lowest_low is None or current_bar.low < self.lowest_low:
self.lowest_low = current_bar.low
# 计算止损价
if self.trailing_percent is not None:
trailing_offset = self.lowest_low * self.trailing_percent
else:
trailing_offset = self.trailing_points
stop_loss_price = self.lowest_low + trailing_offset
if current_bar.high >= stop_loss_price:
self.close_position("CLOSE_SHORT", -position)
self._reset_state()
self.log(f"CloseOperation (Short Trailing Stop) @ {stop_loss_price:.5f}")
def _reset_state(self):
"""重置跟踪止损状态"""
self.entry_price = None
self.highest_high = None
self.lowest_low = None
# --- 模板方法 ---
def on_init(self):
super().on_init()
self.cancel_all_pending_orders(self.main_symbol)
self._reset_state()
def close_position(self, direction: str, volume: int):
self.send_market_order(direction, volume, offset="CLOSE")
def send_market_order(self, direction: str, volume: int, offset: str):
order_id = f"{self.symbol}_{direction}_MARKET_{self.order_id_counter}"
self.order_id_counter += 1
order = Order(
id=order_id,
symbol=self.symbol,
direction=direction,
volume=volume,
price_type="MARKET",
submitted_time=self.get_current_time(),
offset=offset,
)
self.send_order(order)
def on_rollover(self, old_symbol: str, new_symbol: str):
super().on_rollover(old_symbol, new_symbol)
self._reset_state()
self.log("Rollover: Reset trailing stop state.")

View File

@@ -0,0 +1,255 @@
import numpy as np
import talib
from typing import Optional, Any, List
from src.core_data import Bar, Order
from src.indicators.base_indicators import Indicator
from src.indicators.indicators import Empty
from src.strategies.base_strategy import Strategy
class AreaReversalStrategy(Strategy):
"""
面积反转策略(开仓逻辑不变,出场替换为 ATR 动态跟踪止损)
"""
def __init__(
self,
context: Any,
main_symbol: str,
enable_log: bool,
trade_volume: int,
ma_period: int = 14,
area_window: int = 14,
strength_window: int = 50,
breakout_window: int = 20,
quantile_threshold: float = 0.4,
top_k: int = 3,
# --- 原有跟踪止损(保留为后备)---
trailing_points: float = 100.0,
trailing_percent: float = None,
# --- 新增 ATR 动态止损参数 ---
atr_period: int = 14,
initial_atr_mult: float = 3.0, # 初始止损 = 1.0 * ATR
max_atr_mult: float = 9.0, # 最大止损 = 3.0 * ATR
scale_threshold_mult: float = 1.0, # 盈利达 initial_atr_mult * ATR 时开始扩大
use_atr_trailing: bool = True, # 是否启用 ATR 止损
# --- 其他 ---
order_direction: Optional[List[str]] = None,
indicators: Optional[List[Indicator]] = None,
):
super().__init__(context, main_symbol, enable_log)
if order_direction is None:
order_direction = ["BUY", "SELL"]
if indicators is None:
indicators = [Empty(), Empty()]
self.trade_volume = trade_volume
self.ma_period = ma_period
self.area_window = area_window
self.strength_window = strength_window
self.breakout_window = breakout_window
self.quantile_threshold = quantile_threshold
self.top_k = top_k
self.trailing_points = trailing_points
self.trailing_percent = trailing_percent
self.atr_period = atr_period
self.initial_atr_mult = initial_atr_mult
self.max_atr_mult = max_atr_mult
self.scale_threshold_mult = scale_threshold_mult
self.use_atr_trailing = use_atr_trailing
self.order_direction = order_direction
self.indicators = indicators
# 状态(新增 entry_atr
self.entry_price = None
self.highest_high = None
self.lowest_low = None
self.entry_atr = None # 入场时的 ATR 值
self.order_id_counter = 0
self.min_bars_needed = max(
ma_period,
area_window * 3,
strength_window,
breakout_window,
atr_period
) + 10
self.log("AreaReversalStrategy with ATR Trailing Stop Initialized")
def _calculate_areas(self, closes: np.array, ma: np.array) -> np.array:
diffs = np.abs(closes - ma)
areas = talib.SUM(diffs, self.area_window)
return areas
def on_open_bar(self, open_price: float, symbol: str):
self.symbol = symbol
bar_history = self.get_bar_history()
if len(bar_history) < self.min_bars_needed or not self.trading:
return
position = self.get_current_positions().get(self.symbol, 0)
current_bar = bar_history[-1]
# === 提取价格序列(新增 highs, lows 用于 ATR===
closes = np.array([b.close for b in bar_history], dtype=float)
highs = np.array([b.high for b in bar_history], dtype=float)
lows = np.array([b.low for b in bar_history], dtype=float)
# === 计算指标 ===
ma = talib.SMA(closes, self.ma_period)
areas = self._calculate_areas(closes, ma)
# 新增:计算 ATR
if self.use_atr_trailing:
atr = talib.ATR(highs, lows, closes, self.atr_period)
current_atr = atr[-1]
else:
current_atr = None
A1 = areas[-1]
A2 = areas[-2] if len(areas) >= 2 else 0
historical_areas = areas[-(self.strength_window + 1):-1]
if len(historical_areas) < self.strength_window:
return
# === 面积信号条件(完全不变)===
area_contracting = (A1 < A2) and (A2 > 0)
threshold = np.nanpercentile(historical_areas, self.quantile_threshold * 100)
strength_satisfied = (A2 >= threshold)
top_k_values = np.partition(historical_areas, -self.top_k)[-self.top_k:]
local_peak = (A2 >= np.min(top_k_values))
area_signal = area_contracting and strength_satisfied and local_peak
# === 突破判断(完全不变)===
recent_bars = bar_history[-self.breakout_window:]
highest = max(b.high for b in recent_bars)
lowest = min(b.low for b in recent_bars)
# =============== 开仓逻辑(完全不变)==============
if position == 0 and area_signal:
if "BUY" in self.order_direction and current_bar.high >= highest:
self.send_market_order("BUY", self.trade_volume, "OPEN")
self.entry_price = current_bar.close
self.highest_high = current_bar.high
self.lowest_low = None
if self.use_atr_trailing and current_atr is not None:
self.entry_atr = current_atr # 记录入场 ATR
self.log(f"🚀 Long Entry | A2={A2:.4f}")
elif "SELL" in self.order_direction and current_bar.low <= lowest:
self.send_market_order("SELL", self.trade_volume, "OPEN")
self.entry_price = current_bar.close
self.lowest_low = current_bar.low
self.highest_high = None
if self.use_atr_trailing and current_atr is not None:
self.entry_atr = current_atr
self.log(f"⬇️ Short Entry | A2={A2:.4f}")
# =============== 出场逻辑ATR 动态跟踪止损 ===============
elif position != 0 and self.entry_price is not None:
if self.use_atr_trailing and self.entry_atr is not None:
# --- ATR 动态止损 ---
if position > 0:
if self.highest_high is None or current_bar.high > self.highest_high:
self.highest_high = current_bar.high
unrealized_pnl = current_bar.close - self.entry_price
scale_threshold_pnl = self.scale_threshold_mult * self.initial_atr_mult * self.entry_atr
if unrealized_pnl <= 0:
trail_mult = self.initial_atr_mult
elif unrealized_pnl >= scale_threshold_pnl:
trail_mult = self.max_atr_mult
else:
ratio = unrealized_pnl / scale_threshold_pnl
trail_mult = self.initial_atr_mult + ratio * (self.max_atr_mult - self.initial_atr_mult)
stop_loss_price = self.highest_high - trail_mult * self.entry_atr
if current_bar.low <= stop_loss_price:
self.close_position("CLOSE_LONG", position)
self._reset_state()
self.log(f"CloseOperation (ATR Trailing) | Mult={trail_mult:.2f}")
else: # short
if self.lowest_low is None or current_bar.low < self.lowest_low:
self.lowest_low = current_bar.low
unrealized_pnl = self.entry_price - current_bar.close
scale_threshold_pnl = self.scale_threshold_mult * self.initial_atr_mult * self.entry_atr
if unrealized_pnl <= 0:
trail_mult = self.initial_atr_mult
elif unrealized_pnl >= scale_threshold_pnl:
trail_mult = self.max_atr_mult
else:
ratio = unrealized_pnl / scale_threshold_pnl
trail_mult = self.initial_atr_mult + ratio * (self.max_atr_mult - self.initial_atr_mult)
stop_loss_price = self.lowest_low + trail_mult * self.entry_atr
if current_bar.high >= stop_loss_price:
self.close_position("CLOSE_SHORT", -position)
self._reset_state()
self.log(f"CloseOperation (ATR Trailing) | Mult={trail_mult:.2f}")
else:
# --- 保留原有跟踪止损(后备)---
if position > 0:
if self.highest_high is None or current_bar.high > self.highest_high:
self.highest_high = current_bar.high
if self.trailing_percent is not None:
offset = self.highest_high * self.trailing_percent
else:
offset = self.trailing_points
stop_loss_price = self.highest_high - offset
if current_bar.low <= stop_loss_price:
self.close_position("CLOSE_LONG", position)
self._reset_state()
else:
if self.lowest_low is None or current_bar.low < self.lowest_low:
self.lowest_low = current_bar.low
if self.trailing_percent is not None:
offset = self.lowest_low * self.trailing_percent
else:
offset = self.trailing_points
stop_loss_price = self.lowest_low + offset
if current_bar.high >= stop_loss_price:
self.close_position("CLOSE_SHORT", -position)
self._reset_state()
def _reset_state(self):
self.entry_price = None
self.highest_high = None
self.lowest_low = None
self.entry_atr = None
# --- 模板方法(不变)---
def on_init(self):
super().on_init()
self.cancel_all_pending_orders(self.main_symbol)
self._reset_state()
def close_position(self, direction: str, volume: int):
self.send_market_order(direction, volume, offset="CLOSE")
def send_market_order(self, direction: str, volume: int, offset: str):
order_id = f"{self.symbol}_{direction}_MARKET_{self.order_id_counter}"
self.order_id_counter += 1
order = Order(
id=order_id,
symbol=self.symbol,
direction=direction,
volume=volume,
price_type="MARKET",
submitted_time=self.get_current_time(),
offset=offset,
)
self.send_order(order)
def on_rollover(self, old_symbol: str, new_symbol: str):
super().on_rollover(old_symbol, new_symbol)
self._reset_state()
self.log("Rollover: Reset trailing stop state.")

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,60 @@
# 面积反转策略Area Reversal Strategy
本策略源自主观交易员“猛将兄”的反转交易思想:**观测价格与均线之间的“面积”变化,当面积先扩张、再收缩,且伴随反向突破时,视为趋势动能耗尽、反转启动的信号**。我们将该主观逻辑完全量化,形成一套可复现、可验证的系统化策略。
## 📌 核心逻辑
1. **面积定义**
- 单根K线偏离`|close_t - MA_t|`
- 窗口面积(长度 L`A_t = Σ_{i=t-L+1}^{t} |close_i - MA_i|`
- 面积越大,表示价格对均线的**偏离强度越强且越持续**
2. **开仓条件(三者需同时满足)**
- **面积扩张后收缩**`A₁ < A₂`(最新面积小于前一段)
- **强度达标**`A₂ ≥ 过去 W 个面积的 50% 分位数`
- **局部见顶**`A₂` 为过去 W 个面积中的前 3 大值之一
- **反向突破**:价格突破前 N 根K线高/低点
3. **策略类型**
- **反转策略**:捕捉趋势末端的动能衰竭点
- **右侧入场**:需等待面积收缩 + 突破双重确认,避免左侧抄底
---
## 🧪 Strategy 1固定点数跟踪止损Baseline
- **出场逻辑**
入场后记录持仓期间最高价(多头)或最低价(空头),设置**固定点数回撤阈值**(如 100 跳)作为跟踪止损。
- 止损价 = `最高价 - 100`(多头)
- 止损价 = `最低价 + 100`(空头)
- **优点**
- 逻辑简单,回测稳定
- 在强趋势行情中能有效捕获大段利润
- **缺点**
- **100 跳对多数品种过大**,导致回撤不可控
- **调小后(如 50 跳)易被震荡行情洗出**,错过后续趋势
- **无法自适应不同品种的波动特性**(黄金 vs 外汇 vs 股指)
> 💡 此版本为策略基线,用于验证面积信号本身的有效性。
---
## 🚀 Strategy 2ATR 动态跟踪止损Optimized
- **核心改进**
引入 **ATR平均真实波幅** 动态调整止损距离,实现:
- **初始止损小**1.0 × ATR→ 控制单笔风险
- **盈利后止损逐步扩大**(线性过渡至 3.0 × ATR→ 容忍趋势中的正常回撤
- **完全自适应品种波动率**,统一参数适用于多资产
```
- **优势**
- **解决“100跳太大调小就失效”的困境**
- 在趋势行情中**让利润充分奔跑**,在震荡行情中**自动收紧风险**
- 保留原始开仓逻辑不变,仅优化风险管理
> ✅ Strategy 2 在不改变信号生成的前提下,显著提升策略的风险调整后收益,是面积反转策略的**工程化优化方向**。
---

View File

@@ -0,0 +1,103 @@
import multiprocessing
from typing import Tuple, Dict, Any, Optional
from src.analysis.result_analyzer import ResultAnalyzer
from src.backtest_engine import BacktestEngine
from src.data_manager import DataManager
# --- 单个回测任务函数 ---
# 这个函数将在每个独立的进程中运行,因此它必须是自包含的
def run_single_backtest(
combination: Tuple[float, float], # 传入当前参数组合
common_config: Dict[str, Any] # 传入公共配置 (如数据路径, 初始资金等)
) -> Optional[Dict[str, Any]]:
"""
运行单个参数组合的回测任务。
此函数将在一个独立的进程中执行。
"""
p1_value, p2_value = combination
# 从 common_config 中获取必要的配置
symbol = common_config['symbol']
data_path = common_config['data_path']
initial_capital = common_config['initial_capital']
slippage_rate = common_config['slippage_rate']
commission_rate = common_config['commission_rate']
start_time = common_config['start_time']
end_time = common_config['end_time']
roll_over_mode = common_config['roll_over_mode']
# bar_duration_seconds = common_config['bar_duration_seconds'] # 如果DataManager需要可以再传
param1_name = common_config['param1_name']
param2_name = common_config['param2_name']
# 每个进程内部独立初始化 DataManager 和 BacktestEngine
# 确保每个进程有自己的数据副本和模拟状态
data_manager = DataManager(
file_path=data_path,
symbol=symbol,
# bar_duration_seconds=bar_duration_seconds, # 如果DataManager需要根据数据文件路径推断或者额外参数传入
# start_date=start_time.date(), # DataManager 现在通过 file_path 和 symbol 处理数据
# end_date=end_time.date(),
)
# data_manager.load_data() # DataManager 内部加载数据
strategy_parameters = {
'main_symbol': common_config['main_symbol'],
'trade_volume': 1,
param1_name: p1_value, # 15分钟扫荡K线下影线占其总范围的最小比例。
param2_name: p2_value, # 15分钟限价单的入场点位于扫荡K线低点到收盘价的斐波那契回撤比例。
'order_direction': common_config['order_direction'],
'enable_log': False, # 建议在调试和测试时开启日志
}
# 打印当前进程正在处理的组合信息
# 注意:多进程打印会交错显示
print(f"--- 正在运行组合: {strategy_parameters} (PID: {multiprocessing.current_process().pid}) ---")
try:
# 初始化回测引擎
engine = BacktestEngine(
data_manager=data_manager,
strategy_class=common_config['strategy'],
strategy_params=strategy_parameters,
initial_capital=initial_capital,
slippage_rate=slippage_rate,
commission_rate=commission_rate,
roll_over_mode=True, # 保持换月模式
start_time=common_config['start_time'],
end_time=common_config['end_time']
)
# 运行回测,传入时间范围
engine.run_backtest()
# 获取回测结果并分析
results = engine.get_backtest_results()
portfolio_snapshots = results["portfolio_snapshots"]
trade_history = results["trade_history"]
bars = results["all_bars"]
initial_capital_result = results["initial_capital"]
if portfolio_snapshots:
analyzer = ResultAnalyzer(portfolio_snapshots, trade_history, bars, initial_capital_result)
# analyzer.generate_report()
# analyzer.plot_performance()
metrics = analyzer.calculate_all_metrics()
# 将当前组合的参数和性能指标存储起来
result_entry = {**strategy_parameters, **metrics}
return result_entry
else:
print(
f" 组合 {strategy_parameters} 没有生成投资组合快照,无法进行结果分析。(PID: {multiprocessing.current_process().pid})")
# 返回一个包含参数和默认0值的结果以便追踪失败组合
return {**strategy_parameters, "total_return": 0.0, "annualized_return": 0.0, "sharpe_ratio": 0.0,
"max_drawdown": 0.0, "error": "No portfolio snapshots"}
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print(
f" 组合 {strategy_parameters} 运行失败: {e}\n{error_trace} (PID: {multiprocessing.current_process().pid})")
# 返回错误信息,以便后续处理
return {**strategy_parameters, "error": str(e), "traceback": error_trace}