qka 1.0.3.dev1__tar.gz → 1.0.4.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/PKG-INFO +1 -1
- qka-1.0.4.dev1/docs/user-guide/backtest.md +465 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/examples/simple_backtest_demo.py +5 -8
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/mkdocs.yml +0 -1
- qka-1.0.4.dev1/qka/__init__.py +40 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/core/__init__.py +2 -2
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/core/backtest.py +328 -357
- qka-1.0.4.dev1/qka/core/plot.py +218 -0
- qka-1.0.3.dev1/docs/user-guide/backtest.md +0 -418
- qka-1.0.3.dev1/qka/__init__.py +0 -24
- qka-1.0.3.dev1/qka/core/data/config.py +0 -40
- qka-1.0.3.dev1/qka/core/data/factory.py +0 -37
- qka-1.0.3.dev1/qka/core/data/sources.py +0 -40
- qka-1.0.3.dev1/qka/core/plot.py +0 -90
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/.github/workflows/docs.yml +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/.github/workflows/release.yml +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/.gitignore +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/.vscode/settings.json +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/CHANGELOG.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/LICENSE +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/README.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/A/350/202/241/345/270/202/345/234/272/350/257/201/345/210/270/344/273/243/347/240/201/345/221/275/345/220/215/350/247/204/345/210/231.md" +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/brokers/index.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/core/config.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/core/events.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/core/index.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/index.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/mcp/index.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/utils/index.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/utils/logger.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/api/utils/tools.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/enhanced_features_phase1.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/examples/index.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/getting-started/concepts.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/getting-started/first-strategy.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/getting-started/installation.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/index.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/user-guide/config.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/user-guide/data.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/user-guide/events.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/user-guide/logging.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/user-guide/strategy.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/docs/user-guide/trading.md +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/examples/basic_test.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/examples/enhanced_features_demo.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/examples/simple_demo.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/pyproject.toml +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/brokers/__init__.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/brokers/client.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/brokers/server.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/brokers/trade.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/core/config.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/core/data/__init__.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/core/data/akshare.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/core/data/base.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/core/data/qmt.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/core/events.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/mcp/__init__.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/mcp/api.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/mcp/server.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/utils/__init__.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/utils/anis.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/utils/logger.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/utils/tools.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}/qka/utils/util.py +0 -0
- {qka-1.0.3.dev1 → qka-1.0.4.dev1}//345/267/245/344/275/234/350/277/233/345/272/246/350/256/260/345/275/225.md" +0 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# 回测分析
|
|
2
|
+
|
|
3
|
+
QKA 提供了简洁易用的回测功能,帮助您快速验证交易策略的有效性。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
### 三步完成回测
|
|
8
|
+
|
|
9
|
+
QKA 回测非常简单,只需三步:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import qka
|
|
13
|
+
|
|
14
|
+
# 第1步:获取数据
|
|
15
|
+
data_obj = qka.data(stocks=['000001', '000002']) # 默认使用akshare数据源
|
|
16
|
+
|
|
17
|
+
# 第2步:定义策略
|
|
18
|
+
class SimpleStrategy(qka.Strategy):
|
|
19
|
+
def on_bar(self, data, broker, current_date):
|
|
20
|
+
for symbol, df in data.items():
|
|
21
|
+
if len(df) >= 20:
|
|
22
|
+
price = df['close'].iloc[-1]
|
|
23
|
+
ma20 = df['close'].rolling(20).mean().iloc[-1]
|
|
24
|
+
|
|
25
|
+
if price > ma20: # 突破均线买入
|
|
26
|
+
broker.buy(symbol, 0.5, price)
|
|
27
|
+
elif price < ma20: # 跌破均线卖出
|
|
28
|
+
broker.sell(symbol, 1.0, price)
|
|
29
|
+
|
|
30
|
+
# 第3步:运行回测并查看结果
|
|
31
|
+
result = qka.backtest(data_obj, SimpleStrategy(), start_time='2023-01-01', end_time='2023-12-31')
|
|
32
|
+
|
|
33
|
+
print(f"总收益率: {result['total_return']:.2%}")
|
|
34
|
+
print(f"年化收益率: {result['annual_return']:.2%}")
|
|
35
|
+
print(f"夏普比率: {result['sharpe_ratio']:.2f}")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> **数据源说明**: 默认使用akshare数据源。如需使用其他数据源,可以调用 `set_source('qmt')` 或在 `data()` 函数中指定 `source` 参数。
|
|
39
|
+
|
|
40
|
+
## 策略开发
|
|
41
|
+
|
|
42
|
+
### 策略基类
|
|
43
|
+
|
|
44
|
+
所有策略都需要继承 `qka.Strategy` 基类:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import qka
|
|
48
|
+
|
|
49
|
+
class MyStrategy(qka.Strategy):
|
|
50
|
+
def on_bar(self, data, broker, current_date):
|
|
51
|
+
"""
|
|
52
|
+
每个交易日调用的策略逻辑
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
data: 历史数据字典 {股票代码: DataFrame}
|
|
56
|
+
broker: 交易接口
|
|
57
|
+
current_date: 当前日期
|
|
58
|
+
"""
|
|
59
|
+
# 在这里实现你的策略逻辑
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def on_start(self, broker):
|
|
63
|
+
"""回测开始时调用"""
|
|
64
|
+
print(f"策略 {self.name} 开始运行")
|
|
65
|
+
|
|
66
|
+
def on_end(self, broker):
|
|
67
|
+
"""回测结束时调用"""
|
|
68
|
+
print(f"策略 {self.name} 运行结束")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 策略示例
|
|
72
|
+
|
|
73
|
+
#### 移动平均策略
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
class MovingAverageStrategy(qka.Strategy):
|
|
77
|
+
def __init__(self, short_window=5, long_window=20):
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.short_window = short_window
|
|
80
|
+
self.long_window = long_window
|
|
81
|
+
|
|
82
|
+
def on_bar(self, data, broker, current_date):
|
|
83
|
+
for symbol, df in data.items():
|
|
84
|
+
if len(df) < self.long_window:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# 计算短期和长期移动平均
|
|
88
|
+
short_ma = df['close'].rolling(self.short_window).mean().iloc[-1]
|
|
89
|
+
long_ma = df['close'].rolling(self.long_window).mean().iloc[-1]
|
|
90
|
+
current_price = df['close'].iloc[-1]
|
|
91
|
+
|
|
92
|
+
# 金叉买入,死叉卖出
|
|
93
|
+
if short_ma > long_ma and broker.get_position(symbol) == 0:
|
|
94
|
+
broker.buy(symbol, 0.3, current_price) # 用30%资金买入
|
|
95
|
+
elif short_ma < long_ma and broker.get_position(symbol) > 0:
|
|
96
|
+
broker.sell(symbol, 1.0, current_price) # 全部卖出
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### 布林带策略
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
class BollingerBandStrategy(qka.Strategy):
|
|
103
|
+
def __init__(self, window=20, num_std=2):
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.window = window
|
|
106
|
+
self.num_std = num_std
|
|
107
|
+
|
|
108
|
+
def on_bar(self, data, broker, current_date):
|
|
109
|
+
for symbol, df in data.items():
|
|
110
|
+
if len(df) < self.window:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# 计算布林带
|
|
114
|
+
close_prices = df['close']
|
|
115
|
+
rolling_mean = close_prices.rolling(self.window).mean().iloc[-1]
|
|
116
|
+
rolling_std = close_prices.rolling(self.window).std().iloc[-1]
|
|
117
|
+
|
|
118
|
+
upper_band = rolling_mean + (rolling_std * self.num_std)
|
|
119
|
+
lower_band = rolling_mean - (rolling_std * self.num_std)
|
|
120
|
+
current_price = close_prices.iloc[-1]
|
|
121
|
+
|
|
122
|
+
# 价格突破下轨买入,突破上轨卖出
|
|
123
|
+
if current_price < lower_band and broker.get_position(symbol) == 0:
|
|
124
|
+
broker.buy(symbol, 0.4, current_price)
|
|
125
|
+
elif current_price > upper_band and broker.get_position(symbol) > 0:
|
|
126
|
+
broker.sell(symbol, 1.0, current_price)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 交易接口
|
|
130
|
+
|
|
131
|
+
### Broker 类
|
|
132
|
+
|
|
133
|
+
`Broker` 类提供了所有交易相关的功能:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# 获取当前持仓
|
|
137
|
+
position = broker.get_position('000001') # 返回持仓股数
|
|
138
|
+
|
|
139
|
+
# 获取可用现金
|
|
140
|
+
cash = broker.get_cash()
|
|
141
|
+
|
|
142
|
+
# 获取所有持仓
|
|
143
|
+
positions = broker.get_positions() # 返回 {股票代码: 持仓数量}
|
|
144
|
+
|
|
145
|
+
# 计算总资产
|
|
146
|
+
prices = {'000001': 10.5, '000002': 8.3}
|
|
147
|
+
total_value = broker.get_total_value(prices)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 买入操作
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# 按比例买入(推荐)
|
|
154
|
+
broker.buy('000001', 0.3, price) # 用30%的资金买入
|
|
155
|
+
|
|
156
|
+
# 按股数买入
|
|
157
|
+
broker.buy('000001', 1000, price) # 买入1000股(自动调整为整手)
|
|
158
|
+
|
|
159
|
+
# 买入条件检查
|
|
160
|
+
if broker.get_cash() > 10000: # 现金充足
|
|
161
|
+
if broker.get_position('000001') == 0: # 没有持仓
|
|
162
|
+
broker.buy('000001', 0.2, current_price)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 卖出操作
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# 按比例卖出
|
|
169
|
+
broker.sell('000001', 0.5, price) # 卖出50%的持仓
|
|
170
|
+
broker.sell('000001', 1.0, price) # 全部卖出
|
|
171
|
+
|
|
172
|
+
# 按股数卖出
|
|
173
|
+
broker.sell('000001', 500, price) # 卖出500股
|
|
174
|
+
|
|
175
|
+
# 卖出条件检查
|
|
176
|
+
if broker.get_position('000001') > 0: # 有持仓
|
|
177
|
+
broker.sell('000001', 1.0, current_price) # 全部卖出
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 回测配置
|
|
181
|
+
|
|
182
|
+
### 基本配置
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import qka
|
|
186
|
+
|
|
187
|
+
# 自定义 Broker 配置
|
|
188
|
+
custom_broker = qka.Broker(
|
|
189
|
+
initial_cash=500000, # 初始资金50万
|
|
190
|
+
commission_rate=0.0003 # 手续费率0.03%
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# 使用自定义配置运行回测
|
|
194
|
+
result = qka.backtest(
|
|
195
|
+
data=data_obj,
|
|
196
|
+
strategy=MyStrategy(),
|
|
197
|
+
broker=custom_broker,
|
|
198
|
+
start_time='2023-01-01',
|
|
199
|
+
end_time='2023-12-31'
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 数据获取
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import qka
|
|
207
|
+
|
|
208
|
+
# 使用默认数据源(akshare)
|
|
209
|
+
data_obj = qka.data(stocks=['000001'])
|
|
210
|
+
|
|
211
|
+
# 多只股票
|
|
212
|
+
data_obj = qka.data(stocks=['000001', '000002', '600000'])
|
|
213
|
+
|
|
214
|
+
# 设置全局数据源
|
|
215
|
+
qka.set_source('qmt')
|
|
216
|
+
data_obj = qka.data(stocks=['000001.SZ', '600000.SH']) # QMT格式股票代码
|
|
217
|
+
|
|
218
|
+
# 临时指定数据源
|
|
219
|
+
data_obj = qka.data(stocks=['000001'], source='akshare')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## 回测结果分析
|
|
223
|
+
|
|
224
|
+
### 基本指标
|
|
225
|
+
|
|
226
|
+
回测结果包含以下关键指标:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
# 基本收益指标
|
|
230
|
+
print(f"初始资金: {result['initial_capital']:,.0f}")
|
|
231
|
+
print(f"最终资产: {result['final_value']:,.0f}")
|
|
232
|
+
print(f"总收益率: {result['total_return']:.2%}")
|
|
233
|
+
print(f"年化收益率: {result['annual_return']:.2%}")
|
|
234
|
+
|
|
235
|
+
# 风险指标
|
|
236
|
+
print(f"收益波动率: {result['volatility']:.2%}")
|
|
237
|
+
print(f"夏普比率: {result['sharpe_ratio']:.2f}")
|
|
238
|
+
print(f"最大回撤: {result['max_drawdown']:.2%}")
|
|
239
|
+
|
|
240
|
+
# 交易指标
|
|
241
|
+
print(f"总交易次数: {result['total_trades']}")
|
|
242
|
+
print(f"总手续费: {result['total_commission']:,.2f}")
|
|
243
|
+
print(f"胜率: {result['win_rate']:.2%}")
|
|
244
|
+
print(f"交易天数: {result['trading_days']}")
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 详细数据
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
# 每日净值数据
|
|
251
|
+
daily_values = result['daily_values']
|
|
252
|
+
for record in daily_values[:5]: # 显示前5天
|
|
253
|
+
print(f"日期: {record['date']}, 总资产: {record['total_value']:,.2f}")
|
|
254
|
+
|
|
255
|
+
# 交易记录
|
|
256
|
+
trades = result['trades']
|
|
257
|
+
for trade in trades[:5]: # 显示前5笔交易
|
|
258
|
+
print(f"{trade['date']}: {trade['action']} {trade['symbol']} "
|
|
259
|
+
f"{trade['shares']}股 @{trade['price']}")
|
|
260
|
+
|
|
261
|
+
# 最终持仓
|
|
262
|
+
positions = result['positions']
|
|
263
|
+
print(f"最终持仓: {positions}")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## 结果可视化
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
import qka
|
|
270
|
+
|
|
271
|
+
# 绘制回测结果图表
|
|
272
|
+
qka.plot(result)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## 策略开发技巧
|
|
276
|
+
|
|
277
|
+
### 数据处理
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
def on_bar(self, data, broker, current_date):
|
|
281
|
+
for symbol, df in data.items():
|
|
282
|
+
# 检查数据长度
|
|
283
|
+
if len(df) < 20:
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
# 获取最新价格
|
|
287
|
+
current_price = df['close'].iloc[-1]
|
|
288
|
+
|
|
289
|
+
# 计算技术指标
|
|
290
|
+
sma_20 = df['close'].rolling(20).mean().iloc[-1]
|
|
291
|
+
rsi = self.calculate_rsi(df['close'], 14)
|
|
292
|
+
|
|
293
|
+
# 处理缺失值
|
|
294
|
+
if pd.isna(sma_20) or pd.isna(rsi):
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# 策略逻辑
|
|
298
|
+
if rsi < 30 and current_price > sma_20:
|
|
299
|
+
broker.buy(symbol, 0.2, current_price)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 风险控制
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
def on_bar(self, data, broker, current_date):
|
|
306
|
+
for symbol, df in data.items():
|
|
307
|
+
current_price = df['close'].iloc[-1]
|
|
308
|
+
position = broker.get_position(symbol)
|
|
309
|
+
|
|
310
|
+
# 止损逻辑
|
|
311
|
+
if position > 0:
|
|
312
|
+
avg_cost = broker.avg_costs.get(symbol, current_price)
|
|
313
|
+
if current_price < avg_cost * 0.95: # 5%止损
|
|
314
|
+
broker.sell(symbol, 1.0, current_price)
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
# 仓位控制
|
|
318
|
+
total_value = broker.get_total_value({symbol: current_price})
|
|
319
|
+
position_value = position * current_price
|
|
320
|
+
position_ratio = position_value / total_value
|
|
321
|
+
|
|
322
|
+
if position_ratio > 0.3: # 单只股票最大30%仓位
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
# 买入逻辑
|
|
326
|
+
# ...
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 多股票策略
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
class MultiStockStrategy(qka.Strategy):
|
|
333
|
+
def on_bar(self, data, broker, current_date):
|
|
334
|
+
# 收集所有股票的信号
|
|
335
|
+
signals = {}
|
|
336
|
+
for symbol, df in data.items():
|
|
337
|
+
if len(df) >= 20:
|
|
338
|
+
signal = self.calculate_signal(df)
|
|
339
|
+
signals[symbol] = signal
|
|
340
|
+
|
|
341
|
+
# 按信号强度排序
|
|
342
|
+
sorted_signals = sorted(signals.items(),
|
|
343
|
+
key=lambda x: x[1], reverse=True)
|
|
344
|
+
|
|
345
|
+
# 只选择前3只股票
|
|
346
|
+
selected_stocks = sorted_signals[:3]
|
|
347
|
+
|
|
348
|
+
# 平均分配资金
|
|
349
|
+
for symbol, signal in selected_stocks:
|
|
350
|
+
if signal > 0.5 and broker.get_position(symbol) == 0:
|
|
351
|
+
broker.buy(symbol, 0.3, data[symbol]['close'].iloc[-1])
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## 最佳实践
|
|
355
|
+
|
|
356
|
+
### 1. 数据质量检查
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
def on_bar(self, data, broker, current_date):
|
|
360
|
+
for symbol, df in data.items():
|
|
361
|
+
# 检查价格合理性
|
|
362
|
+
current_price = df['close'].iloc[-1]
|
|
363
|
+
if current_price <= 0 or pd.isna(current_price):
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
# 检查成交量
|
|
367
|
+
volume = df['volume'].iloc[-1]
|
|
368
|
+
if volume <= 0:
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
# 策略逻辑
|
|
372
|
+
# ...
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### 2. 参数化策略
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
class ParameterizedStrategy(qka.Strategy):
|
|
379
|
+
def __init__(self, ma_period=20, position_size=0.3, stop_loss=0.05):
|
|
380
|
+
super().__init__()
|
|
381
|
+
self.ma_period = ma_period
|
|
382
|
+
self.position_size = position_size
|
|
383
|
+
self.stop_loss = stop_loss
|
|
384
|
+
|
|
385
|
+
def on_bar(self, data, broker, current_date):
|
|
386
|
+
for symbol, df in data.items():
|
|
387
|
+
if len(df) < self.ma_period:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
current_price = df['close'].iloc[-1]
|
|
391
|
+
ma = df['close'].rolling(self.ma_period).mean().iloc[-1]
|
|
392
|
+
|
|
393
|
+
# 使用参数化的逻辑
|
|
394
|
+
if current_price > ma:
|
|
395
|
+
broker.buy(symbol, self.position_size, current_price)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 3. 策略状态管理
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
class StatefulStrategy(qka.Strategy):
|
|
402
|
+
def __init__(self):
|
|
403
|
+
super().__init__()
|
|
404
|
+
self.last_signal = {}
|
|
405
|
+
self.entry_prices = {}
|
|
406
|
+
|
|
407
|
+
def on_bar(self, data, broker, current_date):
|
|
408
|
+
for symbol, df in data.items():
|
|
409
|
+
# 保存策略状态
|
|
410
|
+
current_signal = self.calculate_signal(df)
|
|
411
|
+
last_signal = self.last_signal.get(symbol, 0)
|
|
412
|
+
|
|
413
|
+
# 信号变化时才交易
|
|
414
|
+
if current_signal != last_signal:
|
|
415
|
+
if current_signal > 0:
|
|
416
|
+
price = df['close'].iloc[-1]
|
|
417
|
+
broker.buy(symbol, 0.3, price)
|
|
418
|
+
self.entry_prices[symbol] = price
|
|
419
|
+
else:
|
|
420
|
+
broker.sell(symbol, 1.0, df['close'].iloc[-1])
|
|
421
|
+
self.entry_prices.pop(symbol, None)
|
|
422
|
+
|
|
423
|
+
self.last_signal[symbol] = current_signal
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## 常见问题
|
|
427
|
+
|
|
428
|
+
### Q: 如何处理停牌股票?
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
def on_bar(self, data, broker, current_date):
|
|
432
|
+
for symbol, df in data.items():
|
|
433
|
+
# 检查是否停牌
|
|
434
|
+
if current_date not in df.index:
|
|
435
|
+
continue # 跳过停牌股票
|
|
436
|
+
|
|
437
|
+
current_price = df.loc[current_date, 'close']
|
|
438
|
+
if current_price == 0:
|
|
439
|
+
continue # 价格为0可能是停牌
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Q: 如何避免未来函数?
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
def on_bar(self, data, broker, current_date):
|
|
446
|
+
for symbol, df in data.items():
|
|
447
|
+
# 只使用当前日期之前的数据
|
|
448
|
+
historical_data = df.loc[:current_date]
|
|
449
|
+
|
|
450
|
+
# 计算指标时确保不使用未来数据
|
|
451
|
+
sma = historical_data['close'].rolling(20).mean().iloc[-1]
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Q: 如何处理数据缺失?
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
def on_bar(self, data, broker, current_date):
|
|
458
|
+
for symbol, df in data.items():
|
|
459
|
+
# 检查关键数据是否缺失
|
|
460
|
+
if df[['open', 'high', 'low', 'close', 'volume']].iloc[-1].isna().any():
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
# 使用前向填充处理缺失值
|
|
464
|
+
df_filled = df.fillna(method='ffill')
|
|
465
|
+
```
|
|
@@ -3,13 +3,10 @@ QKA 简化回测API演示 - 极简版本
|
|
|
3
3
|
只需3步:获取数据 -> 定义策略 -> 运行回测
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
from qka.core.data import data
|
|
8
|
-
from qka.core.backtest import backtest, Strategy
|
|
9
|
-
from qka.core.plot import plot_backtest
|
|
6
|
+
import qka
|
|
10
7
|
|
|
11
8
|
# 第1步:定义策略
|
|
12
|
-
class SimpleStrategy(Strategy):
|
|
9
|
+
class SimpleStrategy(qka.Strategy):
|
|
13
10
|
def on_bar(self, data, broker, current_date):
|
|
14
11
|
for symbol, df in data.items():
|
|
15
12
|
if len(df) < 20: # 需要足够的历史数据
|
|
@@ -28,17 +25,17 @@ class SimpleStrategy(Strategy):
|
|
|
28
25
|
# 第2步:获取数据并运行回测
|
|
29
26
|
if __name__ == "__main__":
|
|
30
27
|
# 获取数据
|
|
31
|
-
data_obj = data(
|
|
28
|
+
data_obj = qka.data(stocks=['000001', '000002']) # 使用默认数据源
|
|
32
29
|
|
|
33
30
|
# 运行回测
|
|
34
|
-
result = backtest(
|
|
31
|
+
result = qka.backtest(
|
|
35
32
|
data=data_obj,
|
|
36
33
|
strategy=SimpleStrategy(),
|
|
37
34
|
start_time='2023-01-01',
|
|
38
35
|
end_time='2023-12-31'
|
|
39
36
|
)
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
qka.plot(result)
|
|
42
39
|
|
|
43
40
|
# 第3步:查看结果
|
|
44
41
|
print(f"总收益率: {result['total_return']:.2%}")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QKA - 量化交易框架
|
|
3
|
+
|
|
4
|
+
统一的访问接口,支持 qka.xxx 的访问模式
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = version("qka")
|
|
11
|
+
except PackageNotFoundError:
|
|
12
|
+
__version__ = "0.1.0" # fallback version
|
|
13
|
+
|
|
14
|
+
# 核心功能直接导入
|
|
15
|
+
from qka.core.data import data, set_source, get_source, register_data_source, get_available_sources
|
|
16
|
+
from qka.core.backtest import backtest, Strategy, Broker
|
|
17
|
+
from qka.core.config import config, load_config
|
|
18
|
+
from qka.core.events import event_engine, emit_event
|
|
19
|
+
from qka.core.plot import plot
|
|
20
|
+
|
|
21
|
+
# 子模块导入
|
|
22
|
+
from qka import core, utils, mcp
|
|
23
|
+
|
|
24
|
+
# 交易相关(有依赖的模块暂时不导入,避免导入错误)
|
|
25
|
+
# from qka.brokers.trade import create_trader
|
|
26
|
+
# from qka.brokers.client import QMTClient
|
|
27
|
+
# from qka.brokers.server import QMTServer
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# 核心功能
|
|
31
|
+
'data', 'backtest', 'Strategy', 'Broker', 'plot',
|
|
32
|
+
# 配置
|
|
33
|
+
'config', 'load_config',
|
|
34
|
+
# 数据源管理
|
|
35
|
+
'set_source', 'get_source', 'register_data_source', 'get_available_sources',
|
|
36
|
+
# 事件系统
|
|
37
|
+
'event_engine', 'emit_event',
|
|
38
|
+
# 子模块
|
|
39
|
+
'core', 'utils', 'mcp'
|
|
40
|
+
]
|
|
@@ -7,11 +7,11 @@ from .data import data
|
|
|
7
7
|
from .backtest import backtest, Strategy
|
|
8
8
|
from .config import config, load_config
|
|
9
9
|
from .events import EventType, event_engine, emit_event, start_event_engine, stop_event_engine
|
|
10
|
-
from .plot import
|
|
10
|
+
from .plot import plot
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
13
|
'data', 'backtest', 'Strategy',
|
|
14
14
|
'config', 'load_config',
|
|
15
15
|
'EventType', 'event_engine', 'emit_event', 'start_event_engine', 'stop_event_engine',
|
|
16
|
-
'
|
|
16
|
+
'plot'
|
|
17
17
|
]
|