新增实盘策略:FisherTrendStrategy(FG)
This commit is contained in:
177
AGENTS.md
Normal file
177
AGENTS.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# AGENTS.md - Quant Trading Project Guidelines
|
||||
|
||||
This document provides guidelines for agentic coding agents operating in this repository.
|
||||
|
||||
## 对话语言规则 (Conversation Language Rules)
|
||||
|
||||
- **请始终使用简体中文进行回复,除非我明确要求使用其他语言。**
|
||||
- **涉及编程术语时,请保留英文原文并(在必要时)提供中文解释。**
|
||||
|
||||
## Project Overview
|
||||
|
||||
Python-based quantitative trading system with:
|
||||
- Backtesting engine (`src/backtest_engine.py`)
|
||||
- Multiple strategy implementations (`src/strategies/`)
|
||||
- TQSdk integration for real-time trading (`src/tqsdk_engine.py`, `src/tqsdk_real_engine.py`)
|
||||
- Strategy management (`strategy_manager/`)
|
||||
- Data processing and analysis (`src/analysis/`)
|
||||
|
||||
## Build, Lint, and Test Commands
|
||||
|
||||
### Python Environment
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run single test file
|
||||
pytest test/test_file.py -v
|
||||
|
||||
# Run single test function
|
||||
pytest test/test_file.py::TestClass::test_function -v
|
||||
|
||||
# Run tests with coverage
|
||||
pytest --cov=src --cov-report=term-missing
|
||||
|
||||
# Run tests matching pattern
|
||||
pytest -k "test_name_pattern"
|
||||
```
|
||||
|
||||
### Jupyter Notebooks
|
||||
Several research notebooks exist in root directory:
|
||||
- `main.ipynb`, `main2.ipynb`, `main_multi.ipynb`
|
||||
- `grid_search.ipynb`, `grid_search_multi_process.ipynb`
|
||||
- `tqsdk_main.ipynb`, `tqsdk_main2.ipynb`
|
||||
|
||||
Run via Jupyter Lab:
|
||||
```bash
|
||||
jupyter lab
|
||||
```
|
||||
|
||||
### Strategy Manager (Web Backend)
|
||||
```bash
|
||||
cd strategy_manager && python start.py
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Imports
|
||||
- Use absolute imports from `src` package:
|
||||
```python
|
||||
from src.backtest_engine import BacktestEngine
|
||||
from src.strategies.base_strategy import Strategy
|
||||
```
|
||||
- Group imports: standard library → third-party → local modules
|
||||
- Use `TYPE_CHECKING` for circular import avoidance with type hints:
|
||||
```python
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from src.core_data import Order
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- **Classes**: PascalCase (`BacktestEngine`, `SimpleLimitBuyStrategy`)
|
||||
- **Functions/Variables**: snake_case (`get_current_positions`, `send_order`)
|
||||
- **Constants**: UPPER_SNAKE_CASE (`BEIJING_TZ`, `TRADING_SESSIONS_TIMES`)
|
||||
- **Private members**: Leading underscore (`_indicator_cache`, `_clean_params_for_hashing`)
|
||||
|
||||
### Type Annotations
|
||||
- Use Python's `typing` module for type hints
|
||||
- Common patterns:
|
||||
```python
|
||||
from typing import Dict, Any, Optional, List, Union
|
||||
def func(param: Dict[str, Any]) -> Optional[float]:
|
||||
```
|
||||
- Use string annotations for forward references when needed:
|
||||
```python
|
||||
def send_order(self, order: "Order") -> Optional[Order]:
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
- Use specific exception types, not bare `except:`
|
||||
- Log errors with context:
|
||||
```python
|
||||
try:
|
||||
# operation
|
||||
except Exception as e:
|
||||
self.log(f"Operation failed: {e}")
|
||||
```
|
||||
- Raise descriptive exceptions:
|
||||
```python
|
||||
raise ValueError("Step direction inconsistent with range")
|
||||
```
|
||||
|
||||
### Docstrings and Comments
|
||||
- Use Chinese for comments and docstrings (project convention)
|
||||
- Document public methods with Args/Returns sections:
|
||||
```python
|
||||
def generate_parameter_range(start, end, step):
|
||||
"""
|
||||
根据开始、结束和步长生成参数值列表。
|
||||
|
||||
Args:
|
||||
start: 参数范围的起始值
|
||||
end: 参数范围的结束值
|
||||
step: 参数的步长
|
||||
|
||||
Returns:
|
||||
生成的参数值列表
|
||||
"""
|
||||
```
|
||||
|
||||
### Code Formatting
|
||||
- No strict formatting rules (no ruff/black config found)
|
||||
- Follow Python PEP 8 generally
|
||||
- Maximum line length: 120 characters recommended
|
||||
- Use 4 spaces for indentation
|
||||
|
||||
### Project-Specific Patterns
|
||||
|
||||
#### Strategy Implementation
|
||||
All strategies inherit from `Strategy` ABC:
|
||||
```python
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
class MyStrategy(Strategy):
|
||||
def on_open_bar(self, open: float, symbol: str):
|
||||
# Implement strategy logic
|
||||
pass
|
||||
```
|
||||
|
||||
#### Backtest Context
|
||||
Strategies interact with `BacktestContext` for:
|
||||
- Order management (`send_order`, `cancel_order`)
|
||||
- Position tracking (`get_current_positions`)
|
||||
- Historical data (`get_price_history`, `get_bar_history`)
|
||||
|
||||
#### Trading Sessions
|
||||
Define trading hours in `src/common_utils.py`:
|
||||
```python
|
||||
TRADING_SESSIONS_TIMES: List[Tuple[time, time]] = [
|
||||
(time(9, 0, 0), time(11, 30, 0)),
|
||||
(time(13, 30, 0), time(15, 0, 0)),
|
||||
(time(21, 0, 0), time(23, 0, 0))
|
||||
]
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
NewQuant/
|
||||
├── src/ # Core codebase
|
||||
│ ├── strategies/ # Strategy implementations
|
||||
│ ├── analysis/ # Result analysis tools
|
||||
│ ├── indicators/ # Technical indicators
|
||||
│ └── *.py # Engine and utility modules
|
||||
├── futures_trading_strategies/ # Additional strategies by commodity
|
||||
├── strategy_manager/ # Web backend and launcher
|
||||
├── test/ # Tests (Jupyter notebooks)
|
||||
├── data/ # Data files
|
||||
└── *.ipynb # Research notebooks
|
||||
```
|
||||
|
||||
## Key Files for Reference
|
||||
|
||||
- `src/strategies/base_strategy.py` - Strategy interface definition
|
||||
- `src/backtest_engine.py` - Backtesting core
|
||||
- `src/common_utils.py` - Utility functions and constants
|
||||
- `strategy_manager/start.py` - Web service entry point
|
||||
@@ -0,0 +1,216 @@
|
||||
import numpy as np
|
||||
import math
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List, Dict
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class PragmaticCyberneticStrategy(Strategy):
|
||||
"""
|
||||
【务实优化版·控制论策略】
|
||||
|
||||
优化核心:
|
||||
1. 保持“钝感”:拒绝灵敏的趋势反转平仓,保留“死拿趋势”的盈利特性。
|
||||
2. 修复系数:将 Stoch 映射系数从 0.66 提升至 1.98,使 Fisher 值域恢复到 [-3, 3] 可调范围。
|
||||
- 现在你可以设置 exit_level = 2.0 来捕捉极端利润,而不是被迫等到换月。
|
||||
3. 纯粹逻辑:开仓是开仓,平仓是平仓。互不干扰,消除中间地带的内耗。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 核心参数 (保持原策略风格) ---
|
||||
trend_period: int = 26, # T: 趋势中轴
|
||||
fisher_period: int = 20, # FB: 动能周期 (原策略 46 可能太慢,建议 20-30)
|
||||
atr_period: int = 23,
|
||||
|
||||
# --- 阈值参数 (关键调整) ---
|
||||
# 1. 止盈阈值:因为系数修复了,现在 FB 能跑到 2.5 以上。
|
||||
# 建议设为 2.0 - 2.5。如果设得很高(如 5.0),效果就等于你原来的“死拿”。
|
||||
fisher_exit_level: float = 2.2,
|
||||
|
||||
# 2. 入场阈值:保持在 0.5 左右,只接深回调
|
||||
fb_entry_threshold: float = 0.,
|
||||
|
||||
stop_mult: float = 2, # 稍微放宽止损,适应趋势震荡
|
||||
limit_offset_mult: float = 0.2, # FV 挂单偏移
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
|
||||
self.fisher_exit_level = fisher_exit_level
|
||||
self.fb_entry_threshold = fb_entry_threshold
|
||||
self.stop_mult = stop_mult
|
||||
self.limit_offset_mult = limit_offset_mult
|
||||
|
||||
self.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.indicator = indicator
|
||||
|
||||
# 缓存
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len) + 5
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
# 记录上一根 Bar 的 FB,仅用于判断拐点
|
||||
self._prev_fb = 0.0
|
||||
self.order_id_counter = 0
|
||||
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def _calculate_indicators(self):
|
||||
"""
|
||||
计算逻辑:保留原策略的简洁性,仅修复 Scaling 系数
|
||||
"""
|
||||
if len(self._closes) < self._buf_len:
|
||||
return None, None, None, None
|
||||
|
||||
# 1. 趋势中轴 T (物理中轴)
|
||||
h_trend = list(self._highs)[-self.t_len:]
|
||||
l_trend = list(self._lows)[-self.t_len:]
|
||||
T = (max(h_trend) + min(l_trend)) / 2.0
|
||||
|
||||
# 2. 公平价 FV (FIR 滤波)
|
||||
FV = (self._closes[-1] + 2 * self._closes[-2] + 2 * self._closes[-3] + self._closes[-4]) / 6.0
|
||||
|
||||
# 3. Fisher Transform (非递归版,响应更快)
|
||||
h_fisher = list(self._highs)[-self.f_len:]
|
||||
l_fisher = list(self._lows)[-self.f_len:]
|
||||
max_h, min_l = max(h_fisher), min(l_fisher)
|
||||
denom = max_h - min_l if max_h != min_l else self.min_tick
|
||||
|
||||
# --- 关键修正点 ---
|
||||
# 你的原代码是 0.66 * (stoc - 0.5),这导致最大值被锁死。
|
||||
# 改为 1.98,使得输入范围从 [-0.33, 0.33] 扩大到 [-0.99, 0.99]。
|
||||
# 这样 math.log 就能计算出 -3 到 +3 的值,让止盈逻辑“复活”且可控。
|
||||
stoc = (self._closes[-1] - min_l) / denom
|
||||
value = max(-0.999, min(0.999, 1.98 * (stoc - 0.5)))
|
||||
FB = 0.5 * math.log((1.0 + value) / (1.0 - value))
|
||||
|
||||
# 4. ATR
|
||||
tr_list = []
|
||||
for i in range(1, self.atr_len + 1):
|
||||
h, l, pc = self._highs[-i], self._lows[-i], self._closes[-i - 1]
|
||||
tr = max(h - l, abs(h - pc), abs(l - pc))
|
||||
tr_list.append(tr)
|
||||
ATR = sum(tr_list) / self.atr_len
|
||||
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
# 每次 Bar 重置挂单,防止挂单“长在”图表上
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
if T is None: return
|
||||
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
# 状态定义
|
||||
# 这里我们不去定义 "short_signal" 这种会诱发反向平仓的变量
|
||||
# 而是只关注眼下的:大势(T) 和 动能(FB)
|
||||
trend_up = prev_bar.close > T
|
||||
trend_down = prev_bar.close < T
|
||||
|
||||
# ==========================================
|
||||
# 1. 持仓逻辑:简单、迟钝、粘性强
|
||||
# ==========================================
|
||||
if pos != 0:
|
||||
stop_dist = max(self.stop_mult * ATR, self.min_tick * 20)
|
||||
|
||||
if pos > 0:
|
||||
# A. 动能止盈 (Mean Reversion Exit)
|
||||
# 只有当行情极其疯狂(FB > 2.2) 且开始回头时才止盈。
|
||||
# 正常波动绝不下车。
|
||||
if FB > self.fisher_exit_level and FB < self._prev_fb:
|
||||
self.log(f"TAKE PROFIT (Long): FB {FB:.2f} Peak Reached")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 硬止损 (ATR Stop) - 最后的防线
|
||||
if prev_bar.close < entry_price - stop_dist:
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# C. (可选) 极端趋势反转保护
|
||||
# 如果你希望策略更像原来的“死拿”,这部分可以注释掉,或者把判定条件设严
|
||||
# if prev_bar.close < T - ATR: ...
|
||||
|
||||
elif pos < 0:
|
||||
# A. 动能止盈
|
||||
if FB < -self.fisher_exit_level and FB > self._prev_fb:
|
||||
self.log(f"TAKE PROFIT (Short): FB {FB:.2f} Bottom Reached")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 硬止损
|
||||
if prev_bar.close > entry_price + stop_dist:
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# ==========================================
|
||||
# 2. 开仓逻辑:严苛、左侧、顺大势
|
||||
# ==========================================
|
||||
if pos == 0:
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
# 挂单价格优化:
|
||||
# 在 FV 基础上再便宜一点点,增加胜率
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
|
||||
# 开多:趋势向上 + 动能深跌 (FB < -0.5)
|
||||
if trend_up and FB < -self.fb_entry_threshold and is_met:
|
||||
if "BUY" in self.order_direction:
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
|
||||
# 开空:趋势向下 + 动能冲高 (FB > 0.5)
|
||||
elif trend_down and FB > self.fb_entry_threshold and is_met:
|
||||
if "SELL" in self.order_direction:
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
|
||||
# 记录上一根FB,仅用于止盈时的拐点比较
|
||||
self._prev_fb = FB
|
||||
|
||||
# --- 辅助 ---
|
||||
def send_market_order(self, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_MKT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||||
submitted_time=self.get_current_time(), offset=offset))
|
||||
|
||||
def send_limit_order(self, price, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_LMT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=price))
|
||||
@@ -0,0 +1,224 @@
|
||||
import numpy as np
|
||||
import math
|
||||
import talib
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class PragmaticCyberneticStrategy(Strategy):
|
||||
"""
|
||||
【务实回归版·控制论策略】(TA-Lib 提速 + 换月支持)
|
||||
|
||||
哲学坚守:
|
||||
1. 捍卫原版出场逻辑:坚决使用基于 entry_price 的固定硬止损。脱离成本区后,给予价格无限宽容度,实现长线死拿。
|
||||
2. 动能极值止盈:恢复 FB > 1.6 且拐头时的精准落袋为安。
|
||||
3. 拒绝画蛇添足:没有任何 _target_state,不堆砌 Trick,底层数据即真理。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 核心周期 ---
|
||||
trend_period: int = 26,
|
||||
fisher_period: int = 20,
|
||||
atr_period: int = 23,
|
||||
|
||||
# --- 经过验证的优秀参数 ---
|
||||
fisher_exit_level: float = 1.6, # 配合 1.98 修复后的 FB,1.6 是一个非常好的极值止盈点
|
||||
fb_entry_threshold: float = 0.1, # 0.1 确保在顺大势的前提下,动能稍有配合即可入场
|
||||
|
||||
# --- 风险控制 ---
|
||||
stop_mult: float = 2.0, # 原始的硬止损乘数
|
||||
limit_offset_mult: float = 0.,
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
|
||||
self.fisher_exit_level = fisher_exit_level
|
||||
self.fb_entry_threshold = fb_entry_threshold
|
||||
self.stop_mult = stop_mult
|
||||
self.limit_offset_mult = limit_offset_mult
|
||||
|
||||
self.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.indicator = indicator
|
||||
|
||||
# 为 talib.ATR 预留充足的数据预热空间
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len * 2) + 100
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
# 使用 deque 记录 FB,相比单独的 _prev_fb 变量更安全,不会因为初始 0 导致误判
|
||||
self._fbs = deque(maxlen=2)
|
||||
self.order_id_counter = 0
|
||||
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def _calculate_indicators(self):
|
||||
"""完全无状态的纯数学指标计算 (基于 TA-Lib)"""
|
||||
if len(self._closes) < self._buf_len:
|
||||
return None, None, None, None
|
||||
|
||||
np_highs = np.array(self._highs, dtype=np.float64)
|
||||
np_lows = np.array(self._lows, dtype=np.float64)
|
||||
np_closes = np.array(self._closes, dtype=np.float64)
|
||||
|
||||
# 1. T轴 (大势过滤)
|
||||
t_max_h = talib.MAX(np_highs, timeperiod=self.t_len)[-1]
|
||||
t_min_l = talib.MIN(np_lows, timeperiod=self.t_len)[-1]
|
||||
T = (t_max_h + t_min_l) / 2.0
|
||||
|
||||
# 2. FV (公允价值挂单基准)
|
||||
FV = (np_closes[-1] + 2 * np_closes[-2] + 2 * np_closes[-3] + np_closes[-4]) / 6.0
|
||||
|
||||
# 3. Fisher Transform (1.98 数学修复版)
|
||||
f_max_h = talib.MAX(np_highs, timeperiod=self.f_len)[-1]
|
||||
f_min_l = talib.MIN(np_lows, timeperiod=self.f_len)[-1]
|
||||
denom = f_max_h - f_min_l if f_max_h != f_min_l else self.min_tick
|
||||
|
||||
stoc = (np_closes[-1] - f_min_l) / denom
|
||||
value = max(-0.999, min(0.999, 1.98 * (stoc - 0.5)))
|
||||
FB = 0.5 * math.log((1.0 + value) / (1.0 - value))
|
||||
|
||||
# 4. ATR
|
||||
atr_array = talib.ATR(np_highs, np_lows, np_closes, timeperiod=self.atr_len)
|
||||
ATR = atr_array[-1]
|
||||
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
if T is None or math.isnan(ATR): return
|
||||
|
||||
self._fbs.append(FB)
|
||||
# 必须凑齐两根 FB 才能判断拐点,否则跳过
|
||||
if len(self._fbs) < 2: return
|
||||
|
||||
current_fb = self._fbs[-1]
|
||||
prev_fb = self._fbs[-2]
|
||||
|
||||
# 绝对无状态:从账户物理数据获取真实持仓和开仓均价
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
# ==========================================
|
||||
# 核心一:经典出场逻辑 (你的原版设计)
|
||||
# ==========================================
|
||||
if pos != 0:
|
||||
# 最小 20 tick 保护,防止 ATR 极度萎缩时止损过窄
|
||||
stop_dist = max(self.stop_mult * ATR, self.min_tick * 20)
|
||||
|
||||
if pos > 0:
|
||||
# A. 动能极值止盈:到达 1.6 且刚刚发生向下拐头
|
||||
if current_fb > self.fisher_exit_level and current_fb < prev_fb:
|
||||
self.log(f"TAKE PROFIT (Long): FB {current_fb:.2f} Peak Reached")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 原版硬止损:只看 entry_price,给趋势震荡留出无限空间
|
||||
if prev_bar.close < entry_price - stop_dist:
|
||||
self.log(f"HARD STOP (Long): Close {prev_bar.close} < Limit {entry_price - stop_dist}")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
elif pos < 0:
|
||||
# A. 动能极值止盈
|
||||
if current_fb < -self.fisher_exit_level and current_fb > prev_fb:
|
||||
self.log(f"TAKE PROFIT (Short): FB {current_fb:.2f} Bottom Reached")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 原版硬止损
|
||||
if prev_bar.close > entry_price + stop_dist:
|
||||
self.log(f"HARD STOP (Short): Close {prev_bar.close} > Limit {entry_price + stop_dist}")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# ==========================================
|
||||
# 核心二:经典入场逻辑
|
||||
# ==========================================
|
||||
elif pos == 0 and is_met:
|
||||
# 趋势向上,且 FB < -0.1 (符合你的优异参数)
|
||||
if prev_bar.close > T and current_fb < -self.fb_entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
self.log(f"ENTRY LONG SIGNAL: T={T:.2f}, FB={current_fb:.2f}")
|
||||
|
||||
# 趋势向下,且 FB > 0.1
|
||||
elif prev_bar.close < T and current_fb > self.fb_entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
self.log(f"ENTRY SHORT SIGNAL: T={T:.2f}, FB={current_fb:.2f}")
|
||||
|
||||
# ==========================================
|
||||
# 彻底隔离历史噪音:原生换月机制
|
||||
# ==========================================
|
||||
def on_contract_rollover(self, old_symbol: str, new_symbol: str):
|
||||
self.log(f"ROLLOVER TRIGGERED: {old_symbol} -> {new_symbol}")
|
||||
|
||||
# 撤销老合约全部残留动作
|
||||
self.cancel_all_pending_orders(old_symbol)
|
||||
|
||||
# 市价清空老合约,因为换月会导致 entry_price 和盘面断层,强制物理归零最安全
|
||||
pos = self.get_current_positions().get(old_symbol, 0)
|
||||
if pos > 0:
|
||||
self.send_order(Order(id=f"ROLLOVER_CLOSE_L_{old_symbol}", symbol=old_symbol, direction="CLOSE_LONG",
|
||||
volume=abs(pos), price_type="MARKET", submitted_time=self.get_current_time(),
|
||||
offset="CLOSE"))
|
||||
elif pos < 0:
|
||||
self.send_order(Order(id=f"ROLLOVER_CLOSE_S_{old_symbol}", symbol=old_symbol, direction="CLOSE_SHORT",
|
||||
volume=abs(pos), price_type="MARKET", submitted_time=self.get_current_time(),
|
||||
offset="CLOSE"))
|
||||
|
||||
# 切换主交易标的
|
||||
self.main_symbol = new_symbol
|
||||
self.symbol = new_symbol
|
||||
|
||||
# --- 下单系统 ---
|
||||
def send_market_order(self, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_MKT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||||
submitted_time=self.get_current_time(), offset=offset))
|
||||
|
||||
def send_limit_order(self, price, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_LMT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=price))
|
||||
1253
futures_trading_strategies/FG/FisherTrendStrategy/Strategy.ipynb
Normal file
1253
futures_trading_strategies/FG/FisherTrendStrategy/Strategy.ipynb
Normal file
File diff suppressed because one or more lines are too long
1393
futures_trading_strategies/FG/FisherTrendStrategy/Strategy2.ipynb
Normal file
1393
futures_trading_strategies/FG/FisherTrendStrategy/Strategy2.ipynb
Normal file
File diff suppressed because one or more lines are too long
1536
futures_trading_strategies/FG/FisherTrendStrategy/Strategy3.ipynb
Normal file
1536
futures_trading_strategies/FG/FisherTrendStrategy/Strategy3.ipynb
Normal file
File diff suppressed because one or more lines are too long
239
futures_trading_strategies/FG/FisherTrendStrategy/Strategy3.py
Normal file
239
futures_trading_strategies/FG/FisherTrendStrategy/Strategy3.py
Normal file
@@ -0,0 +1,239 @@
|
||||
import numpy as np
|
||||
import math
|
||||
import talib
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class PragmaticCyberneticStrategy(Strategy):
|
||||
"""
|
||||
【务实回归版·控制论策略】(TA-Lib 提速 + 换月支持)
|
||||
|
||||
哲学坚守:
|
||||
1. 捍卫原版出场逻辑:坚决使用基于 entry_price 的固定硬止损。脱离成本区后,给予价格无限宽容度,实现长线死拿。
|
||||
2. 动能极值止盈:恢复 FB > 1.6 且拐头时的精准落袋为安。
|
||||
3. 拒绝画蛇添足:没有任何 _target_state,不堆砌 Trick,底层数据即真理。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 核心周期 ---
|
||||
trend_period: int = 26,
|
||||
fisher_period: int = 20,
|
||||
atr_period: int = 23,
|
||||
|
||||
# --- 经过验证的优秀参数 ---
|
||||
fisher_exit_level: float = 1.6, # 配合 1.98 修复后的 FB,1.6 是一个非常好的极值止盈点
|
||||
fb_entry_threshold: float = 0.1, # 0.1 确保在顺大势的前提下,动能稍有配合即可入场
|
||||
|
||||
# --- 风险控制 ---
|
||||
stop_mult: float = 2.0, # 原始的硬止损乘数
|
||||
limit_offset_mult: float = 0.,
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
|
||||
self.fisher_exit_level = fisher_exit_level
|
||||
self.fb_entry_threshold = fb_entry_threshold
|
||||
self.stop_mult = stop_mult
|
||||
self.limit_offset_mult = limit_offset_mult
|
||||
|
||||
self.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.indicator = indicator
|
||||
|
||||
# 为 talib.ATR 预留充足的数据预热空间
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len * 2) + 100
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
# 使用 deque 记录 FB,相比单独的 _prev_fb 变量更安全,不会因为初始 0 导致误判
|
||||
self._fbs = deque(maxlen=2)
|
||||
self.order_id_counter = 0
|
||||
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def _calculate_indicators(self):
|
||||
"""完全无状态的纯数学指标计算 (基于 TA-Lib)"""
|
||||
if len(self._closes) < self._buf_len:
|
||||
return None, None, None, None
|
||||
|
||||
np_highs = np.array(self._highs, dtype=np.float64)
|
||||
np_lows = np.array(self._lows, dtype=np.float64)
|
||||
np_closes = np.array(self._closes, dtype=np.float64)
|
||||
|
||||
# 1. T轴 (大势过滤)
|
||||
t_max_h = talib.MAX(np_highs, timeperiod=self.t_len)[-1]
|
||||
t_min_l = talib.MIN(np_lows, timeperiod=self.t_len)[-1]
|
||||
T = (t_max_h + t_min_l) / 2.0
|
||||
|
||||
# 2. FV (公允价值挂单基准)
|
||||
FV = (np_closes[-1] + 2 * np_closes[-2] + 2 * np_closes[-3] + np_closes[-4]) / 6.0
|
||||
|
||||
# 3. Fisher Transform (1.98 数学修复版)
|
||||
f_max_h = talib.MAX(np_highs, timeperiod=self.f_len)[-1]
|
||||
f_min_l = talib.MIN(np_lows, timeperiod=self.f_len)[-1]
|
||||
denom = f_max_h - f_min_l if f_max_h != f_min_l else self.min_tick
|
||||
|
||||
stoc = (np_closes[-1] - f_min_l) / denom
|
||||
value = max(-0.999, min(0.999, 1.98 * (stoc - 0.5)))
|
||||
FB = 0.5 * math.log((1.0 + value) / (1.0 - value))
|
||||
|
||||
# 4. ATR
|
||||
atr_array = talib.ATR(np_highs, np_lows, np_closes, timeperiod=self.atr_len)
|
||||
ATR = atr_array[-1]
|
||||
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
if T is None or math.isnan(ATR): return
|
||||
|
||||
self._fbs.append(FB)
|
||||
# 必须凑齐两根 FB 才能判断拐点,否则跳过
|
||||
if len(self._fbs) < 2: return
|
||||
|
||||
current_fb = self._fbs[-1]
|
||||
prev_fb = self._fbs[-2]
|
||||
|
||||
# 绝对无状态:从账户物理数据获取真实持仓和开仓均价
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
# ==========================================
|
||||
# 核心一:经典出场逻辑 (你的原版设计)
|
||||
# ==========================================
|
||||
if pos != 0:
|
||||
stop_dist = max(self.stop_mult * ATR, self.min_tick * 20)
|
||||
|
||||
# --- 预判:是否存在反向入场信号? ---
|
||||
# 反向做多信号:大势已多 (C > T),且动能回调 (FB < -0.1)
|
||||
is_long_signal = (prev_bar.close > T) and (current_fb < -self.fb_entry_threshold)
|
||||
# 反向做空信号:大势已空 (C < T),且动能反弹 (FB > 0.1)
|
||||
is_short_signal = (prev_bar.close < T) and (current_fb > self.fb_entry_threshold)
|
||||
|
||||
if pos > 0:
|
||||
# A. 动能极值止盈
|
||||
if current_fb > self.fisher_exit_level and current_fb < prev_fb:
|
||||
self.log(f"TAKE PROFIT (Long): FB {current_fb:.2f} Peak Reached")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 原版硬止损 (最后的底线)
|
||||
if prev_bar.close < entry_price - stop_dist:
|
||||
self.log(f"HARD STOP (Long): Close {prev_bar.close} < Limit {entry_price - stop_dist}")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# C. 解决冲突:反向信号强制平仓
|
||||
# 如果我们持有多单,但盘面竟然发出了“做空”信号,说明大势已去,逻辑矛盾,必须平仓!
|
||||
if is_short_signal and is_met:
|
||||
self.log(f"REVERSAL SIGNAL EXIT (Long): Trend Down & Short Signal Triggered.")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
elif pos < 0:
|
||||
# A. 动能极值止盈
|
||||
if current_fb < -self.fisher_exit_level and current_fb > prev_fb:
|
||||
self.log(f"TAKE PROFIT (Short): FB {current_fb:.2f} Bottom Reached")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 原版硬止损
|
||||
if prev_bar.close > entry_price + stop_dist:
|
||||
self.log(f"HARD STOP (Short): Close {prev_bar.close} > Limit {entry_price + stop_dist}")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# C. 解决冲突:反向信号强制平仓
|
||||
# 如果我们持有空单,但盘面发出了“做多”信号,平空!
|
||||
if is_long_signal and is_met:
|
||||
self.log(f"REVERSAL SIGNAL EXIT (Short): Trend Up & Long Signal Triggered.")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# ==========================================
|
||||
# 核心二:经典入场逻辑 (完全不变)
|
||||
# ==========================================
|
||||
elif pos == 0 and is_met:
|
||||
if prev_bar.close > T and current_fb < -self.fb_entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
|
||||
elif prev_bar.close < T and current_fb > self.fb_entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
|
||||
# ==========================================
|
||||
# 彻底隔离历史噪音:原生换月机制
|
||||
# ==========================================
|
||||
def on_contract_rollover(self, old_symbol: str, new_symbol: str):
|
||||
self.log(f"ROLLOVER TRIGGERED: {old_symbol} -> {new_symbol}")
|
||||
|
||||
# 撤销老合约全部残留动作
|
||||
self.cancel_all_pending_orders(old_symbol)
|
||||
|
||||
# 市价清空老合约,因为换月会导致 entry_price 和盘面断层,强制物理归零最安全
|
||||
pos = self.get_current_positions().get(old_symbol, 0)
|
||||
if pos > 0:
|
||||
self.send_order(Order(id=f"ROLLOVER_CLOSE_L_{old_symbol}", symbol=old_symbol, direction="CLOSE_LONG",
|
||||
volume=abs(pos), price_type="MARKET", submitted_time=self.get_current_time(),
|
||||
offset="CLOSE"))
|
||||
elif pos < 0:
|
||||
self.send_order(Order(id=f"ROLLOVER_CLOSE_S_{old_symbol}", symbol=old_symbol, direction="CLOSE_SHORT",
|
||||
volume=abs(pos), price_type="MARKET", submitted_time=self.get_current_time(),
|
||||
offset="CLOSE"))
|
||||
|
||||
# 切换主交易标的
|
||||
self.main_symbol = new_symbol
|
||||
self.symbol = new_symbol
|
||||
|
||||
# --- 下单系统 ---
|
||||
def send_market_order(self, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_MKT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||||
submitted_time=self.get_current_time(), offset=offset))
|
||||
|
||||
def send_limit_order(self, price, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_LMT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=price))
|
||||
File diff suppressed because one or more lines are too long
216
src/strategies/FisherTrendStrategy/FisherTrendStrategy.py
Normal file
216
src/strategies/FisherTrendStrategy/FisherTrendStrategy.py
Normal file
@@ -0,0 +1,216 @@
|
||||
import numpy as np
|
||||
import math
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List, Dict
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class PragmaticCyberneticStrategy(Strategy):
|
||||
"""
|
||||
【务实优化版·控制论策略】
|
||||
|
||||
优化核心:
|
||||
1. 保持“钝感”:拒绝灵敏的趋势反转平仓,保留“死拿趋势”的盈利特性。
|
||||
2. 修复系数:将 Stoch 映射系数从 0.66 提升至 1.98,使 Fisher 值域恢复到 [-3, 3] 可调范围。
|
||||
- 现在你可以设置 exit_level = 2.0 来捕捉极端利润,而不是被迫等到换月。
|
||||
3. 纯粹逻辑:开仓是开仓,平仓是平仓。互不干扰,消除中间地带的内耗。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 核心参数 (保持原策略风格) ---
|
||||
trend_period: int = 26, # T: 趋势中轴
|
||||
fisher_period: int = 20, # FB: 动能周期 (原策略 46 可能太慢,建议 20-30)
|
||||
atr_period: int = 23,
|
||||
|
||||
# --- 阈值参数 (关键调整) ---
|
||||
# 1. 止盈阈值:因为系数修复了,现在 FB 能跑到 2.5 以上。
|
||||
# 建议设为 2.0 - 2.5。如果设得很高(如 5.0),效果就等于你原来的“死拿”。
|
||||
fisher_exit_level: float = 2.2,
|
||||
|
||||
# 2. 入场阈值:保持在 0.5 左右,只接深回调
|
||||
fb_entry_threshold: float = 0.,
|
||||
|
||||
stop_mult: float = 2, # 稍微放宽止损,适应趋势震荡
|
||||
limit_offset_mult: float = 0.2, # FV 挂单偏移
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
|
||||
self.fisher_exit_level = fisher_exit_level
|
||||
self.fb_entry_threshold = fb_entry_threshold
|
||||
self.stop_mult = stop_mult
|
||||
self.limit_offset_mult = limit_offset_mult
|
||||
|
||||
self.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.indicator = indicator
|
||||
|
||||
# 缓存
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len) + 5
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
# 记录上一根 Bar 的 FB,仅用于判断拐点
|
||||
self._prev_fb = 0.0
|
||||
self.order_id_counter = 0
|
||||
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def _calculate_indicators(self):
|
||||
"""
|
||||
计算逻辑:保留原策略的简洁性,仅修复 Scaling 系数
|
||||
"""
|
||||
if len(self._closes) < self._buf_len:
|
||||
return None, None, None, None
|
||||
|
||||
# 1. 趋势中轴 T (物理中轴)
|
||||
h_trend = list(self._highs)[-self.t_len:]
|
||||
l_trend = list(self._lows)[-self.t_len:]
|
||||
T = (max(h_trend) + min(l_trend)) / 2.0
|
||||
|
||||
# 2. 公平价 FV (FIR 滤波)
|
||||
FV = (self._closes[-1] + 2 * self._closes[-2] + 2 * self._closes[-3] + self._closes[-4]) / 6.0
|
||||
|
||||
# 3. Fisher Transform (非递归版,响应更快)
|
||||
h_fisher = list(self._highs)[-self.f_len:]
|
||||
l_fisher = list(self._lows)[-self.f_len:]
|
||||
max_h, min_l = max(h_fisher), min(l_fisher)
|
||||
denom = max_h - min_l if max_h != min_l else self.min_tick
|
||||
|
||||
# --- 关键修正点 ---
|
||||
# 你的原代码是 0.66 * (stoc - 0.5),这导致最大值被锁死。
|
||||
# 改为 1.98,使得输入范围从 [-0.33, 0.33] 扩大到 [-0.99, 0.99]。
|
||||
# 这样 math.log 就能计算出 -3 到 +3 的值,让止盈逻辑“复活”且可控。
|
||||
stoc = (self._closes[-1] - min_l) / denom
|
||||
value = max(-0.999, min(0.999, 1.98 * (stoc - 0.5)))
|
||||
FB = 0.5 * math.log((1.0 + value) / (1.0 - value))
|
||||
|
||||
# 4. ATR
|
||||
tr_list = []
|
||||
for i in range(1, self.atr_len + 1):
|
||||
h, l, pc = self._highs[-i], self._lows[-i], self._closes[-i - 1]
|
||||
tr = max(h - l, abs(h - pc), abs(l - pc))
|
||||
tr_list.append(tr)
|
||||
ATR = sum(tr_list) / self.atr_len
|
||||
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
# 每次 Bar 重置挂单,防止挂单“长在”图表上
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
if T is None: return
|
||||
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
# 状态定义
|
||||
# 这里我们不去定义 "short_signal" 这种会诱发反向平仓的变量
|
||||
# 而是只关注眼下的:大势(T) 和 动能(FB)
|
||||
trend_up = prev_bar.close > T
|
||||
trend_down = prev_bar.close < T
|
||||
|
||||
# ==========================================
|
||||
# 1. 持仓逻辑:简单、迟钝、粘性强
|
||||
# ==========================================
|
||||
if pos != 0:
|
||||
stop_dist = max(self.stop_mult * ATR, self.min_tick * 20)
|
||||
|
||||
if pos > 0:
|
||||
# A. 动能止盈 (Mean Reversion Exit)
|
||||
# 只有当行情极其疯狂(FB > 2.2) 且开始回头时才止盈。
|
||||
# 正常波动绝不下车。
|
||||
if FB > self.fisher_exit_level and FB < self._prev_fb:
|
||||
self.log(f"TAKE PROFIT (Long): FB {FB:.2f} Peak Reached")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 硬止损 (ATR Stop) - 最后的防线
|
||||
if prev_bar.close < entry_price - stop_dist:
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# C. (可选) 极端趋势反转保护
|
||||
# 如果你希望策略更像原来的“死拿”,这部分可以注释掉,或者把判定条件设严
|
||||
# if prev_bar.close < T - ATR: ...
|
||||
|
||||
elif pos < 0:
|
||||
# A. 动能止盈
|
||||
if FB < -self.fisher_exit_level and FB > self._prev_fb:
|
||||
self.log(f"TAKE PROFIT (Short): FB {FB:.2f} Bottom Reached")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 硬止损
|
||||
if prev_bar.close > entry_price + stop_dist:
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# ==========================================
|
||||
# 2. 开仓逻辑:严苛、左侧、顺大势
|
||||
# ==========================================
|
||||
if pos == 0:
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
# 挂单价格优化:
|
||||
# 在 FV 基础上再便宜一点点,增加胜率
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
|
||||
# 开多:趋势向上 + 动能深跌 (FB < -0.5)
|
||||
if trend_up and FB < -self.fb_entry_threshold and is_met:
|
||||
if "BUY" in self.order_direction:
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
|
||||
# 开空:趋势向下 + 动能冲高 (FB > 0.5)
|
||||
elif trend_down and FB > self.fb_entry_threshold and is_met:
|
||||
if "SELL" in self.order_direction:
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
|
||||
# 记录上一根FB,仅用于止盈时的拐点比较
|
||||
self._prev_fb = FB
|
||||
|
||||
# --- 辅助 ---
|
||||
def send_market_order(self, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_MKT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||||
submitted_time=self.get_current_time(), offset=offset))
|
||||
|
||||
def send_limit_order(self, price, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_LMT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=price))
|
||||
224
src/strategies/FisherTrendStrategy/FisherTrendStrategy2.py
Normal file
224
src/strategies/FisherTrendStrategy/FisherTrendStrategy2.py
Normal file
@@ -0,0 +1,224 @@
|
||||
import numpy as np
|
||||
import math
|
||||
import talib
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class PragmaticCyberneticStrategy(Strategy):
|
||||
"""
|
||||
【务实回归版·控制论策略】(TA-Lib 提速 + 换月支持)
|
||||
|
||||
哲学坚守:
|
||||
1. 捍卫原版出场逻辑:坚决使用基于 entry_price 的固定硬止损。脱离成本区后,给予价格无限宽容度,实现长线死拿。
|
||||
2. 动能极值止盈:恢复 FB > 1.6 且拐头时的精准落袋为安。
|
||||
3. 拒绝画蛇添足:没有任何 _target_state,不堆砌 Trick,底层数据即真理。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 核心周期 ---
|
||||
trend_period: int = 26,
|
||||
fisher_period: int = 20,
|
||||
atr_period: int = 23,
|
||||
|
||||
# --- 经过验证的优秀参数 ---
|
||||
fisher_exit_level: float = 1.6, # 配合 1.98 修复后的 FB,1.6 是一个非常好的极值止盈点
|
||||
fb_entry_threshold: float = 0.1, # 0.1 确保在顺大势的前提下,动能稍有配合即可入场
|
||||
|
||||
# --- 风险控制 ---
|
||||
stop_mult: float = 2.0, # 原始的硬止损乘数
|
||||
limit_offset_mult: float = 0.,
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
|
||||
self.fisher_exit_level = fisher_exit_level
|
||||
self.fb_entry_threshold = fb_entry_threshold
|
||||
self.stop_mult = stop_mult
|
||||
self.limit_offset_mult = limit_offset_mult
|
||||
|
||||
self.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.indicator = indicator
|
||||
|
||||
# 为 talib.ATR 预留充足的数据预热空间
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len * 2) + 100
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
# 使用 deque 记录 FB,相比单独的 _prev_fb 变量更安全,不会因为初始 0 导致误判
|
||||
self._fbs = deque(maxlen=2)
|
||||
self.order_id_counter = 0
|
||||
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def _calculate_indicators(self):
|
||||
"""完全无状态的纯数学指标计算 (基于 TA-Lib)"""
|
||||
if len(self._closes) < self._buf_len:
|
||||
return None, None, None, None
|
||||
|
||||
np_highs = np.array(self._highs, dtype=np.float64)
|
||||
np_lows = np.array(self._lows, dtype=np.float64)
|
||||
np_closes = np.array(self._closes, dtype=np.float64)
|
||||
|
||||
# 1. T轴 (大势过滤)
|
||||
t_max_h = talib.MAX(np_highs, timeperiod=self.t_len)[-1]
|
||||
t_min_l = talib.MIN(np_lows, timeperiod=self.t_len)[-1]
|
||||
T = (t_max_h + t_min_l) / 2.0
|
||||
|
||||
# 2. FV (公允价值挂单基准)
|
||||
FV = (np_closes[-1] + 2 * np_closes[-2] + 2 * np_closes[-3] + np_closes[-4]) / 6.0
|
||||
|
||||
# 3. Fisher Transform (1.98 数学修复版)
|
||||
f_max_h = talib.MAX(np_highs, timeperiod=self.f_len)[-1]
|
||||
f_min_l = talib.MIN(np_lows, timeperiod=self.f_len)[-1]
|
||||
denom = f_max_h - f_min_l if f_max_h != f_min_l else self.min_tick
|
||||
|
||||
stoc = (np_closes[-1] - f_min_l) / denom
|
||||
value = max(-0.999, min(0.999, 1.98 * (stoc - 0.5)))
|
||||
FB = 0.5 * math.log((1.0 + value) / (1.0 - value))
|
||||
|
||||
# 4. ATR
|
||||
atr_array = talib.ATR(np_highs, np_lows, np_closes, timeperiod=self.atr_len)
|
||||
ATR = atr_array[-1]
|
||||
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
if T is None or math.isnan(ATR): return
|
||||
|
||||
self._fbs.append(FB)
|
||||
# 必须凑齐两根 FB 才能判断拐点,否则跳过
|
||||
if len(self._fbs) < 2: return
|
||||
|
||||
current_fb = self._fbs[-1]
|
||||
prev_fb = self._fbs[-2]
|
||||
|
||||
# 绝对无状态:从账户物理数据获取真实持仓和开仓均价
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
# ==========================================
|
||||
# 核心一:经典出场逻辑 (你的原版设计)
|
||||
# ==========================================
|
||||
if pos != 0:
|
||||
# 最小 20 tick 保护,防止 ATR 极度萎缩时止损过窄
|
||||
stop_dist = max(self.stop_mult * ATR, self.min_tick * 20)
|
||||
|
||||
if pos > 0:
|
||||
# A. 动能极值止盈:到达 1.6 且刚刚发生向下拐头
|
||||
if current_fb > self.fisher_exit_level and current_fb < prev_fb:
|
||||
self.log(f"TAKE PROFIT (Long): FB {current_fb:.2f} Peak Reached")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 原版硬止损:只看 entry_price,给趋势震荡留出无限空间
|
||||
if prev_bar.close < entry_price - stop_dist:
|
||||
self.log(f"HARD STOP (Long): Close {prev_bar.close} < Limit {entry_price - stop_dist}")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
elif pos < 0:
|
||||
# A. 动能极值止盈
|
||||
if current_fb < -self.fisher_exit_level and current_fb > prev_fb:
|
||||
self.log(f"TAKE PROFIT (Short): FB {current_fb:.2f} Bottom Reached")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 原版硬止损
|
||||
if prev_bar.close > entry_price + stop_dist:
|
||||
self.log(f"HARD STOP (Short): Close {prev_bar.close} > Limit {entry_price + stop_dist}")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# ==========================================
|
||||
# 核心二:经典入场逻辑
|
||||
# ==========================================
|
||||
elif pos == 0 and is_met:
|
||||
# 趋势向上,且 FB < -0.1 (符合你的优异参数)
|
||||
if prev_bar.close > T and current_fb < -self.fb_entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
self.log(f"ENTRY LONG SIGNAL: T={T:.2f}, FB={current_fb:.2f}")
|
||||
|
||||
# 趋势向下,且 FB > 0.1
|
||||
elif prev_bar.close < T and current_fb > self.fb_entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
self.log(f"ENTRY SHORT SIGNAL: T={T:.2f}, FB={current_fb:.2f}")
|
||||
|
||||
# ==========================================
|
||||
# 彻底隔离历史噪音:原生换月机制
|
||||
# ==========================================
|
||||
def on_contract_rollover(self, old_symbol: str, new_symbol: str):
|
||||
self.log(f"ROLLOVER TRIGGERED: {old_symbol} -> {new_symbol}")
|
||||
|
||||
# 撤销老合约全部残留动作
|
||||
self.cancel_all_pending_orders(old_symbol)
|
||||
|
||||
# 市价清空老合约,因为换月会导致 entry_price 和盘面断层,强制物理归零最安全
|
||||
pos = self.get_current_positions().get(old_symbol, 0)
|
||||
if pos > 0:
|
||||
self.send_order(Order(id=f"ROLLOVER_CLOSE_L_{old_symbol}", symbol=old_symbol, direction="CLOSE_LONG",
|
||||
volume=abs(pos), price_type="MARKET", submitted_time=self.get_current_time(),
|
||||
offset="CLOSE"))
|
||||
elif pos < 0:
|
||||
self.send_order(Order(id=f"ROLLOVER_CLOSE_S_{old_symbol}", symbol=old_symbol, direction="CLOSE_SHORT",
|
||||
volume=abs(pos), price_type="MARKET", submitted_time=self.get_current_time(),
|
||||
offset="CLOSE"))
|
||||
|
||||
# 切换主交易标的
|
||||
self.main_symbol = new_symbol
|
||||
self.symbol = new_symbol
|
||||
|
||||
# --- 下单系统 ---
|
||||
def send_market_order(self, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_MKT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||||
submitted_time=self.get_current_time(), offset=offset))
|
||||
|
||||
def send_limit_order(self, price, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_LMT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=price))
|
||||
1253
src/strategies/FisherTrendStrategy/Strategy.ipynb
Normal file
1253
src/strategies/FisherTrendStrategy/Strategy.ipynb
Normal file
File diff suppressed because one or more lines are too long
1393
src/strategies/FisherTrendStrategy/Strategy2.ipynb
Normal file
1393
src/strategies/FisherTrendStrategy/Strategy2.ipynb
Normal file
File diff suppressed because one or more lines are too long
1536
src/strategies/FisherTrendStrategy/Strategy3.ipynb
Normal file
1536
src/strategies/FisherTrendStrategy/Strategy3.ipynb
Normal file
File diff suppressed because one or more lines are too long
239
src/strategies/FisherTrendStrategy/Strategy3.py
Normal file
239
src/strategies/FisherTrendStrategy/Strategy3.py
Normal file
@@ -0,0 +1,239 @@
|
||||
import numpy as np
|
||||
import math
|
||||
import talib
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class PragmaticCyberneticStrategy(Strategy):
|
||||
"""
|
||||
【务实回归版·控制论策略】(TA-Lib 提速 + 换月支持)
|
||||
|
||||
哲学坚守:
|
||||
1. 捍卫原版出场逻辑:坚决使用基于 entry_price 的固定硬止损。脱离成本区后,给予价格无限宽容度,实现长线死拿。
|
||||
2. 动能极值止盈:恢复 FB > 1.6 且拐头时的精准落袋为安。
|
||||
3. 拒绝画蛇添足:没有任何 _target_state,不堆砌 Trick,底层数据即真理。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 核心周期 ---
|
||||
trend_period: int = 26,
|
||||
fisher_period: int = 20,
|
||||
atr_period: int = 23,
|
||||
|
||||
# --- 经过验证的优秀参数 ---
|
||||
fisher_exit_level: float = 1.6, # 配合 1.98 修复后的 FB,1.6 是一个非常好的极值止盈点
|
||||
fb_entry_threshold: float = 0.1, # 0.1 确保在顺大势的前提下,动能稍有配合即可入场
|
||||
|
||||
# --- 风险控制 ---
|
||||
stop_mult: float = 2.0, # 原始的硬止损乘数
|
||||
limit_offset_mult: float = 0.,
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
|
||||
self.fisher_exit_level = fisher_exit_level
|
||||
self.fb_entry_threshold = fb_entry_threshold
|
||||
self.stop_mult = stop_mult
|
||||
self.limit_offset_mult = limit_offset_mult
|
||||
|
||||
self.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.indicator = indicator
|
||||
|
||||
# 为 talib.ATR 预留充足的数据预热空间
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len * 2) + 100
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
# 使用 deque 记录 FB,相比单独的 _prev_fb 变量更安全,不会因为初始 0 导致误判
|
||||
self._fbs = deque(maxlen=2)
|
||||
self.order_id_counter = 0
|
||||
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def _calculate_indicators(self):
|
||||
"""完全无状态的纯数学指标计算 (基于 TA-Lib)"""
|
||||
if len(self._closes) < self._buf_len:
|
||||
return None, None, None, None
|
||||
|
||||
np_highs = np.array(self._highs, dtype=np.float64)
|
||||
np_lows = np.array(self._lows, dtype=np.float64)
|
||||
np_closes = np.array(self._closes, dtype=np.float64)
|
||||
|
||||
# 1. T轴 (大势过滤)
|
||||
t_max_h = talib.MAX(np_highs, timeperiod=self.t_len)[-1]
|
||||
t_min_l = talib.MIN(np_lows, timeperiod=self.t_len)[-1]
|
||||
T = (t_max_h + t_min_l) / 2.0
|
||||
|
||||
# 2. FV (公允价值挂单基准)
|
||||
FV = (np_closes[-1] + 2 * np_closes[-2] + 2 * np_closes[-3] + np_closes[-4]) / 6.0
|
||||
|
||||
# 3. Fisher Transform (1.98 数学修复版)
|
||||
f_max_h = talib.MAX(np_highs, timeperiod=self.f_len)[-1]
|
||||
f_min_l = talib.MIN(np_lows, timeperiod=self.f_len)[-1]
|
||||
denom = f_max_h - f_min_l if f_max_h != f_min_l else self.min_tick
|
||||
|
||||
stoc = (np_closes[-1] - f_min_l) / denom
|
||||
value = max(-0.999, min(0.999, 1.98 * (stoc - 0.5)))
|
||||
FB = 0.5 * math.log((1.0 + value) / (1.0 - value))
|
||||
|
||||
# 4. ATR
|
||||
atr_array = talib.ATR(np_highs, np_lows, np_closes, timeperiod=self.atr_len)
|
||||
ATR = atr_array[-1]
|
||||
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
if T is None or math.isnan(ATR): return
|
||||
|
||||
self._fbs.append(FB)
|
||||
# 必须凑齐两根 FB 才能判断拐点,否则跳过
|
||||
if len(self._fbs) < 2: return
|
||||
|
||||
current_fb = self._fbs[-1]
|
||||
prev_fb = self._fbs[-2]
|
||||
|
||||
# 绝对无状态:从账户物理数据获取真实持仓和开仓均价
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
# ==========================================
|
||||
# 核心一:经典出场逻辑 (你的原版设计)
|
||||
# ==========================================
|
||||
if pos != 0:
|
||||
stop_dist = max(self.stop_mult * ATR, self.min_tick * 20)
|
||||
|
||||
# --- 预判:是否存在反向入场信号? ---
|
||||
# 反向做多信号:大势已多 (C > T),且动能回调 (FB < -0.1)
|
||||
is_long_signal = (prev_bar.close > T) and (current_fb < -self.fb_entry_threshold)
|
||||
# 反向做空信号:大势已空 (C < T),且动能反弹 (FB > 0.1)
|
||||
is_short_signal = (prev_bar.close < T) and (current_fb > self.fb_entry_threshold)
|
||||
|
||||
if pos > 0:
|
||||
# A. 动能极值止盈
|
||||
if current_fb > self.fisher_exit_level and current_fb < prev_fb:
|
||||
self.log(f"TAKE PROFIT (Long): FB {current_fb:.2f} Peak Reached")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 原版硬止损 (最后的底线)
|
||||
if prev_bar.close < entry_price - stop_dist:
|
||||
self.log(f"HARD STOP (Long): Close {prev_bar.close} < Limit {entry_price - stop_dist}")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# C. 解决冲突:反向信号强制平仓
|
||||
# 如果我们持有多单,但盘面竟然发出了“做空”信号,说明大势已去,逻辑矛盾,必须平仓!
|
||||
if is_short_signal and is_met:
|
||||
self.log(f"REVERSAL SIGNAL EXIT (Long): Trend Down & Short Signal Triggered.")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
elif pos < 0:
|
||||
# A. 动能极值止盈
|
||||
if current_fb < -self.fisher_exit_level and current_fb > prev_fb:
|
||||
self.log(f"TAKE PROFIT (Short): FB {current_fb:.2f} Bottom Reached")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# B. 原版硬止损
|
||||
if prev_bar.close > entry_price + stop_dist:
|
||||
self.log(f"HARD STOP (Short): Close {prev_bar.close} > Limit {entry_price + stop_dist}")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# C. 解决冲突:反向信号强制平仓
|
||||
# 如果我们持有空单,但盘面发出了“做多”信号,平空!
|
||||
if is_long_signal and is_met:
|
||||
self.log(f"REVERSAL SIGNAL EXIT (Short): Trend Up & Long Signal Triggered.")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# ==========================================
|
||||
# 核心二:经典入场逻辑 (完全不变)
|
||||
# ==========================================
|
||||
elif pos == 0 and is_met:
|
||||
if prev_bar.close > T and current_fb < -self.fb_entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
|
||||
elif prev_bar.close < T and current_fb > self.fb_entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
|
||||
# ==========================================
|
||||
# 彻底隔离历史噪音:原生换月机制
|
||||
# ==========================================
|
||||
def on_contract_rollover(self, old_symbol: str, new_symbol: str):
|
||||
self.log(f"ROLLOVER TRIGGERED: {old_symbol} -> {new_symbol}")
|
||||
|
||||
# 撤销老合约全部残留动作
|
||||
self.cancel_all_pending_orders(old_symbol)
|
||||
|
||||
# 市价清空老合约,因为换月会导致 entry_price 和盘面断层,强制物理归零最安全
|
||||
pos = self.get_current_positions().get(old_symbol, 0)
|
||||
if pos > 0:
|
||||
self.send_order(Order(id=f"ROLLOVER_CLOSE_L_{old_symbol}", symbol=old_symbol, direction="CLOSE_LONG",
|
||||
volume=abs(pos), price_type="MARKET", submitted_time=self.get_current_time(),
|
||||
offset="CLOSE"))
|
||||
elif pos < 0:
|
||||
self.send_order(Order(id=f"ROLLOVER_CLOSE_S_{old_symbol}", symbol=old_symbol, direction="CLOSE_SHORT",
|
||||
volume=abs(pos), price_type="MARKET", submitted_time=self.get_current_time(),
|
||||
offset="CLOSE"))
|
||||
|
||||
# 切换主交易标的
|
||||
self.main_symbol = new_symbol
|
||||
self.symbol = new_symbol
|
||||
|
||||
# --- 下单系统 ---
|
||||
def send_market_order(self, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_MKT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||||
submitted_time=self.get_current_time(), offset=offset))
|
||||
|
||||
def send_limit_order(self, price, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_LMT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=price))
|
||||
432
src/strategies/FisherTrendStrategy/search_Strategy.ipynb
Normal file
432
src/strategies/FisherTrendStrategy/search_Strategy.ipynb
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -33,7 +33,7 @@ class ITrendStrategy(Strategy):
|
||||
min_tick: float = 1.0, # 核心:必须根据品种设置 (例如 0.2, 1.0, 5.0)
|
||||
|
||||
# --- 【策略参数】 ---
|
||||
length: int = 20, # 趋势线周期
|
||||
length: int = 46, # 趋势线周期
|
||||
range_fraction: float = 0.35, # 入场回调系数 (Range的比例)
|
||||
stop_loss_ticks: int = 30, # 【新】硬止损跳数 (替代百分比)
|
||||
|
||||
@@ -182,7 +182,10 @@ class ITrendStrategy(Strategy):
|
||||
|
||||
# 计算回调距离 (Range Fraction)
|
||||
# 例如 Range=10, Frac=0.35 -> 3.5 -> 对齐到tick
|
||||
raw_offset = self.rng_frac
|
||||
if self.rng_frac > 1:
|
||||
raw_offset = self.rng_frac
|
||||
else:
|
||||
raw_offset = prev_range * self.rng_frac
|
||||
offset_price = self.round_to_tick(raw_offset)
|
||||
|
||||
self.log(f'RANDOM OFFSET: {offset_price}, is_bullish={is_bullish}, is_bearish={is_bearish}')
|
||||
|
||||
@@ -11,13 +11,11 @@ from src.strategies.base_strategy import Strategy
|
||||
|
||||
class ITrendStrategy(Strategy):
|
||||
"""
|
||||
【Ehlers 瞬时趋势线策略 (Stateless / Robust Version)】
|
||||
【Ehlers 瞬时趋势线策略 (反转/均值回归版)】
|
||||
|
||||
架构优化:
|
||||
1. 【无状态化】:彻底移除 save_state/load_state 和 position_meta。
|
||||
2. 【客观数据】:使用 get_average_position_price 获取真实持仓成本。
|
||||
3. 【详细日志】:每一根 Bar 打印详细的指标状态、持仓情况和止损计算。
|
||||
4. 【逻辑保持】:延续之前的持续挂单逻辑,但代码更简洁健壮。
|
||||
逻辑变更:
|
||||
1. 【反向开仓】:趋势金叉时在高位挂单做空,死叉时在低位挂单做多。
|
||||
2. 【Range计算修正】:修复了原代码中Offset未乘以Range的Bug。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -31,7 +29,7 @@ class ITrendStrategy(Strategy):
|
||||
|
||||
# --- 【策略参数】 ---
|
||||
length: int = 20,
|
||||
range_fraction: float = 3.0, # 注意:这里现在代表“固定价格偏移量”(如3跳*Tick)或具体数值
|
||||
range_fraction: float = 0.35,
|
||||
stop_loss_ticks: int = 30,
|
||||
|
||||
# --- 【其他】 ---
|
||||
@@ -43,14 +41,14 @@ class ITrendStrategy(Strategy):
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
self.rng_frac = range_fraction # 这里应传入具体的偏移价格(如3.0)或在外部计算好
|
||||
self.rng_frac = range_fraction
|
||||
self.stop_ticks = stop_loss_ticks
|
||||
self.order_direction = order_direction
|
||||
self.alpha = 2.0 / (length + 1.0)
|
||||
|
||||
self.indicator = indicator
|
||||
|
||||
# 历史数据缓存 (Warm-up 重建用,无需保存)
|
||||
# 历史数据缓存
|
||||
self._mid_price_history = deque(maxlen=50)
|
||||
self._itrend_history = deque(maxlen=50)
|
||||
self._trigger_history = deque(maxlen=50)
|
||||
@@ -61,15 +59,13 @@ class ITrendStrategy(Strategy):
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
"""辅助函数:将价格对齐到最小变动价位"""
|
||||
if self.min_tick <= 0: return price
|
||||
# 使用 epsilon 防止浮点数精度问题
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def on_init(self):
|
||||
super().on_init()
|
||||
# 不再加载任何状态
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
self.log(f"合约换月: {old_symbol} -> {new_symbol},清空指标缓存。")
|
||||
self.log(f"合约换月: {old_symbol} -> {new_symbol},重置状态。")
|
||||
self._mid_price_history.clear()
|
||||
self._itrend_history.clear()
|
||||
self._trigger_history.clear()
|
||||
@@ -81,19 +77,16 @@ class ITrendStrategy(Strategy):
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bars = self.get_bar_history()
|
||||
|
||||
# 撤销上一根 Bar 的挂单 (持续刷新逻辑)
|
||||
if len(bars) < 2: return
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
|
||||
if len(bars) < 2: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
# --- 1. 指标计算 (完全基于历史数据重算) ---
|
||||
# --- 1. 指标计算 ---
|
||||
mid_price = (prev_bar.high + prev_bar.low) / 2.0
|
||||
self._mid_price_history.append(mid_price)
|
||||
self.bar_count += 1
|
||||
|
||||
# 预热检查
|
||||
if len(self._mid_price_history) < 3:
|
||||
self._itrend_history.append(mid_price)
|
||||
self._trigger_history.append(mid_price)
|
||||
@@ -119,127 +112,106 @@ class ITrendStrategy(Strategy):
|
||||
current_trigger = 2.0 * current_itrend - self._itrend_history[-3]
|
||||
self._trigger_history.append(current_trigger)
|
||||
|
||||
# --- 2. 状态获取 ---
|
||||
# --- 2. 交易决策 ---
|
||||
if len(self._trigger_history) < 2: return
|
||||
|
||||
curr_trig = self._trigger_history[-1]
|
||||
prev_trig = self._trigger_history[-2]
|
||||
curr_itrend = self._itrend_history[-1]
|
||||
prev_itrend = self._itrend_history[-2]
|
||||
|
||||
# 趋势状态判定
|
||||
is_trend_up = curr_trig > curr_itrend
|
||||
is_trend_down = curr_trig < curr_itrend
|
||||
trend_str = "BULL" if is_trend_up else ("BEAR" if is_trend_down else "NEUTRAL")
|
||||
# 信号定义保持不变:金叉/死叉
|
||||
is_bullish = (prev_trig <= prev_itrend) and (curr_trig > curr_itrend)
|
||||
is_bearish = (prev_trig >= prev_itrend) and (curr_trig < curr_itrend)
|
||||
|
||||
# 获取真实持仓状态
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
avg_price = 0.0
|
||||
if position_volume != 0:
|
||||
avg_price = self.get_average_position_price(self.symbol)
|
||||
# 处理可能的 None 返回 (虽然理论上不应发生)
|
||||
if avg_price is None:
|
||||
avg_price = 0.0
|
||||
|
||||
# --- 3. 详细日志打印 (核心优化) ---
|
||||
log_msg = (
|
||||
f"Trend: {trend_str} (Trig:{curr_trig:.2f} / IT:{curr_itrend:.2f}) | "
|
||||
f"Pos: {position_volume} @ {avg_price:.2f}"
|
||||
)
|
||||
# self.log(log_msg) # 如果日志太多可注释,建议保留关键信息
|
||||
|
||||
# --- 4. 交易决策 ---
|
||||
# 计算 Range (High - Low)
|
||||
prev_range = prev_bar.high - prev_bar.low
|
||||
|
||||
# === 分支 A: 持仓管理 (止损/平仓) ===
|
||||
if position_volume != 0:
|
||||
|
||||
# 计算止损价
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
# --- A1. 强制跳数止损 (Tick Stop) ---
|
||||
# 【注】:止损逻辑是基于持仓方向的,无需反转,保持原样
|
||||
stop_diff = self.stop_ticks * self.min_tick
|
||||
stop_price = 0.0
|
||||
|
||||
self.log(f'Holding {position_volume} position volume '
|
||||
f'is_bullish={is_bullish}, is_bearish={is_bearish} '
|
||||
f'entry_price={entry_price}, stop_diff={stop_diff}')
|
||||
|
||||
if position_volume > 0: # 多头
|
||||
stop_price = self.round_to_tick(avg_price - stop_diff)
|
||||
|
||||
# 打印详细持仓监控日志
|
||||
self.log(f"{log_msg} | StopLevel: {stop_price:.2f} | PnL State: Checking...")
|
||||
|
||||
# 止损检查
|
||||
stop_price = self.round_to_tick(entry_price - stop_diff)
|
||||
if prev_bar.close < stop_price:
|
||||
self.log(f"!!! LONG STOP LOSS !!! Close:{prev_bar.close} < Stop:{stop_price}")
|
||||
self.send_market_order("CLOSE_LONG", abs(position_volume), "CLOSE")
|
||||
return
|
||||
|
||||
# 趋势反转检查
|
||||
if is_trend_down:
|
||||
self.log(f"!!! TREND REVERSAL EXIT (Long) !!! Trigger < ITrend")
|
||||
self.log(f"LONG STOP TRIGGERED. Close:{prev_bar.close} < Stop:{stop_price}")
|
||||
self.send_market_order("CLOSE_LONG", abs(position_volume), "CLOSE")
|
||||
return
|
||||
|
||||
elif position_volume < 0: # 空头
|
||||
stop_price = self.round_to_tick(avg_price + stop_diff)
|
||||
|
||||
self.log(f"{log_msg} | StopLevel: {stop_price:.2f} | PnL State: Checking...")
|
||||
|
||||
# 止损检查
|
||||
stop_price = self.round_to_tick(entry_price + stop_diff)
|
||||
if prev_bar.close > stop_price:
|
||||
self.log(f"!!! SHORT STOP LOSS !!! Close:{prev_bar.close} > Stop:{stop_price}")
|
||||
self.log(f"SHORT STOP TRIGGERED. Close:{prev_bar.close} > Stop:{stop_price}")
|
||||
self.send_market_order("CLOSE_SHORT", abs(position_volume), "CLOSE")
|
||||
return
|
||||
|
||||
# 趋势反转检查
|
||||
if is_trend_up:
|
||||
self.log(f"!!! TREND REVERSAL EXIT (Short) !!! Trigger > ITrend")
|
||||
self.send_market_order("CLOSE_SHORT", abs(position_volume), "CLOSE")
|
||||
return
|
||||
# --- A2. 信号平仓 (逻辑反转) ---
|
||||
# 如果持有多头,但出现了金叉(is_bullish),由于新逻辑金叉是做空的信号,所以需要平多
|
||||
if position_volume > 0 and is_bullish:
|
||||
self.send_market_order("CLOSE_LONG", abs(position_volume), "CLOSE")
|
||||
return
|
||||
|
||||
# === 分支 B: 开仓管理 (Continuous Entry) ===
|
||||
# 如果持有空头,但出现了死叉(is_bearish),由于新逻辑死叉是做多的信号,所以需要平空
|
||||
if position_volume < 0 and is_bearish:
|
||||
self.send_market_order("CLOSE_SHORT", abs(position_volume), "CLOSE")
|
||||
return
|
||||
|
||||
# === 分支 B: 开仓管理 (逻辑反转) ===
|
||||
else: # position_volume == 0
|
||||
|
||||
# 持续挂单逻辑
|
||||
offset_price = self.round_to_tick(self.rng_frac)
|
||||
# 【修复】:计算回调距离 = 波动幅度(Range) * 系数
|
||||
if self.rng_frac > 1:
|
||||
raw_offset = self.rng_frac
|
||||
else:
|
||||
raw_offset = prev_range * self.rng_frac
|
||||
offset_price = self.round_to_tick(raw_offset)
|
||||
|
||||
# 多头挂单
|
||||
if is_trend_up and "BUY" in self.order_direction:
|
||||
if self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
limit_price = self.round_to_tick(open_price - offset_price)
|
||||
self.log(f'REVERSE ENTRY OFFSET: {offset_price}, is_bullish={is_bullish}, is_bearish={is_bearish}')
|
||||
|
||||
self.log(
|
||||
f"{log_msg} | Action: PLACING BUY LIMIT @ {limit_price:.2f} (Open:{open_price} - Off:{offset_price})")
|
||||
self.send_limit_order(limit_price, "BUY", self.trade_volume, "OPEN")
|
||||
# 【反转】:金叉 (is_bullish) -> 挂单做空 (SELL) at Close + Offset
|
||||
if is_bullish and "SELL" in self.order_direction and (
|
||||
self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())):
|
||||
# 挂单价 = Close + 回调点数 (高位空)
|
||||
limit_price = prev_bar.close + offset_price
|
||||
limit_price = self.round_to_tick(limit_price)
|
||||
|
||||
# 空头挂单
|
||||
elif is_trend_down and "SELL" in self.order_direction:
|
||||
if self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
limit_price = self.round_to_tick(open_price + offset_price)
|
||||
self.send_limit_order(limit_price, "SELL", self.trade_volume, "OPEN")
|
||||
self.log(
|
||||
f"Reverse Signal SELL (Bullish Cross). Close:{prev_bar.close} + Offset:{offset_price} = Limit:{limit_price}")
|
||||
|
||||
self.log(
|
||||
f"{log_msg} | Action: PLACING SELL LIMIT @ {limit_price:.2f} (Open:{open_price} + Off:{offset_price})")
|
||||
self.send_limit_order(limit_price, "SELL", self.trade_volume, "OPEN")
|
||||
# 【反转】:死叉 (is_bearish) -> 挂单做多 (BUY) at Close - Offset
|
||||
elif is_bearish and "BUY" in self.order_direction and (
|
||||
self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())):
|
||||
# 挂单价 = Close - 回调点数 (低位多)
|
||||
limit_price = prev_bar.close - offset_price
|
||||
limit_price = self.round_to_tick(limit_price)
|
||||
|
||||
# --- 简化后的交易辅助函数 (无 meta/state) ---
|
||||
self.send_limit_order(limit_price, "BUY", self.trade_volume, "OPEN")
|
||||
self.log(
|
||||
f"Reverse Signal BUY (Bearish Cross). Close:{prev_bar.close} - Offset:{offset_price} = Limit:{limit_price}")
|
||||
|
||||
# --- 交易辅助函数 (保持不变) ---
|
||||
def send_market_order(self, direction: str, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_MKT_{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
|
||||
)
|
||||
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 send_limit_order(self, limit_price: float, direction: str, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_LMT_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction=direction,
|
||||
volume=volume,
|
||||
price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(),
|
||||
offset=offset,
|
||||
limit_price=limit_price
|
||||
)
|
||||
order = Order(id=order_id, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=limit_price)
|
||||
self.send_order(order)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,22 +5,18 @@ from typing import Optional, Any, List, Dict
|
||||
|
||||
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 InstantaneousTrendStrategy(Strategy):
|
||||
class PragmaticCyberneticStrategy(Strategy):
|
||||
"""
|
||||
【Ehlers 瞬时趋势线策略 (期货实战版)】
|
||||
【务实优化版·控制论策略】
|
||||
|
||||
针对期货日内交易优化:
|
||||
1. 【跳数止损】:使用固定的 min_tick 跳数作为止损,替代百分比。
|
||||
2. 【价格对齐】:所有计算出的挂单价格,强制对齐到 min_tick 的整数倍。
|
||||
3. 【无反手】:止损或信号反转仅平仓。
|
||||
|
||||
参数含义变更:
|
||||
- min_tick: 合约最小变动价位 (如 IF为0.2, RB为1)
|
||||
- stop_loss_ticks: 止损跳数 (如 50跳)
|
||||
优化核心:
|
||||
1. 保持“钝感”:拒绝灵敏的趋势反转平仓,保留“死拿趋势”的盈利特性。
|
||||
2. 修复系数:将 Stoch 映射系数从 0.66 提升至 1.98,使 Fisher 值域恢复到 [-3, 3] 可调范围。
|
||||
- 现在你可以设置 exit_level = 2.0 来捕捉极端利润,而不是被迫等到换月。
|
||||
3. 纯粹逻辑:开仓是开仓,平仓是平仓。互不干扰,消除中间地带的内耗。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -29,206 +25,192 @@ class InstantaneousTrendStrategy(Strategy):
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 【合约规格参数】 ---
|
||||
min_tick: float = 1.0, # 核心:必须根据品种设置 (例如 0.2, 1.0, 5.0)
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 【策略参数】 ---
|
||||
length: int = 20, # 趋势线周期
|
||||
range_fraction: float = 0.35, # 入场回调系数 (Range的比例)
|
||||
stop_loss_ticks: int = 30, # 【新】硬止损跳数 (替代百分比)
|
||||
# --- 核心参数 (保持原策略风格) ---
|
||||
trend_period: int = 26, # T: 趋势中轴
|
||||
fisher_period: int = 20, # FB: 动能周期 (原策略 46 可能太慢,建议 20-30)
|
||||
atr_period: int = 23,
|
||||
|
||||
# --- 阈值参数 (关键调整) ---
|
||||
# 1. 止盈阈值:因为系数修复了,现在 FB 能跑到 2.5 以上。
|
||||
# 建议设为 2.0 - 2.5。如果设得很高(如 5.0),效果就等于你原来的“死拿”。
|
||||
fisher_exit_level: float = 2.2,
|
||||
|
||||
# 2. 入场阈值:保持在 0.5 左右,只接深回调
|
||||
fb_entry_threshold: float = 0.,
|
||||
|
||||
stop_mult: float = 2, # 稍微放宽止损,适应趋势震荡
|
||||
limit_offset_mult: float = 0.2, # FV 挂单偏移
|
||||
|
||||
# --- 【其他】 ---
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None: order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
self.rng_frac = range_fraction
|
||||
self.stop_ticks = stop_loss_ticks # 止损跳数
|
||||
self.order_direction = order_direction
|
||||
self.alpha = 2.0 / (length + 1.0)
|
||||
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
|
||||
self.fisher_exit_level = fisher_exit_level
|
||||
self.fb_entry_threshold = fb_entry_threshold
|
||||
self.stop_mult = stop_mult
|
||||
self.limit_offset_mult = limit_offset_mult
|
||||
|
||||
self.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.indicator = indicator
|
||||
|
||||
# 历史数据缓存
|
||||
self._mid_price_history = deque(maxlen=50)
|
||||
self._itrend_history = deque(maxlen=50)
|
||||
self._trigger_history = deque(maxlen=50)
|
||||
# 缓存
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len) + 5
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
self.bar_count = 0
|
||||
self.position_meta: Dict[str, Any] = self.context.load_state()
|
||||
# 记录上一根 Bar 的 FB,仅用于判断拐点
|
||||
self._prev_fb = 0.0
|
||||
self.order_id_counter = 0
|
||||
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
"""辅助函数:将价格对齐到最小变动价位"""
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def on_init(self):
|
||||
super().on_init()
|
||||
self.position_meta = self.context.load_state()
|
||||
def _calculate_indicators(self):
|
||||
"""
|
||||
计算逻辑:保留原策略的简洁性,仅修复 Scaling 系数
|
||||
"""
|
||||
if len(self._closes) < self._buf_len:
|
||||
return None, None, None, None
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
self.log(f"合约换月: {old_symbol} -> {new_symbol},重置状态。")
|
||||
self._mid_price_history.clear()
|
||||
self._itrend_history.clear()
|
||||
self._trigger_history.clear()
|
||||
self.bar_count = 0
|
||||
# 1. 趋势中轴 T (物理中轴)
|
||||
h_trend = list(self._highs)[-self.t_len:]
|
||||
l_trend = list(self._lows)[-self.t_len:]
|
||||
T = (max(h_trend) + min(l_trend)) / 2.0
|
||||
|
||||
if old_symbol in self.position_meta:
|
||||
del self.position_meta[old_symbol]
|
||||
self.save_state(self.position_meta)
|
||||
# 2. 公平价 FV (FIR 滤波)
|
||||
FV = (self._closes[-1] + 2 * self._closes[-2] + 2 * self._closes[-3] + self._closes[-4]) / 6.0
|
||||
|
||||
self.symbol = new_symbol
|
||||
self.cancel_all_pending_orders(old_symbol)
|
||||
# 3. Fisher Transform (非递归版,响应更快)
|
||||
h_fisher = list(self._highs)[-self.f_len:]
|
||||
l_fisher = list(self._lows)[-self.f_len:]
|
||||
max_h, min_l = max(h_fisher), min(l_fisher)
|
||||
denom = max_h - min_l if max_h != min_l else self.min_tick
|
||||
|
||||
# --- 关键修正点 ---
|
||||
# 你的原代码是 0.66 * (stoc - 0.5),这导致最大值被锁死。
|
||||
# 改为 1.98,使得输入范围从 [-0.33, 0.33] 扩大到 [-0.99, 0.99]。
|
||||
# 这样 math.log 就能计算出 -3 到 +3 的值,让止盈逻辑“复活”且可控。
|
||||
stoc = (self._closes[-1] - min_l) / denom
|
||||
value = max(-0.999, min(0.999, 1.98 * (stoc - 0.5)))
|
||||
FB = 0.5 * math.log((1.0 + value) / (1.0 - value))
|
||||
|
||||
# 4. ATR
|
||||
tr_list = []
|
||||
for i in range(1, self.atr_len + 1):
|
||||
h, l, pc = self._highs[-i], self._lows[-i], self._closes[-i - 1]
|
||||
tr = max(h - l, abs(h - pc), abs(l - pc))
|
||||
tr_list.append(tr)
|
||||
ATR = sum(tr_list) / self.atr_len
|
||||
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 2: return
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
# 每次 Bar 重置挂单,防止挂单“长在”图表上
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
# --- 1. 指标计算 ---
|
||||
mid_price = (prev_bar.high + prev_bar.low) / 2.0
|
||||
self._mid_price_history.append(mid_price)
|
||||
self.bar_count += 1
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
if len(self._mid_price_history) < 3:
|
||||
self._itrend_history.append(mid_price)
|
||||
self._trigger_history.append(mid_price)
|
||||
return
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
if T is None: return
|
||||
|
||||
price = list(self._mid_price_history)
|
||||
itrend_prev = list(self._itrend_history)
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
current_itrend = 0.0
|
||||
if self.bar_count < 7:
|
||||
current_itrend = (price[-1] + 2 * price[-2] + price[-3]) / 4.0
|
||||
else:
|
||||
alpha = self.alpha
|
||||
a2 = alpha * alpha
|
||||
current_itrend = (alpha - a2 / 4) * price[-1] + (a2 / 2) * price[-2] - (alpha - 0.75 * a2) * price[-3] + \
|
||||
2 * (1 - alpha) * itrend_prev[-1] - (1 - alpha) ** 2 * itrend_prev[-2]
|
||||
# 状态定义
|
||||
# 这里我们不去定义 "short_signal" 这种会诱发反向平仓的变量
|
||||
# 而是只关注眼下的:大势(T) 和 动能(FB)
|
||||
trend_up = prev_bar.close > T
|
||||
trend_down = prev_bar.close < T
|
||||
|
||||
self._itrend_history.append(current_itrend)
|
||||
# ==========================================
|
||||
# 1. 持仓逻辑:简单、迟钝、粘性强
|
||||
# ==========================================
|
||||
if pos != 0:
|
||||
stop_dist = max(self.stop_mult * ATR, self.min_tick * 20)
|
||||
|
||||
if len(self._itrend_history) < 3:
|
||||
current_trigger = current_itrend
|
||||
else:
|
||||
current_trigger = 2.0 * current_itrend - self._itrend_history[-3]
|
||||
self._trigger_history.append(current_trigger)
|
||||
|
||||
# --- 2. 交易决策 ---
|
||||
if len(self._trigger_history) < 2: return
|
||||
|
||||
curr_trig = self._trigger_history[-1]
|
||||
prev_trig = self._trigger_history[-2]
|
||||
curr_itrend = self._itrend_history[-1]
|
||||
prev_itrend = self._itrend_history[-2]
|
||||
|
||||
is_bullish = (prev_trig <= prev_itrend) and (curr_trig > curr_itrend)
|
||||
is_bearish = (prev_trig >= prev_itrend) and (curr_trig < curr_itrend)
|
||||
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
meta = self.position_meta.get(symbol)
|
||||
|
||||
prev_range = prev_bar.high - prev_bar.low
|
||||
|
||||
# === 分支 A: 持仓管理 (止损/平仓) ===
|
||||
if position_volume != 0:
|
||||
if not meta:
|
||||
self.send_market_order("CLOSE_LONG" if position_volume > 0 else "CLOSE_SHORT", abs(position_volume),
|
||||
"CLOSE")
|
||||
return
|
||||
|
||||
entry_price = meta['entry_price']
|
||||
|
||||
# --- A1. 强制跳数止损 (Tick Stop) ---
|
||||
# 计算止损价差绝对值
|
||||
stop_diff = self.stop_ticks * self.min_tick
|
||||
|
||||
if position_volume > 0: # 多头
|
||||
# 止损价 = 开仓价 - 止损点数
|
||||
stop_price = entry_price - stop_diff
|
||||
# 对齐一下(虽然entry_price理论上已对齐,但为了保险)
|
||||
stop_price = self.round_to_tick(stop_price)
|
||||
|
||||
if prev_bar.close < stop_price:
|
||||
self.log(
|
||||
f"LONG STOP TRIGGERED. Close:{prev_bar.close} < Stop:{stop_price} (-{self.stop_ticks} ticks)")
|
||||
self.send_market_order("CLOSE_LONG", abs(position_volume), "CLOSE")
|
||||
if pos > 0:
|
||||
# A. 动能止盈 (Mean Reversion Exit)
|
||||
# 只有当行情极其疯狂(FB > 2.2) 且开始回头时才止盈。
|
||||
# 正常波动绝不下车。
|
||||
if FB > self.fisher_exit_level and FB < self._prev_fb:
|
||||
self.log(f"TAKE PROFIT (Long): FB {FB:.2f} Peak Reached")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
elif position_volume < 0: # 空头
|
||||
# 止损价 = 开仓价 + 止损点数
|
||||
stop_price = entry_price + stop_diff
|
||||
stop_price = self.round_to_tick(stop_price)
|
||||
|
||||
if prev_bar.close > stop_price:
|
||||
self.log(
|
||||
f"SHORT STOP TRIGGERED. Close:{prev_bar.close} > Stop:{stop_price} (+{self.stop_ticks} ticks)")
|
||||
self.send_market_order("CLOSE_SHORT", abs(position_volume), "CLOSE")
|
||||
# B. 硬止损 (ATR Stop) - 最后的防线
|
||||
if prev_bar.close < entry_price - stop_dist:
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# --- A2. 信号平仓 ---
|
||||
if position_volume > 0 and is_bearish:
|
||||
self.send_market_order("CLOSE_LONG", abs(position_volume), "CLOSE")
|
||||
return
|
||||
# C. (可选) 极端趋势反转保护
|
||||
# 如果你希望策略更像原来的“死拿”,这部分可以注释掉,或者把判定条件设严
|
||||
# if prev_bar.close < T - ATR: ...
|
||||
|
||||
if position_volume < 0 and is_bullish:
|
||||
self.send_market_order("CLOSE_SHORT", abs(position_volume), "CLOSE")
|
||||
return
|
||||
elif pos < 0:
|
||||
# A. 动能止盈
|
||||
if FB < -self.fisher_exit_level and FB > self._prev_fb:
|
||||
self.log(f"TAKE PROFIT (Short): FB {FB:.2f} Bottom Reached")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# === 分支 B: 开仓管理 ===
|
||||
else: # position_volume == 0
|
||||
# B. 硬止损
|
||||
if prev_bar.close > entry_price + stop_dist:
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# 计算回调距离 (Range Fraction)
|
||||
# 例如 Range=10, Frac=0.35 -> 3.5 -> 对齐到tick
|
||||
raw_offset = prev_range * self.rng_frac
|
||||
offset_price = self.round_to_tick(raw_offset)
|
||||
# ==========================================
|
||||
# 2. 开仓逻辑:严苛、左侧、顺大势
|
||||
# ==========================================
|
||||
if pos == 0:
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
# 开多
|
||||
if is_bullish and "BUY" in self.order_direction and (self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())):
|
||||
# 挂单价 = Open - 回调点数
|
||||
limit_price = open_price - offset_price
|
||||
limit_price = self.round_to_tick(limit_price) # 再次确保对齐
|
||||
# 挂单价格优化:
|
||||
# 在 FV 基础上再便宜一点点,增加胜率
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
|
||||
meta = {'entry_price': limit_price}
|
||||
self.send_limit_order(limit_price, "BUY", self.trade_volume, "OPEN", meta)
|
||||
self.log(f"Signal BUY. Open:{open_price} - Offset:{offset_price} = Limit:{limit_price}")
|
||||
# 开多:趋势向上 + 动能深跌 (FB < -0.5)
|
||||
if trend_up and FB < -self.fb_entry_threshold and is_met:
|
||||
if "BUY" in self.order_direction:
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
|
||||
# 开空
|
||||
elif is_bearish and "SELL" in self.order_direction and (self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())):
|
||||
# 挂单价 = Open + 回调点数
|
||||
limit_price = open_price + offset_price
|
||||
limit_price = self.round_to_tick(limit_price)
|
||||
# 开空:趋势向下 + 动能冲高 (FB > 0.5)
|
||||
elif trend_down and FB > self.fb_entry_threshold and is_met:
|
||||
if "SELL" in self.order_direction:
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
|
||||
meta = {'entry_price': limit_price}
|
||||
self.send_limit_order(limit_price, "SELL", self.trade_volume, "OPEN", meta)
|
||||
self.log(f"Signal SELL. Open:{open_price} + Offset:{offset_price} = Limit:{limit_price}")
|
||||
# 记录上一根FB,仅用于止盈时的拐点比较
|
||||
self._prev_fb = FB
|
||||
|
||||
# --- 交易辅助函数 ---
|
||||
def send_market_order(self, direction: str, volume: int, offset: str, meta: Optional[Dict] = None):
|
||||
if offset == "OPEN" and meta: self.position_meta[self.symbol] = meta
|
||||
order_id = f"{self.symbol}_{direction}_MKT_{self.order_id_counter}"
|
||||
# --- 辅助 ---
|
||||
def send_market_order(self, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_MKT_{ts}_{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)
|
||||
if offset == "CLOSE" and self.symbol in self.position_meta:
|
||||
del self.position_meta[self.symbol]
|
||||
self.save_state(self.position_meta)
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||||
submitted_time=self.get_current_time(), offset=offset))
|
||||
|
||||
def send_limit_order(self, limit_price: float, direction: str, volume: int, offset: str,
|
||||
meta: Optional[Dict] = None):
|
||||
if offset == "OPEN" and meta: self.position_meta[self.symbol] = meta
|
||||
order_id = f"{self.symbol}_{direction}_LMT_{self.order_id_counter}"
|
||||
def send_limit_order(self, price, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_LMT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(id=order_id, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=limit_price)
|
||||
self.send_order(order)
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=price))
|
||||
1284
src/strategies/TestStrategy/TestStrategy2.ipynb
Normal file
1284
src/strategies/TestStrategy/TestStrategy2.ipynb
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
import math
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List, Dict
|
||||
|
||||
@@ -9,25 +9,19 @@ from src.indicators.indicators import Empty
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 策略实现 (Pure Fisher Momentum Strategy)
|
||||
# =============================================================================
|
||||
|
||||
class PureFisherMomentumStrategy(Strategy):
|
||||
class CyberneticMidPointATRStrategy(Strategy):
|
||||
"""
|
||||
纯粹 Fisher 动能策略 (基于收益率分布)
|
||||
【控制论·物理中轴 ATR 止损策略】
|
||||
|
||||
核心修正:
|
||||
为了解决不同年份参数敏感度不一致的问题,本策略不再处理非平稳的"价格(Price)",
|
||||
而是处理平稳的"对数收益率(Log Returns)"。
|
||||
策略逻辑:
|
||||
1. 趋势锚点 (T): 20周期物理中点,决定做多/做空方向。
|
||||
2. 费舍尔动能 (FB): 10周期去递归 Fisher 变换,捕捉短期回调超卖/超买。
|
||||
3. 瞬时公平价 (FV): 4-Tap FIR 滤波器,作为限价单挂单位置。
|
||||
4. ATR 止损: 基于 N 周期滑动窗口 ATR 的动态止损 (EntryPrice ± n * ATR)。
|
||||
|
||||
逻辑链条:
|
||||
1. Calculate: r = ln(Close / Prev_Close) -> 捕捉瞬时动能。
|
||||
2. Normalize: 将 r 映射到 [-1, 1] -> 消除绝对波动率的差异(自适应高低波环境)。
|
||||
3. Fisher: 对归一化后的 r 进行非线性变换 -> 极化信号,使尾部事件(突破)更明显。
|
||||
4. Bollinger: 动态衡量 Fisher 值的统计显著性。
|
||||
|
||||
参数极简,逻辑对称。
|
||||
执行:
|
||||
- 仅在空仓时,根据 T 和 FB 信号在 FV 处挂限价单。
|
||||
- 持仓时,根据 ATR 动态止损或 FB 极值平仓。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -36,269 +30,200 @@ class PureFisherMomentumStrategy(Strategy):
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 【核心参数】 ---
|
||||
fisher_period: int = 10, # 动能观测窗口 (决定灵敏度)
|
||||
bb_length: int = 20, # 分布统计窗口 (决定稳定性)
|
||||
bb_std_dev: float = 2.0, # 突破阈值 (决定胜率/盈亏比倾向)
|
||||
# --- 【风控参数】 ---
|
||||
atr_period: int = 230,
|
||||
protection_atr_mult: float = 4.0, # 仅用于防黑天鹅,不参与日常逻辑
|
||||
# --- 【外部依赖】 ---
|
||||
indicator: Optional[Indicator] = None,
|
||||
# --- 【合约规格参数】 ---
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 【策略参数】 ---
|
||||
trend_period: int = 20, # 趋势中轴窗口
|
||||
fisher_period: int = 10, # 费舍尔动能窗口
|
||||
atr_period: int = 23, # ATR 计算窗口
|
||||
stop_mult: float = 1.5, # ATR 止损倍数 (n)
|
||||
|
||||
# --- 【其他】 ---
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None: order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
self.main_symbol = main_symbol
|
||||
self.min_tick = min_tick
|
||||
|
||||
self.fisher_period = fisher_period
|
||||
self.bb_length = bb_length
|
||||
self.bb_std_dev = bb_std_dev
|
||||
self.atr_period = atr_period
|
||||
self.protection_atr_mult = protection_atr_mult
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
self.stop_mult = stop_mult
|
||||
|
||||
self.indicator = indicator if indicator is not None else Empty()
|
||||
self.order_direction = order_direction
|
||||
self.indicator = indicator
|
||||
|
||||
# --- 内部状态 ---
|
||||
self._prev_close = 0.0
|
||||
# --- 非递归状态管理 ---
|
||||
# 缓存长度需要覆盖最大的计算窗口
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len) + 5
|
||||
|
||||
# 1. 收益率缓存 (用于归一化计算)
|
||||
self._returns_buffer: deque = deque(maxlen=self.fisher_period)
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
# 2. Fisher 递归变量
|
||||
self.prev_value1 = 0.0
|
||||
self.prev_fisher_val = 0.0
|
||||
|
||||
# 3. 布林带缓存 (存储计算好的 Fisher 值)
|
||||
self._fisher_buffer: deque = deque(maxlen=self.bb_length)
|
||||
|
||||
# 4. 交易状态
|
||||
self.position_meta: Dict[str, Any] = self.context.load_state()
|
||||
self.order_id_counter = 0
|
||||
|
||||
self.log(f"PureFisher Init. Period={fisher_period}, BB={bb_length}/{bb_std_dev}")
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
"""辅助函数:将价格对齐到最小变动价位"""
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def _reset_state(self):
|
||||
"""重置状态缓存"""
|
||||
self._highs.clear()
|
||||
self._lows.clear()
|
||||
self._closes.clear()
|
||||
|
||||
def on_init(self):
|
||||
super().on_init()
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
self.position_meta = self.context.load_state()
|
||||
self._reset_state()
|
||||
|
||||
# --- 数学核心 (Fisher on Returns) ---
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
"""合约换月处理"""
|
||||
self.log(f"合约换月: {old_symbol} -> {new_symbol},重置状态。")
|
||||
self._reset_state()
|
||||
self.symbol = new_symbol
|
||||
self.cancel_all_pending_orders(old_symbol)
|
||||
|
||||
def _calculate_fisher_on_returns(self, current_return: float) -> float:
|
||||
def _calculate_indicators(self):
|
||||
"""
|
||||
对 [收益率] 进行 Fisher 变换。
|
||||
相比对价格变换,收益率无趋势性,不会饱和,参数在不同年份具有极高鲁棒性。
|
||||
计算核心物理指标 (完全非递归滑动窗口)
|
||||
返回: (T, FV, FB, ATR)
|
||||
"""
|
||||
self._returns_buffer.append(current_return)
|
||||
if len(self._returns_buffer) < self.fisher_period:
|
||||
return 0.0
|
||||
# 数据量检查 (至少需要满足最长窗口 + 1 根用于算 TR)
|
||||
if len(self._closes) < max(self.t_len, self.f_len, self.atr_len + 1):
|
||||
return None, None, None, None
|
||||
|
||||
# 1. 归一化 (Normalize)
|
||||
# 寻找这一段时间内波动率的极值,自适应当前市场环境
|
||||
data = np.array(self._returns_buffer)
|
||||
min_val = np.min(data)
|
||||
max_val = np.max(data)
|
||||
# --- 1. 趋势中轴 (T) ---
|
||||
h_trend = list(self._highs)[-self.t_len:]
|
||||
l_trend = list(self._lows)[-self.t_len:]
|
||||
T = (max(h_trend) + min(l_trend)) / 2.0
|
||||
|
||||
if max_val - min_val < 1e-9:
|
||||
val1 = 0.0
|
||||
else:
|
||||
# 映射到 [-1, 1]
|
||||
val1 = 2.0 * ((current_return - min_val) / (max_val - min_val) - 0.5)
|
||||
# --- 2. 瞬时公平价 (FV) ---
|
||||
# FIR 滤波器: (C0 + 2*C1 + 2*C2 + C3) / 6
|
||||
FV = (self._closes[-1] + 2 * self._closes[-2] + 2 * self._closes[-3] + self._closes[-4]) / 6.0
|
||||
|
||||
# 2. 平滑 (Smoothing)
|
||||
# 消除收益率的随机噪音
|
||||
val1 = 0.33 * val1 + 0.67 * self.prev_value1
|
||||
# --- 3. 费舍尔动能 (FB) ---
|
||||
h_fisher = list(self._highs)[-self.f_len:]
|
||||
l_fisher = list(self._lows)[-self.f_len:]
|
||||
max_h = max(h_fisher)
|
||||
min_l = min(l_fisher)
|
||||
denom = max_h - min_l if max_h != min_l else self.min_tick
|
||||
|
||||
# 3. 钳制 (Clamping)
|
||||
if val1 > 0.99: val1 = 0.999
|
||||
if val1 < -0.99: val1 = -0.999
|
||||
self.prev_value1 = val1
|
||||
stoc = (self._closes[-1] - min_l) / denom
|
||||
value = max(-0.99, min(0.99, 0.66 * (stoc - 0.5)))
|
||||
FB = 0.5 * math.log((1.0 + value) / (1.0 - value))
|
||||
|
||||
# 4. 变换 (Transform)
|
||||
fisher = 0.5 * np.log((1 + val1) / (1 - val1)) + 0.5 * self.prev_fisher_val
|
||||
self.prev_fisher_val = fisher
|
||||
# --- 4. 非递归 ATR (滑动窗口 TR 的平均值) ---
|
||||
tr_list = []
|
||||
for i in range(1, self.atr_len + 1):
|
||||
# idx: -1, -2, ... -atr_len
|
||||
h = self._highs[-i]
|
||||
l = self._lows[-i]
|
||||
pc = self._closes[-i - 1] # 前一根收盘
|
||||
tr = max(h - l, abs(h - pc), abs(l - pc))
|
||||
tr_list.append(tr)
|
||||
|
||||
return fisher
|
||||
ATR = sum(tr_list) / self.atr_len
|
||||
|
||||
# --- 主逻辑 ---
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
|
||||
# 预热检查
|
||||
required_len = max(self.fisher_period + self.bb_length, self.atr_period) + 2
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
# 1. 立即撤销所有挂单
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
current_bar = bar_history[-1]
|
||||
# 2. 获取历史 Bar
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
# 1. 计算 ATR (仅用于风控计算)
|
||||
highs = np.array([b.high for b in bar_history], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
current_atr = talib.ATR(highs, lows, closes, self.atr_period)[-1]
|
||||
# 3. 更新缓存
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
# 2. 计算对数收益率 (Log Return)
|
||||
# 使用 Log Return 具有更好的数学性质 (时间可加性)
|
||||
prev_close = bar_history[-2].close
|
||||
current_return = np.log(current_bar.close / prev_close)
|
||||
# 4. 计算指标
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
if T is None: return # 数据预热中
|
||||
|
||||
# 3. 计算 Fisher 指标
|
||||
current_fisher = self._calculate_fisher_on_returns(current_return)
|
||||
|
||||
# 4. 计算布林带 (基于 Fisher 值)
|
||||
self._fisher_buffer.append(current_fisher)
|
||||
if len(self._fisher_buffer) < self.bb_length:
|
||||
return
|
||||
|
||||
f_arr = np.array(self._fisher_buffer)
|
||||
bb_mean = np.mean(f_arr)
|
||||
bb_std = np.std(f_arr)
|
||||
|
||||
bb_upper = bb_mean + self.bb_std_dev * bb_std
|
||||
bb_lower = bb_mean - self.bb_std_dev * bb_std
|
||||
|
||||
# 5. 状态同步
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
self._sync_position_state(position_volume)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
if not self.trading: return
|
||||
|
||||
# 6. 交易逻辑分流
|
||||
# ==========================================
|
||||
# 模块 A:持仓风控 (ATR 止损与极值平仓)
|
||||
# ==========================================
|
||||
if position_volume != 0:
|
||||
# 持仓逻辑:先检查黑天鹅硬止损,再检查Fisher回归逻辑
|
||||
if not self._check_hard_stop(position_volume, current_bar):
|
||||
self._logic_exit(current_fisher, bb_mean, position_volume)
|
||||
else:
|
||||
# 开仓逻辑:Fisher 突破布林带
|
||||
self._logic_entry(current_fisher, bb_upper, bb_lower, current_bar, current_atr)
|
||||
|
||||
def _check_hard_stop(self, volume: int, bar: Bar) -> bool:
|
||||
"""灾难级硬止损 (Black Swan Protection)"""
|
||||
meta = self.position_meta.get(self.symbol)
|
||||
if not meta or 'hard_stop_price' not in meta:
|
||||
return False
|
||||
# --- A1. ATR 动态止损 ---
|
||||
# 止损距离 = n * ATR
|
||||
stop_dist = self.stop_mult * ATR
|
||||
|
||||
stop_price = meta['hard_stop_price']
|
||||
triggered = False
|
||||
direction = ""
|
||||
if position_volume > 0:
|
||||
stop_price = self.round_to_tick(entry_price - stop_dist)
|
||||
if prev_bar.close < stop_price:
|
||||
self.log(
|
||||
f"ATR STOP (Long). Entry:{entry_price}, ATR:{ATR:.2f}, Stop:{stop_price}, Close:{prev_bar.close}")
|
||||
self.send_market_order("CLOSE_LONG", abs(position_volume), "CLOSE")
|
||||
return # 止损后结束本 Bar 逻辑
|
||||
|
||||
if volume > 0 and bar.low <= stop_price:
|
||||
triggered = True
|
||||
direction = "CLOSE_LONG"
|
||||
self.log(f"🛑 HARD STOP (Long) Triggered @ {bar.low:.2f}", level='WARNING')
|
||||
elif volume < 0 and bar.high >= stop_price:
|
||||
triggered = True
|
||||
direction = "CLOSE_SHORT"
|
||||
self.log(f"🛑 HARD STOP (Short) Triggered @ {bar.high:.2f}", level='WARNING')
|
||||
elif position_volume < 0:
|
||||
stop_price = self.round_to_tick(entry_price + stop_dist)
|
||||
if prev_bar.close > stop_price:
|
||||
self.log(
|
||||
f"ATR STOP (Short). Entry:{entry_price}, ATR:{ATR:.2f}, Stop:{stop_price}, Close:{prev_bar.close}")
|
||||
self.send_market_order("CLOSE_SHORT", abs(position_volume), "CLOSE")
|
||||
return # 止损后结束本 Bar 逻辑
|
||||
|
||||
if triggered:
|
||||
self.close_position(direction, abs(volume))
|
||||
return True
|
||||
return False
|
||||
# --- A2. 费舍尔概率极值止盈 ---
|
||||
if position_volume > 0 and FB > 0:
|
||||
self.log(f"FISHER EXIT (Long). FB:{FB:.2f} > 2.0")
|
||||
self.send_market_order("CLOSE_LONG", abs(position_volume), "CLOSE")
|
||||
return
|
||||
|
||||
def _logic_entry(self, fisher: float, upper: float, lower: float, bar: Bar, atr: float):
|
||||
"""
|
||||
一致性开仓逻辑:
|
||||
Fisher(Returns) 代表动能的累积分布。
|
||||
突破上轨 -> 正向动能呈现统计学显著性 -> 做多
|
||||
突破下轨 -> 负向动能呈现统计学显著性 -> 做空
|
||||
"""
|
||||
# 外部单一指标过滤 (如需)
|
||||
if not self.indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
return
|
||||
if position_volume < 0 and FB < -0:
|
||||
self.log(f"FISHER EXIT (Short). FB:{FB:.2f} < -2.0")
|
||||
self.send_market_order("CLOSE_SHORT", abs(position_volume), "CLOSE")
|
||||
return
|
||||
|
||||
direction = None
|
||||
# ==========================================
|
||||
# 模块 B:开仓逻辑 (限价单于 FV 处回调入场)
|
||||
# ==========================================
|
||||
if position_volume == 0:
|
||||
|
||||
if fisher > upper:
|
||||
direction = "BUY"
|
||||
elif fisher < lower:
|
||||
direction = "SELL"
|
||||
curr_close = prev_bar.close
|
||||
limit_price = self.round_to_tick(FV)
|
||||
|
||||
if direction:
|
||||
# 计算固定硬止损 (一旦设定不再更改)
|
||||
entry_price = bar.close
|
||||
stop_dist = atr * self.protection_atr_mult
|
||||
hard_stop = entry_price - stop_dist if direction == "BUY" else entry_price + stop_dist
|
||||
# 做多:大趋势向上 (Close > T) 且 短期超卖 (FB < 0)
|
||||
if curr_close > T and FB < 0:
|
||||
if "BUY" in self.order_direction:
|
||||
if self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
self.log(f"SIGNAL BUY. FB:{FB:.2f}, Limit @ {limit_price}")
|
||||
self.send_limit_order(limit_price, "BUY", self.trade_volume, "OPEN")
|
||||
|
||||
self.log(f"Entry {direction} | Fisher:{fisher:.2f} Breakout BB[{lower:.2f}, {upper:.2f}]")
|
||||
# 做空:大趋势向下 (Close < T) 且 短期超买 (FB > 0)
|
||||
elif curr_close < T and FB > 0:
|
||||
if "SELL" in self.order_direction:
|
||||
if self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
self.log(f"SIGNAL SELL. FB:{FB:.2f}, Limit @ {limit_price}")
|
||||
self.send_limit_order(limit_price, "SELL", self.trade_volume, "OPEN")
|
||||
|
||||
meta = {
|
||||
'entry_price': entry_price,
|
||||
'hard_stop_price': hard_stop,
|
||||
'entry_fisher': fisher
|
||||
}
|
||||
self.send_market_order(direction, self.trade_volume, "OPEN", meta)
|
||||
|
||||
def _logic_exit(self, fisher: float, mid_band: float, volume: int):
|
||||
"""
|
||||
一致性平仓逻辑:
|
||||
Fisher 回归均值 (Mean Reversion of Momentum)
|
||||
意味着极端的加速行情结束,转为随机漫步或反转,此时离场以保护利润。
|
||||
"""
|
||||
direction = None
|
||||
|
||||
if volume > 0: # 多头持仓
|
||||
if fisher < mid_band: # 动能跌破均值
|
||||
direction = "CLOSE_LONG"
|
||||
self.log(f"Exit LONG | Fisher:{fisher:.2f} < Mean:{mid_band:.2f}")
|
||||
|
||||
elif volume < 0: # 空头持仓
|
||||
if fisher > mid_band: # 动能涨破均值
|
||||
direction = "CLOSE_SHORT"
|
||||
self.log(f"Exit SHORT | Fisher:{fisher:.2f} > Mean:{mid_band:.2f}")
|
||||
|
||||
if direction:
|
||||
self.close_position(direction, abs(volume))
|
||||
|
||||
# --- 辅助与风控状态管理 ---
|
||||
|
||||
def _sync_position_state(self, actual_volume: int):
|
||||
"""处理程序重启或外部干预导致的状态不一致"""
|
||||
meta = self.position_meta.get(self.symbol)
|
||||
if actual_volume != 0 and not meta:
|
||||
dir_ = "CLOSE_LONG" if actual_volume > 0 else "CLOSE_SHORT"
|
||||
self.send_market_order(dir_, abs(actual_volume), 'CLOSE')
|
||||
elif actual_volume == 0 and meta:
|
||||
self.position_meta.pop(self.symbol, None)
|
||||
self.save_state(self.position_meta)
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
"""
|
||||
合约换月逻辑
|
||||
Fisher 是基于历史序列的递归计算,换月会导致价格跳空,必须重置。
|
||||
"""
|
||||
super().on_rollover(old_symbol, new_symbol)
|
||||
self.log(f"Rollover: {old_symbol} -> {new_symbol}. Resetting math buffers.")
|
||||
|
||||
# 重置所有递归和缓存
|
||||
self._returns_buffer.clear()
|
||||
self._fisher_buffer.clear()
|
||||
self.prev_value1 = 0.0
|
||||
self.prev_fisher_val = 0.0
|
||||
|
||||
self.position_meta = {}
|
||||
self.save_state(self.position_meta)
|
||||
|
||||
def close_position(self, direction: str, volume: int):
|
||||
self.send_market_order(direction, volume, offset="CLOSE")
|
||||
if self.symbol in self.position_meta:
|
||||
del self.position_meta[self.symbol]
|
||||
self.save_state(self.position_meta)
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str, meta: Optional[Dict] = None):
|
||||
if offset == "OPEN" and meta:
|
||||
self.position_meta[self.symbol] = meta
|
||||
self.save_state(self.position_meta)
|
||||
|
||||
order_id = f"{self.symbol}_{direction}_{offset}_{self.order_id_counter}"
|
||||
# --- 交易辅助函数 ---
|
||||
def send_market_order(self, direction: str, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_MKT_{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)
|
||||
|
||||
order = Order(
|
||||
id=order_id, symbol=self.symbol, direction=direction, volume=volume,
|
||||
price_type="MARKET", submitted_time=self.get_current_time(), offset=offset
|
||||
)
|
||||
def send_limit_order(self, limit_price: float, direction: str, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_LMT_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(id=order_id, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=limit_price)
|
||||
self.send_order(order)
|
||||
240
src/strategies/TestStrategy/TestStrategy3.py
Normal file
240
src/strategies/TestStrategy/TestStrategy3.py
Normal file
@@ -0,0 +1,240 @@
|
||||
import numpy as np
|
||||
import math
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List, Dict
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class CyberneticStrategy(Strategy):
|
||||
"""
|
||||
【控制论策略 - 数学修复版】
|
||||
|
||||
修复重点:
|
||||
1. 修正 Fisher 计算公式:之前的公式被系数锁定在 [-0.34, 0.34],导致永远无法触发止盈。
|
||||
现在采用标准的 Ehlers 递归算法,值域恢复正常的 [-2.0, 2.0] 以上。
|
||||
2. 逻辑闭环:保留“趋势破位平仓”和“动能拐点止盈”。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
min_tick: float = 1.0,
|
||||
|
||||
# --- 周期参数 ---
|
||||
trend_period: int = 26, # 趋势判断
|
||||
fisher_period: int = 10, # Fisher 周期 (标准用法通常较短,如10,你之前用的46可能太长导致钝化)
|
||||
atr_period: int = 23,
|
||||
|
||||
# --- 阈值参数 ---
|
||||
# 修复后 FB 值域变大,阈值需要相应提高
|
||||
fisher_exit_level: float = 1.5, # 建议设为 1.5 - 2.0
|
||||
fb_entry_threshold: float = 0.5, # 入场过滤
|
||||
|
||||
stop_mult: float = 2.0,
|
||||
limit_offset_mult: float = 0.2,
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.min_tick = min_tick
|
||||
|
||||
self.t_len = trend_period
|
||||
self.f_len = fisher_period
|
||||
self.atr_len = atr_period
|
||||
|
||||
self.fisher_exit_level = fisher_exit_level
|
||||
self.fb_entry_threshold = fb_entry_threshold
|
||||
self.stop_mult = stop_mult
|
||||
self.limit_offset_mult = limit_offset_mult
|
||||
|
||||
self.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.indicator = indicator
|
||||
|
||||
# 缓存
|
||||
self._buf_len = max(self.t_len, self.f_len, self.atr_len) + 5
|
||||
self._highs = deque(maxlen=self._buf_len)
|
||||
self._lows = deque(maxlen=self._buf_len)
|
||||
self._closes = deque(maxlen=self._buf_len)
|
||||
|
||||
# --- Fisher 递归计算需要的状态变量 ---
|
||||
# Value[n-1], Fisher[n-1]
|
||||
self._prev_val = 0.0
|
||||
self._prev_fisher = 0.0
|
||||
self._prev_fb_for_exit = 0.0 # 用于判断拐点
|
||||
|
||||
self.order_id_counter = 0
|
||||
|
||||
def round_to_tick(self, price: float) -> float:
|
||||
if self.min_tick <= 0: return price
|
||||
return round(price / self.min_tick) * self.min_tick
|
||||
|
||||
def _calculate_indicators(self):
|
||||
"""
|
||||
使用 Ehlers 标准递归公式计算 Fisher Transform
|
||||
"""
|
||||
if len(self._closes) < self._buf_len:
|
||||
return None, None, None, None
|
||||
|
||||
# 1. 趋势中轴 T
|
||||
h_trend = list(self._highs)[-self.t_len:]
|
||||
l_trend = list(self._lows)[-self.t_len:]
|
||||
T = (max(h_trend) + min(l_trend)) / 2.0
|
||||
|
||||
# 2. FV
|
||||
FV = (self._closes[-1] + 2 * self._closes[-2] + 2 * self._closes[-3] + self._closes[-4]) / 6.0
|
||||
|
||||
# 3. Fisher Transform (Ehlers Recursive Method)
|
||||
# 获取周期内的最高最低
|
||||
h_fisher = list(self._highs)[-self.f_len:]
|
||||
l_fisher = list(self._lows)[-self.f_len:]
|
||||
max_h, min_l = max(h_fisher), min(l_fisher)
|
||||
denom = max_h - min_l if max_h != min_l else self.min_tick
|
||||
|
||||
# --- 核心数学修正 ---
|
||||
# 步骤A: 归一化到 [-1, 1] 并不是用 0.66,而是接近全幅
|
||||
# 当前位置
|
||||
current_price = self._closes[-1]
|
||||
# 归一化 x 范围在 [-1, 1]
|
||||
x = 2.0 * ((current_price - min_l) / denom - 0.5)
|
||||
|
||||
# 步骤B: 平滑输入 Value (Ehlers 公式: Val = 0.33*x + 0.67*Val_prev)
|
||||
# 这步平滑至关重要,否则 Fisher 会因噪音剧烈跳动
|
||||
val = 0.33 * x + 0.67 * self._prev_val
|
||||
|
||||
# 边界保护 (防止 log 炸裂)
|
||||
val = max(-0.999, min(0.999, val))
|
||||
|
||||
# 步骤C: 计算 Fisher 并递归平滑
|
||||
# Fisher = 0.5 * log((1+val)/(1-val)) + 0.5 * Fisher_prev
|
||||
fisher_raw = 0.5 * math.log((1.0 + val) / (1.0 - val))
|
||||
FB = 0.5 * fisher_raw + 0.5 * self._prev_fisher
|
||||
|
||||
# 4. ATR
|
||||
tr_list = []
|
||||
for i in range(1, self.atr_len + 1):
|
||||
h, l, pc = self._highs[-i], self._lows[-i], self._closes[-i - 1]
|
||||
tr = max(h - l, abs(h - pc), abs(l - pc))
|
||||
tr_list.append(tr)
|
||||
ATR = sum(tr_list) / self.atr_len
|
||||
|
||||
# --- 更新状态 (非常重要) ---
|
||||
# 必须在计算完成后更新,供下一根 bar 使用
|
||||
# 注意:这里我们假设是在回测或者实盘顺序执行。如果是向量化计算需重写。
|
||||
self._temp_val = val
|
||||
self._temp_fisher = FB
|
||||
|
||||
return T, FV, FB, ATR
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
bars = self.get_bar_history()
|
||||
if len(bars) < 1: return
|
||||
prev_bar = bars[-1]
|
||||
|
||||
self._highs.append(prev_bar.high)
|
||||
self._lows.append(prev_bar.low)
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
|
||||
# 只有在计算成功后才更新递归状态
|
||||
if T is not None:
|
||||
self._prev_val = self._temp_val
|
||||
self._prev_fisher = self._temp_fisher
|
||||
else:
|
||||
return
|
||||
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
# 状态定义
|
||||
trend_up = prev_bar.close > T
|
||||
trend_down = prev_bar.close < T
|
||||
|
||||
# ====================
|
||||
# 1. 平仓逻辑
|
||||
# ====================
|
||||
if pos != 0:
|
||||
stop_dist = max(self.stop_mult * ATR, self.min_tick * 10)
|
||||
|
||||
if pos > 0:
|
||||
# 趋势破位平仓 (最优先)
|
||||
if trend_down:
|
||||
self.log(f"TREND BROKEN (Long): Close < T")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# 动能止盈 (修复后的 FB 现在能达到 1.5, 2.0 了)
|
||||
if FB > self.fisher_exit_level and FB < self._prev_fb_for_exit:
|
||||
self.log(f"FISHER PROFIT (Long): FB {FB:.2f} < Prev {self._prev_fb_for_exit:.2f}")
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# 硬止损
|
||||
if prev_bar.close < entry_price - stop_dist:
|
||||
self.send_market_order("CLOSE_LONG", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
elif pos < 0:
|
||||
# 趋势破位平仓
|
||||
if trend_up:
|
||||
self.log(f"TREND BROKEN (Short): Close > T")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# 动能止盈
|
||||
if FB < -self.fisher_exit_level and FB > self._prev_fb_for_exit:
|
||||
self.log(f"FISHER PROFIT (Short): FB {FB:.2f} > Prev {self._prev_fb_for_exit:.2f}")
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# 硬止损
|
||||
if prev_bar.close > entry_price + stop_dist:
|
||||
self.send_market_order("CLOSE_SHORT", abs(pos), "CLOSE")
|
||||
return
|
||||
|
||||
# ====================
|
||||
# 2. 开仓逻辑
|
||||
# ====================
|
||||
if pos == 0:
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
|
||||
# 现在的 FB 值域正常了,0.5 左右的阈值才是有意义的
|
||||
if trend_up and FB < -self.fb_entry_threshold and is_met:
|
||||
if "BUY" in self.order_direction:
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
|
||||
elif trend_down and FB > self.fb_entry_threshold and is_met:
|
||||
if "SELL" in self.order_direction:
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
|
||||
# 记录用于拐点判断的上一根 FB
|
||||
self._prev_fb_for_exit = FB
|
||||
|
||||
# --- 辅助 ---
|
||||
def send_market_order(self, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_MKT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||||
submitted_time=self.get_current_time(), offset=offset))
|
||||
|
||||
def send_limit_order(self, price, direction, volume, offset):
|
||||
ts = self.get_current_time().strftime('%H%M%S')
|
||||
oid = f"{self.symbol}_{direction}_LMT_{ts}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
self.send_order(Order(id=oid, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=price))
|
||||
File diff suppressed because one or more lines are too long
1001
strategy_manager/README.md
Normal file
1001
strategy_manager/README.md
Normal file
File diff suppressed because it is too large
Load Diff
646
strategy_manager/frontend/dist/index.html
vendored
646
strategy_manager/frontend/dist/index.html
vendored
@@ -8,29 +8,251 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- 1. 引入 Vue 3 -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.21/vue.global.prod.min.js"></script>
|
||||
<!-- 2. 引入 Naive UI -->
|
||||
<script src="https://unpkg.com/naive-ui/dist/index.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/naive-ui/2.38.1/index.js"></script>
|
||||
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #f5f7fa; margin: 0; }
|
||||
#app { padding: 20px; max-width: 1400px; margin: 0 auto; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.log-container { background: #1e1e1e; padding: 15px; border-radius: 4px; height: 400px; overflow: auto; font-family: monospace; font-size: 12px; color: #ddd; }
|
||||
.log-line { margin: 2px 0; border-bottom: 1px solid #333; padding-bottom: 2px; }
|
||||
:root {
|
||||
--header-bg: #ffffff;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #e8e8e8;
|
||||
--text-primary: #262626;
|
||||
--text-secondary: #8c8c8c;
|
||||
--shadow-sm: 0 2px 8px rgba(0,0,0,0.06);
|
||||
--shadow-md: 0 4px 16px rgba(0,0,0,0.08);
|
||||
--spacing-xs: 8px;
|
||||
--spacing-sm: 12px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#app {
|
||||
padding: var(--spacing-lg);
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 白名单状态标签 */
|
||||
.whitelist-tag { cursor: pointer; }
|
||||
.whitelist-tag:hover { opacity: 0.8; }
|
||||
/* 头部布局优化 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: var(--header-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-row { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||
.stat-card { flex: 1; min-width: 150px; background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.stat-card h4 { margin: 0 0 8px 0; font-size: 13px; color: #666; font-weight: normal; }
|
||||
.stat-card .value { font-size: 28px; font-weight: bold; color: #333; }
|
||||
/* 统计卡片优化 */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--card-bg);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.stat-card h4 {
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.stat-card .value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.stat-card.running .value { color: #27ae60; }
|
||||
.stat-card.stopped .value { color: #e74c3c; }
|
||||
.stat-card.whitelist .value { color: #9b59b6; }
|
||||
|
||||
/* 操作工具栏优化 */
|
||||
.toolbar-card {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.toolbar-card .n-card__content {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
}
|
||||
.toolbar-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding-right: var(--spacing-md);
|
||||
border-right: 1px solid var(--border-color);
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
.toolbar-group:last-child {
|
||||
border-right: none;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.toolbar-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* 表格优化 */
|
||||
.strategy-tabs {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
.n-tabs-nav {
|
||||
padding: 0 var(--spacing-lg);
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.tab-content {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
.n-table-wrapper {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.n-table {
|
||||
font-size: 13px;
|
||||
}
|
||||
.n-table thead .n-th {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
padding: var(--spacing-md) var(--spacing-sm);
|
||||
}
|
||||
.n-table tbody .n-td {
|
||||
padding: var(--spacing-sm) var(--spacing-sm);
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
.n-table tbody .n-tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
.strategy-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.strategy-info {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 日志容器优化 */
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
padding: var(--spacing-md);
|
||||
border-radius: 8px;
|
||||
height: 420px;
|
||||
overflow: auto;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #ddd;
|
||||
}
|
||||
.log-line {
|
||||
margin: 2px 0;
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
.log-line.error { color: #ff6b6b; }
|
||||
.log-line.warning { color: #feca57; }
|
||||
.log-line.info { color: #54a0ff; }
|
||||
.log-line.success { color: #1dd1a1; }
|
||||
|
||||
/* 标签样式 */
|
||||
.status-tag {
|
||||
font-weight: 500;
|
||||
}
|
||||
.whitelist-tag {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.whitelist-tag:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--spacing-md);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.stats-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.stats-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.toolbar-group {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -99,117 +321,204 @@
|
||||
</div>
|
||||
|
||||
<!-- 白名单管理工具栏 -->
|
||||
<n-card title="🛠️ 批量操作" hoverable style="margin-bottom: 20px;">
|
||||
<n-space wrap>
|
||||
<n-button type="success" size="small" @click="batchStart" :disabled="selectedKeys.length === 0">
|
||||
启动选中
|
||||
</n-button>
|
||||
<n-button type="error" size="small" @click="batchStop" :disabled="selectedKeys.length === 0">
|
||||
停止选中
|
||||
</n-button>
|
||||
<n-button type="warning" size="small" @click="batchRestart" :disabled="selectedKeys.length === 0">
|
||||
重启选中
|
||||
</n-button>
|
||||
<n-divider vertical />
|
||||
<n-button type="primary" size="small" @click="batchAddToWhitelist" :disabled="selectedKeys.length === 0">
|
||||
添加到白名单
|
||||
</n-button>
|
||||
<n-button type="info" size="small" @click="batchRemoveFromWhitelist" :disabled="selectedKeys.length === 0">
|
||||
从白名单移除
|
||||
</n-button>
|
||||
<n-button type="success" size="small" @click="batchEnableInWhitelist" :disabled="selectedKeys.length === 0">
|
||||
启用白名单
|
||||
</n-button>
|
||||
<n-button type="warning" size="small" @click="batchDisableInWhitelist" :disabled="selectedKeys.length === 0">
|
||||
禁用白名单
|
||||
</n-button>
|
||||
<n-divider vertical />
|
||||
<n-button type="warning" size="small" @click="triggerAutoStart">
|
||||
🚀 手动触发自动启动
|
||||
</n-button>
|
||||
<n-tag type="info" size="small">
|
||||
今日已自动启动: {{ whitelistAutoStarted ? '是' : '否' }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<n-card title="🛠️ 批量操作" hoverable class="toolbar-card">
|
||||
<div class="toolbar-section">
|
||||
<div class="toolbar-group">
|
||||
<span class="toolbar-label">生命周期</span>
|
||||
<n-button type="success" size="small" @click="batchStart" :disabled="selectedKeys.length === 0">
|
||||
启动选中
|
||||
</n-button>
|
||||
<n-button type="error" size="small" @click="batchStop" :disabled="selectedKeys.length === 0">
|
||||
停止选中
|
||||
</n-button>
|
||||
<n-button type="warning" size="small" @click="batchRestart" :disabled="selectedKeys.length === 0">
|
||||
重启选中
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<span class="toolbar-label">白名单</span>
|
||||
<n-button type="primary" size="small" @click="batchAddToWhitelist" :disabled="selectedKeys.length === 0">
|
||||
添加到白名单
|
||||
</n-button>
|
||||
<n-button type="info" size="small" @click="batchRemoveFromWhitelist" :disabled="selectedKeys.length === 0">
|
||||
从白名单移除
|
||||
</n-button>
|
||||
<n-button type="success" size="small" @click="batchEnableInWhitelist" :disabled="selectedKeys.length === 0">
|
||||
启用
|
||||
</n-button>
|
||||
<n-button type="warning" size="small" @click="batchDisableInWhitelist" :disabled="selectedKeys.length === 0">
|
||||
禁用
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<n-button type="warning" size="small" @click="triggerAutoStart">
|
||||
🚀 手动触发自动启动
|
||||
</n-button>
|
||||
<n-tag type="info" size="small">
|
||||
今日已自动启动: {{ whitelistAutoStarted ? '是' : '否' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 策略列表 -->
|
||||
<n-card title="📋 策略列表" hoverable>
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-button text @click="selectAll" size="small">全选</n-button>
|
||||
<n-button text @click="clearSelection" size="small">清空</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<n-table :single-line="false" striped>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<n-checkbox :checked="allSelected" :indeterminate="partialSelected" @update:checked="toggleSelectAll" />
|
||||
</th>
|
||||
<th>策略标识</th>
|
||||
<th>策略名称</th>
|
||||
<th>运行状态</th>
|
||||
<th>白名单</th>
|
||||
<th>白名单状态</th>
|
||||
<th>PID</th>
|
||||
<th>运行时长</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(info, key) in strategies" :key="key" :class="{ 'n-data-table-tr--selected': selectedKeys.includes(key) }">
|
||||
<td>
|
||||
<n-checkbox :checked="selectedKeys.includes(key)" @update:checked="toggleSelect(key)" />
|
||||
</td>
|
||||
<td><strong>{{ key }}</strong></td>
|
||||
<td>{{ info.config.name }} <br><small style="color:#999">{{ info.symbol }}</small></td>
|
||||
<td>
|
||||
<n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small">
|
||||
{{ info.status === 'running' ? '运行中' : '已停止' }}
|
||||
</n-tag>
|
||||
</td>
|
||||
<td>
|
||||
<n-tag :type="info.in_whitelist ? 'success' : 'default'" size="small" class="whitelist-tag"
|
||||
@click="toggleWhitelist(key)">
|
||||
{{ info.in_whitelist ? '✓ 在白名单' : '✗ 不在' }}
|
||||
</n-tag>
|
||||
</td>
|
||||
<td>
|
||||
<n-tag v-if="info.in_whitelist" :type="info.whitelist_enabled ? 'success' : 'warning'" size="small">
|
||||
{{ info.whitelist_enabled ? '启用' : '禁用' }}
|
||||
</n-tag>
|
||||
<span v-else style="color: #999;">-</span>
|
||||
</td>
|
||||
<td>{{ info.pid || '-' }}</td>
|
||||
<td>{{ info.uptime || '-' }}</td>
|
||||
<td>
|
||||
<n-space>
|
||||
<n-button v-if="info.status === 'stopped'" type="success" size="small" ghost @click="handleAction(key, 'start')">启动</n-button>
|
||||
<n-button v-if="info.status === 'running'" type="error" size="small" ghost @click="handleAction(key, 'stop')">停止</n-button>
|
||||
<n-button v-if="info.status === 'running'" type="warning" size="small" ghost @click="handleAction(key, 'restart')">重启</n-button>
|
||||
<n-button size="small" @click="viewLogs(key)">日志</n-button>
|
||||
</n-space>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="Object.keys(strategies).length === 0">
|
||||
<td colspan="9" style="text-align: center; padding: 30px; color: #999;">暂无策略</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-card>
|
||||
<!-- 策略列表 - 分为两个Tab -->
|
||||
<n-tabs type="line" animated class="strategy-tabs">
|
||||
<!-- 白名单策略Tab -->
|
||||
<n-tab-pane name="whitelist" tab="✅ 白名单策略" :tab-style="{ paddingLeft: '16px', paddingRight: '16px' }">
|
||||
<n-card hoverable :bordered="false">
|
||||
<template #header-extra>
|
||||
<n-space align="center">
|
||||
<n-button text @click="selectAllWhitelist" size="small">全选</n-button>
|
||||
<n-button text @click="clearSelection" size="small">清空</n-button>
|
||||
<n-tag type="success" size="small">{{ Object.keys(whitelistStrategies).length }} 个</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<n-table :single-line="false" striped size="small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 48px; text-align: center;">
|
||||
<n-checkbox :checked="allWhitelistSelected" :indeterminate="partialWhitelistSelected" @update:checked="toggleSelectAllWhitelist" />
|
||||
</th>
|
||||
<th>策略标识</th>
|
||||
<th>策略名称</th>
|
||||
<th style="width: 100px;">运行状态</th>
|
||||
<th style="width: 100px;">白名单状态</th>
|
||||
<th style="width: 80px;">PID</th>
|
||||
<th style="width: 100px;">运行时长</th>
|
||||
<th style="width: 240px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(info, key) in whitelistStrategies" :key="key" :class="{ 'n-data-table-tr--selected': selectedKeys.includes(key) }">
|
||||
<td style="text-align: center;">
|
||||
<n-checkbox :checked="selectedKeys.includes(key)" @update:checked="toggleSelect(key)" />
|
||||
</td>
|
||||
<td><span class="strategy-name">{{ key }}</span></td>
|
||||
<td>
|
||||
<span class="strategy-name">{{ info.config.name }}</span>
|
||||
<br>
|
||||
<span class="strategy-info">{{ info.symbol }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small" class="status-tag">
|
||||
{{ info.status === 'running' ? '运行中' : '已停止' }}
|
||||
</n-tag>
|
||||
</td>
|
||||
<td>
|
||||
<n-tag :type="info.whitelist_enabled ? 'success' : 'warning'" size="small" class="status-tag">
|
||||
{{ info.whitelist_enabled ? '启用' : '禁用' }}
|
||||
</n-tag>
|
||||
</td>
|
||||
<td>{{ info.pid || '-' }}</td>
|
||||
<td>{{ info.uptime || '-' }}</td>
|
||||
<td>
|
||||
<n-space size="small">
|
||||
<n-button v-if="info.status === 'stopped'" type="success" size="small" ghost @click="handleAction(key, 'start')">启动</n-button>
|
||||
<n-button v-if="info.status === 'running'" type="error" size="small" ghost @click="handleAction(key, 'stop')">停止</n-button>
|
||||
<n-button v-if="info.status === 'running'" type="warning" size="small" ghost @click="handleAction(key, 'restart')">重启</n-button>
|
||||
<n-button size="small" @click="viewLogs(key)">日志</n-button>
|
||||
<n-button size="small" @click="removeFromWhitelist(key)" type="warning" quaternary>移出</n-button>
|
||||
</n-space>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="Object.keys(whitelistStrategies).length === 0">
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div>白名单中暂无策略</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-card>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 非白名单策略Tab -->
|
||||
<n-tab-pane name="non-whitelist" tab="❌ 非白名单策略" :tab-style="{ paddingLeft: '16px', paddingRight: '16px' }">
|
||||
<n-card hoverable :bordered="false">
|
||||
<template #header-extra>
|
||||
<n-space align="center">
|
||||
<n-button text @click="selectAllNonWhitelist" size="small">全选</n-button>
|
||||
<n-button text @click="clearSelection" size="small">清空</n-button>
|
||||
<n-tag type="default" size="small">{{ Object.keys(nonWhitelistStrategies).length }} 个</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<n-table :single-line="false" striped size="small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 48px; text-align: center;">
|
||||
<n-checkbox :checked="allNonWhitelistSelected" :indeterminate="partialNonWhitelistSelected" @update:checked="toggleSelectAllNonWhitelist" />
|
||||
</th>
|
||||
<th>策略标识</th>
|
||||
<th>策略名称</th>
|
||||
<th style="width: 100px;">运行状态</th>
|
||||
<th style="width: 100px;">白名单</th>
|
||||
<th style="width: 80px;">PID</th>
|
||||
<th style="width: 100px;">运行时长</th>
|
||||
<th style="width: 240px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(info, key) in nonWhitelistStrategies" :key="key" :class="{ 'n-data-table-tr--selected': selectedKeys.includes(key) }">
|
||||
<td style="text-align: center;">
|
||||
<n-checkbox :checked="selectedKeys.includes(key)" @update:checked="toggleSelect(key)" />
|
||||
</td>
|
||||
<td><span class="strategy-name">{{ key }}</span></td>
|
||||
<td>
|
||||
<span class="strategy-name">{{ info.config.name }}</span>
|
||||
<br>
|
||||
<span class="strategy-info">{{ info.symbol }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small" class="status-tag">
|
||||
{{ info.status === 'running' ? '运行中' : '已停止' }}
|
||||
</n-tag>
|
||||
</td>
|
||||
<td>
|
||||
<n-tag type="default" size="small" class="status-tag">✗ 不在</n-tag>
|
||||
</td>
|
||||
<td>{{ info.pid || '-' }}</td>
|
||||
<td>{{ info.uptime || '-' }}</td>
|
||||
<td>
|
||||
<n-space size="small">
|
||||
<n-button v-if="info.status === 'stopped'" type="success" size="small" ghost @click="handleAction(key, 'start')">启动</n-button>
|
||||
<n-button v-if="info.status === 'running'" type="error" size="small" ghost @click="handleAction(key, 'stop')">停止</n-button>
|
||||
<n-button v-if="info.status === 'running'" type="warning" size="small" ghost @click="handleAction(key, 'restart')">重启</n-button>
|
||||
<n-button size="small" @click="viewLogs(key)">日志</n-button>
|
||||
<n-button size="small" @click="addToWhitelist(key)" type="success" quaternary>加入</n-button>
|
||||
</n-space>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="Object.keys(nonWhitelistStrategies).length === 0">
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">✅</div>
|
||||
<div>所有策略都已在白名单中</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-card>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<!-- 日志弹窗 -->
|
||||
<n-modal v-model:show="showLogModal" style="width: 900px;" preset="card" :title="'📜 实时日志: ' + currentLogKey">
|
||||
<n-modal v-model:show="showLogModal" style="width: 1000px;" preset="card" :title="'📜 实时日志: ' + currentLogKey" :bordered="false">
|
||||
<div class="log-container" id="logBox">
|
||||
<div v-if="logLoading" style="text-align:center; padding:20px;"><n-spin size="medium" /></div>
|
||||
<div v-if="logLoading" style="text-align:center; padding:40px;"><n-spin size="medium" /></div>
|
||||
<div v-else v-for="(line, index) in logLines" :key="index" class="log-line" :class="getLogClass(line)">{{ line }}</div>
|
||||
<div v-if="!logLoading && logLines.length === 0" class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div>暂无日志内容</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button size="small" @click="fetchLogs(currentLogKey)">刷新</n-button>
|
||||
<n-button size="small" @click="showLogModal = false">关闭</n-button>
|
||||
</n-space>
|
||||
<n-button size="small" @click="fetchLogs(currentLogKey)">刷新</n-button>
|
||||
<n-button size="small" @click="showLogModal = false">关闭</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
@@ -241,6 +550,45 @@
|
||||
const stoppedCount = computed(() => Object.values(strategies.value).filter(s => s.status === 'stopped').length);
|
||||
const whitelistCount = computed(() => Object.values(strategies.value).filter(s => s.in_whitelist).length);
|
||||
|
||||
// 分离白名单和非白名单策略
|
||||
const whitelistStrategies = computed(() => {
|
||||
const result = {};
|
||||
for (const [key, info] of Object.entries(strategies.value)) {
|
||||
if (info.in_whitelist) result[key] = info;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const nonWhitelistStrategies = computed(() => {
|
||||
const result = {};
|
||||
for (const [key, info] of Object.entries(strategies.value)) {
|
||||
if (!info.in_whitelist) result[key] = info;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
// 白名单策略选择状态
|
||||
const allWhitelistSelected = computed(() => {
|
||||
const keys = Object.keys(whitelistStrategies.value);
|
||||
return keys.length > 0 && keys.every(k => selectedKeys.value.includes(k));
|
||||
});
|
||||
const partialWhitelistSelected = computed(() => {
|
||||
const keys = Object.keys(whitelistStrategies.value);
|
||||
const selected = keys.filter(k => selectedKeys.value.includes(k));
|
||||
return selected.length > 0 && selected.length < keys.length;
|
||||
});
|
||||
|
||||
// 非白名单策略选择状态
|
||||
const allNonWhitelistSelected = computed(() => {
|
||||
const keys = Object.keys(nonWhitelistStrategies.value);
|
||||
return keys.length > 0 && keys.every(k => selectedKeys.value.includes(k));
|
||||
});
|
||||
const partialNonWhitelistSelected = computed(() => {
|
||||
const keys = Object.keys(nonWhitelistStrategies.value);
|
||||
const selected = keys.filter(k => selectedKeys.value.includes(k));
|
||||
return selected.length > 0 && selected.length < keys.length;
|
||||
});
|
||||
|
||||
const allSelected = computed(() => selectedKeys.value.length > 0 && selectedKeys.value.length === Object.keys(strategies.value).length);
|
||||
const partialSelected = computed(() => selectedKeys.value.length > 0 && selectedKeys.value.length < Object.keys(strategies.value).length);
|
||||
|
||||
@@ -308,6 +656,28 @@
|
||||
selectedKeys.value = [];
|
||||
};
|
||||
|
||||
// 白名单Tab选择函数
|
||||
const selectAllWhitelist = () => {
|
||||
selectedKeys.value = [...new Set([...selectedKeys.value, ...Object.keys(whitelistStrategies.value)])];
|
||||
};
|
||||
const selectAllNonWhitelist = () => {
|
||||
selectedKeys.value = [...new Set([...selectedKeys.value, ...Object.keys(nonWhitelistStrategies.value)])];
|
||||
};
|
||||
const toggleSelectAllWhitelist = (checked) => {
|
||||
if (checked) {
|
||||
selectedKeys.value = [...new Set([...selectedKeys.value, ...Object.keys(whitelistStrategies.value)])];
|
||||
} else {
|
||||
selectedKeys.value = selectedKeys.value.filter(k => !(k in whitelistStrategies.value));
|
||||
}
|
||||
};
|
||||
const toggleSelectAllNonWhitelist = (checked) => {
|
||||
if (checked) {
|
||||
selectedKeys.value = [...new Set([...selectedKeys.value, ...Object.keys(nonWhitelistStrategies.value)])];
|
||||
} else {
|
||||
selectedKeys.value = selectedKeys.value.filter(k => !(k in nonWhitelistStrategies.value));
|
||||
}
|
||||
};
|
||||
|
||||
// 策略操作
|
||||
const handleAction = (name, action) => {
|
||||
const map = { start: '启动', stop: '停止', restart: '重启' };
|
||||
@@ -461,6 +831,36 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 单个策略加入白名单(无弹窗确认)
|
||||
const addToWhitelist = async (name) => {
|
||||
try {
|
||||
const res = await fetch(`/api/whitelist/${name}/add`, { method: 'POST' });
|
||||
if (res.ok) {
|
||||
message.success(`已添加到白名单: ${name}`);
|
||||
fetchStatus();
|
||||
} else {
|
||||
message.error("添加失败");
|
||||
}
|
||||
} catch (e) {
|
||||
message.error("操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 单个策略移出白名单(无弹窗确认)
|
||||
const removeFromWhitelist = async (name) => {
|
||||
try {
|
||||
const res = await fetch(`/api/whitelist/${name}/remove`, { method: 'POST' });
|
||||
if (res.ok) {
|
||||
message.success(`已从白名单移除: ${name}`);
|
||||
fetchStatus();
|
||||
} else {
|
||||
message.error("移除失败");
|
||||
}
|
||||
} catch (e) {
|
||||
message.error("操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
const triggerAutoStart = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/whitelist/auto-start', { method: 'POST' });
|
||||
@@ -525,16 +925,26 @@
|
||||
showLogModal, currentLogKey, logLines, logLoading,
|
||||
fetchStatus, handleAction, viewLogs, fetchLogs, getLogClass,
|
||||
|
||||
// 分离的策略列表
|
||||
whitelistStrategies, nonWhitelistStrategies,
|
||||
|
||||
// 分离的选择状态
|
||||
allWhitelistSelected, partialWhitelistSelected,
|
||||
allNonWhitelistSelected, partialNonWhitelistSelected,
|
||||
|
||||
// 白名单
|
||||
whitelistAutoStarted,
|
||||
selectedKeys,
|
||||
runningCount, stoppedCount, whitelistCount,
|
||||
allSelected, partialSelected,
|
||||
toggleSelect, toggleSelectAll, selectAll, clearSelection,
|
||||
selectAllWhitelist, selectAllNonWhitelist,
|
||||
toggleSelectAllWhitelist, toggleSelectAllNonWhitelist,
|
||||
batchStart, batchStop, batchRestart,
|
||||
batchAddToWhitelist, batchRemoveFromWhitelist,
|
||||
batchEnableInWhitelist, batchDisableInWhitelist,
|
||||
toggleWhitelist, triggerAutoStart
|
||||
toggleWhitelist, triggerAutoStart,
|
||||
addToWhitelist, removeFromWhitelist
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
28
strategy_manager/strategies/FisherTrendStrategy/FG.py
Normal file
28
strategy_manager/strategies/FisherTrendStrategy/FG.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# 策略配置(Python格式)
|
||||
from src.indicators.indicators import Hurst
|
||||
|
||||
CONFIG = {
|
||||
"name": "PragmaticCyberneticStrategy策略",
|
||||
"version": "1.0",
|
||||
"enabled": True,
|
||||
|
||||
"strategy_class": "futures_trading_strategies.FG.FisherTrendStrategy.Strategy3.PragmaticCyberneticStrategy",
|
||||
|
||||
"engine_params": {
|
||||
"symbol": "KQ.m@CZCE.FG",
|
||||
"duration_seconds": 900,
|
||||
"roll_over_mode": True,
|
||||
"history_length": 1000,
|
||||
# 支持Python对象
|
||||
"close_bar_delta": __import__('datetime').timedelta(minutes=58)
|
||||
},
|
||||
|
||||
"strategy_params": {
|
||||
'main_symbol': 'FG',
|
||||
'trade_volume': 1,
|
||||
'enable_log': True,
|
||||
'fb_entry_threshold': 0.1,
|
||||
'fisher_exit_level': 1.7,
|
||||
'indicator': Hurst(115, 0.51, 0.6)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user