193 lines
7.4 KiB
Python
193 lines
7.4 KiB
Python
|
|
import numpy as np
|
|||
|
|
from typing import Optional, Any, List
|
|||
|
|
from src.core_data import Bar, Order
|
|||
|
|
from src.strategies.base_strategy import Strategy
|
|||
|
|
|
|||
|
|
|
|||
|
|
class SemiVarianceAsymmetryStrategy(Strategy):
|
|||
|
|
"""
|
|||
|
|
已实现半方差不对称策略 (RSVA)
|
|||
|
|
|
|||
|
|
核心原理:
|
|||
|
|
放弃"阈值计数",改用"波动能量占比"。
|
|||
|
|
因子 = (上行波动能量 - 下行波动能量) / 总波动能量
|
|||
|
|
|
|||
|
|
优势:
|
|||
|
|
1. 自适应:自动适应2021的高波动和2023的低波动,无需调整阈值。
|
|||
|
|
2. 灵敏:能捕捉到没有大阳线但持续上涨的"蠕动趋势"。
|
|||
|
|
3. 稳健:使用平方项(Variance)而非三次方(Skewness),对异常值更鲁棒。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
context: Any,
|
|||
|
|
main_symbol: str,
|
|||
|
|
enable_log: bool,
|
|||
|
|
trade_volume: int,
|
|||
|
|
# --- 窗口参数 ---
|
|||
|
|
season_days: int = 20, # 计算日内季节性基准的回溯天数
|
|||
|
|
calc_window: int = 120, # 计算不对称因子的窗口 (约5天)
|
|||
|
|
cycle_length: int = 23, # 固定周期 (每天23根Bar)
|
|||
|
|
|
|||
|
|
# --- 信号阈值 ---
|
|||
|
|
# RSVA 范围是 [-1, 1]。
|
|||
|
|
# 0.2 表示上涨能量比下跌能量多20% (即 60% vs 40%),是一个显著的失衡信号。
|
|||
|
|
entry_threshold: float = 0.2,
|
|||
|
|
exit_threshold: float = 0.05,
|
|||
|
|
|
|||
|
|
order_direction: Optional[List[str]] = None,
|
|||
|
|
):
|
|||
|
|
super().__init__(context, main_symbol, enable_log)
|
|||
|
|
if order_direction is None:
|
|||
|
|
order_direction = ['BUY', 'SELL']
|
|||
|
|
|
|||
|
|
self.trade_volume = trade_volume
|
|||
|
|
self.season_days = season_days
|
|||
|
|
self.calc_window = calc_window
|
|||
|
|
self.cycle_length = cycle_length
|
|||
|
|
self.entry_threshold = entry_threshold
|
|||
|
|
self.exit_threshold = exit_threshold
|
|||
|
|
self.order_direction = order_direction
|
|||
|
|
|
|||
|
|
# 计算最小历史需求
|
|||
|
|
# 我们需要: calc_window 个标准化数据
|
|||
|
|
# 每个标准化数据需要回溯: season_days * cycle_length
|
|||
|
|
self.min_history = self.calc_window + (self.season_days * self.cycle_length)
|
|||
|
|
|
|||
|
|
# 缓冲区设大一点,避免频繁触发边界检查
|
|||
|
|
self.calc_buffer_size = self.min_history + 100
|
|||
|
|
|
|||
|
|
self.log(f"RSVA Strategy Init: Window={calc_window}, Thresh={entry_threshold}")
|
|||
|
|
|
|||
|
|
def on_open_bar(self, open_price: float, symbol: str):
|
|||
|
|
self.cancel_all_pending_orders(symbol)
|
|||
|
|
|
|||
|
|
# 1. 获取历史数据 (切片优化)
|
|||
|
|
all_history = self.get_bar_history()
|
|||
|
|
total_len = len(all_history)
|
|||
|
|
|
|||
|
|
if total_len < self.min_history:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 只取计算所需的最后一段数据,保证计算复杂度恒定
|
|||
|
|
start_idx = max(0, total_len - self.calc_buffer_size)
|
|||
|
|
relevant_bars = all_history[start_idx:]
|
|||
|
|
|
|||
|
|
# 转为 numpy array
|
|||
|
|
closes = np.array([b.close for b in relevant_bars])
|
|||
|
|
|
|||
|
|
# 2. 计算对数收益率 (Log Returns)
|
|||
|
|
# 对数收益率消除了价格水平(Price Level)的影响
|
|||
|
|
log_rets = np.diff(np.log(closes))
|
|||
|
|
current_idx = len(log_rets) - 1
|
|||
|
|
|
|||
|
|
# 3. 标准化收益率计算 (De-seasonalization)
|
|||
|
|
# 这一步至关重要:剔除日内季节性(早盘波动大、午盘波动小)的干扰
|
|||
|
|
std_rets = []
|
|||
|
|
|
|||
|
|
# 循环计算过去 calc_window 个点的标准化值
|
|||
|
|
for i in range(self.calc_window):
|
|||
|
|
target_idx = current_idx - i
|
|||
|
|
|
|||
|
|
# 高效切片:利用 stride=cycle_length 提取同一时间槽的历史
|
|||
|
|
# slot_history 包含 [t, t-23, t-46, ...]
|
|||
|
|
slot_history = log_rets[target_idx::-self.cycle_length]
|
|||
|
|
|
|||
|
|
# 截取 season_days
|
|||
|
|
if len(slot_history) > self.season_days:
|
|||
|
|
slot_history = slot_history[:self.season_days]
|
|||
|
|
|
|||
|
|
# 计算该时刻的基准波动率
|
|||
|
|
if len(slot_history) < 5:
|
|||
|
|
# 降级处理:样本不足时用近期全局波动率
|
|||
|
|
slot_vol = np.std(log_rets[-self.cycle_length:]) + 1e-9
|
|||
|
|
else:
|
|||
|
|
slot_vol = np.std(slot_history) + 1e-9
|
|||
|
|
|
|||
|
|
# 标准化 (Z-Score)
|
|||
|
|
std_ret = log_rets[target_idx] / slot_vol
|
|||
|
|
std_rets.append(std_ret)
|
|||
|
|
|
|||
|
|
# 转为数组 (注意:std_rets 是倒序的,但这不影响平方和计算)
|
|||
|
|
std_rets_arr = np.array(std_rets)
|
|||
|
|
|
|||
|
|
# 4. 【核心】计算已实现半方差不对称性 (RSVA)
|
|||
|
|
|
|||
|
|
# 分离正收益和负收益
|
|||
|
|
pos_rets = std_rets_arr[std_rets_arr > 0]
|
|||
|
|
neg_rets = std_rets_arr[std_rets_arr < 0]
|
|||
|
|
|
|||
|
|
# 计算上行能量 (Upside Variance) 和 下行能量 (Downside Variance)
|
|||
|
|
rv_pos = np.sum(pos_rets ** 2)
|
|||
|
|
rv_neg = np.sum(neg_rets ** 2)
|
|||
|
|
total_rv = rv_pos + rv_neg + 1e-9 # 防止除零
|
|||
|
|
|
|||
|
|
# 计算因子: [-1, 1]
|
|||
|
|
# > 0 说明上涨更有力(或更频繁),< 0 说明下跌主导
|
|||
|
|
rsva_factor = (rv_pos - rv_neg) / total_rv
|
|||
|
|
|
|||
|
|
# 5. 交易逻辑
|
|||
|
|
current_pos = self.get_current_positions().get(symbol, 0)
|
|||
|
|
|
|||
|
|
self.log_status(rsva_factor, rv_pos, rv_neg, current_pos)
|
|||
|
|
|
|||
|
|
if current_pos == 0:
|
|||
|
|
self.evaluate_entry(rsva_factor)
|
|||
|
|
else:
|
|||
|
|
self.evaluate_exit(current_pos, rsva_factor)
|
|||
|
|
|
|||
|
|
def evaluate_entry(self, factor: float):
|
|||
|
|
direction = None
|
|||
|
|
|
|||
|
|
# 因子 > 0.2: 哪怕没有极端K线,只要累计的上涨能量显著压过下跌能量,就开仓
|
|||
|
|
if factor > self.entry_threshold:
|
|||
|
|
if "BUY" in self.order_direction:
|
|||
|
|
direction = "BUY"
|
|||
|
|
|
|||
|
|
elif factor < -self.entry_threshold:
|
|||
|
|
if "SELL" in self.order_direction:
|
|||
|
|
direction = "SELL"
|
|||
|
|
|
|||
|
|
if direction:
|
|||
|
|
self.log(f"ENTRY: {direction} | RSVA={factor:.4f}")
|
|||
|
|
self.send_market_order(direction, self.trade_volume, "OPEN")
|
|||
|
|
|
|||
|
|
def evaluate_exit(self, volume: int, factor: float):
|
|||
|
|
do_exit = False
|
|||
|
|
reason = ""
|
|||
|
|
|
|||
|
|
# 当多空能量趋于平衡 (因子回到 0 附近),说明趋势动能耗尽,平仓
|
|||
|
|
# 这种离场方式对震荡市非常友好:一旦陷入震荡,rv_pos 和 rv_neg 会迅速接近,因子归零
|
|||
|
|
if volume > 0 and factor < self.exit_threshold:
|
|||
|
|
do_exit = True
|
|||
|
|
reason = f"Bull Energy Fade (RSVA={factor:.4f})"
|
|||
|
|
|
|||
|
|
elif volume < 0 and factor > -self.exit_threshold:
|
|||
|
|
do_exit = True
|
|||
|
|
reason = f"Bear Energy Fade (RSVA={factor:.4f})"
|
|||
|
|
|
|||
|
|
if do_exit:
|
|||
|
|
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
|||
|
|
self.log(f"EXIT: {reason}")
|
|||
|
|
self.send_market_order(direction, abs(volume), "CLOSE")
|
|||
|
|
|
|||
|
|
def send_market_order(self, direction: str, volume: int, offset: str):
|
|||
|
|
# 严格遵守要求:使用 get_current_time()
|
|||
|
|
current_time = self.get_current_time()
|
|||
|
|
|
|||
|
|
order = Order(
|
|||
|
|
id=f"{self.main_symbol}_{direction}_{current_time.timestamp()}",
|
|||
|
|
symbol=self.symbol,
|
|||
|
|
direction=direction,
|
|||
|
|
volume=volume,
|
|||
|
|
price_type="MARKET",
|
|||
|
|
submitted_time=current_time,
|
|||
|
|
offset=offset
|
|||
|
|
)
|
|||
|
|
self.send_order(order)
|
|||
|
|
|
|||
|
|
def log_status(self, factor: float, pos_e: float, neg_e: float, current_pos: int):
|
|||
|
|
if self.enable_log:
|
|||
|
|
# 仅在有持仓或信号明显时打印
|
|||
|
|
if current_pos != 0 or abs(factor) > self.entry_threshold * 0.8:
|
|||
|
|
self.log(f"Status: Pos={current_pos} | RSVA={factor:.4f} | Energy(+/-)={pos_e:.1f}/{neg_e:.1f}")
|