新增实盘策略:FisherTrendStrategy(FG)

This commit is contained in:
2026-02-25 23:54:24 +08:00
parent c0d996f39b
commit 05adafeb4e
28 changed files with 16221 additions and 1982 deletions

177
AGENTS.md Normal file
View 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

View 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))

View 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 修复后的 FB1.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))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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 修复后的 FB1.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

View 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))

View 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 修复后的 FB1.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))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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 修复后的 FB1.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

File diff suppressed because one or more lines are too long

View File

@@ -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}')

View File

@@ -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

View File

@@ -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))

File diff suppressed because one or more lines are too long

View File

@@ -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)

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
};
}
};

View 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)
}
}