qka 1.6.1.dev4__tar.gz → 1.6.2.dev2__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.6.1.dev4 → qka-1.6.2.dev2}/.gitignore +3 -3
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/PKG-INFO +1 -1
- qka-1.6.2.dev2/docs/examples/buy_and_hold.md +79 -0
- qka-1.6.2.dev2/docs/examples/ma_cross.md +134 -0
- qka-1.6.2.dev2/docs/examples/momentum.md +87 -0
- qka-1.6.2.dev2/docs/examples/multi_factor.md +106 -0
- qka-1.6.2.dev2/docs/examples/rsi_atr.md +60 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/.github/workflows/docs.yml +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/.github/workflows/release.yml +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/.vscode/settings.json +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/CHANGELOG.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/LICENSE +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/README.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/advanced/performance.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/api/brokers.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/api/core.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/api/utils.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/concepts/backtest.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/concepts/broker.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/concepts/data.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/concepts/indicators.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/concepts/sizing.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/concepts/strategy.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/getting-started/quickstart.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/index.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/docs/user-guide/trading.md +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/mkdocs.yml +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/pyproject.toml +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/__init__.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/brokers/__init__.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/brokers/client.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/brokers/server.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/brokers/trade.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/cli.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/core/__init__.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/core/accessor.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/core/backtest.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/core/broker.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/core/data.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/core/report.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/core/sizing.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/core/strategy.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/mcp/__init__.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/mcp/api.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/mcp/server.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/server/__init__.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/server/handlers/__init__.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/server/handlers/class_inspector_handler.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/server/handlers/code_executor_handler.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/server/ws_client.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/server/zmq_server.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/utils/__init__.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/utils/anis.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/utils/logger.py +0 -0
- {qka-1.6.1.dev4 → qka-1.6.2.dev2}/qka/utils/util.py +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# 买入持有与定投
|
|
2
|
+
|
|
3
|
+
最简单的策略:买入,然后不动。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 买入持有
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
"""买入平安银行,一直持有到回测结束"""
|
|
11
|
+
from qka import Data, Strategy, Broker, Backtest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BuyAndHold(Strategy):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.broker = Broker(initial_cash=100_000)
|
|
18
|
+
self.bought = False
|
|
19
|
+
|
|
20
|
+
def on_bar(self, date):
|
|
21
|
+
close = self.get('close')
|
|
22
|
+
if close is None or close.empty:
|
|
23
|
+
return
|
|
24
|
+
if not self.bought and '000001.SZ' in close.index:
|
|
25
|
+
price = float(close['000001.SZ'])
|
|
26
|
+
if price > 0:
|
|
27
|
+
size = self.sizing.percent(0.5, price)
|
|
28
|
+
if size > 0:
|
|
29
|
+
self.broker.buy('000001.SZ', price, size)
|
|
30
|
+
self.bought = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == '__main__':
|
|
34
|
+
data = Data(symbols=['000001.SZ'])
|
|
35
|
+
bt = Backtest(data, BuyAndHold())
|
|
36
|
+
bt.run(benchmark='000300.SH')
|
|
37
|
+
bt.summary()
|
|
38
|
+
bt.report(title='买入持有 - 平安银行')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 定投
|
|
44
|
+
|
|
45
|
+
每月固定时间买入固定金额:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
"""每月 1 号买入 5000 块"""
|
|
49
|
+
from qka import Data, Strategy, Broker, Backtest
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MonthlyDCA(Strategy):
|
|
53
|
+
def __init__(self):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.broker = Broker(initial_cash=100_000)
|
|
56
|
+
|
|
57
|
+
def on_bar(self, date):
|
|
58
|
+
# 每月 1 号买入
|
|
59
|
+
if date.day != 1:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
close = self.get('close')
|
|
63
|
+
if close is None or close.empty:
|
|
64
|
+
return
|
|
65
|
+
if '000001.SZ' in close.index:
|
|
66
|
+
price = float(close['000001.SZ'])
|
|
67
|
+
if price > 0:
|
|
68
|
+
size = self.sizing.fixed_amount(5000, price)
|
|
69
|
+
if size > 0:
|
|
70
|
+
self.broker.buy('000001.SZ', price, size)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == '__main__':
|
|
74
|
+
data = Data(symbols=['000001.SZ'])
|
|
75
|
+
bt = Backtest(data, MonthlyDCA())
|
|
76
|
+
bt.run(benchmark='000300.SH')
|
|
77
|
+
bt.summary()
|
|
78
|
+
bt.report(title='定投 - 每月 1 号 5000 元')
|
|
79
|
+
```
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# 均线交叉
|
|
2
|
+
|
|
3
|
+
5 日均线上穿 20 日均线买入,下穿卖出。经典的趋势跟踪策略。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 单股票
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
"""5 日线上穿 20 日线买入,下穿卖出"""
|
|
11
|
+
from qka import Data, Strategy, Broker, Backtest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaCross(Strategy):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.broker = Broker(initial_cash=100_000)
|
|
18
|
+
|
|
19
|
+
def on_bar(self, date):
|
|
20
|
+
close = self.get('close')
|
|
21
|
+
if close is None or close.empty:
|
|
22
|
+
return
|
|
23
|
+
if '000001.SZ' not in close.index:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
# 取历史均线
|
|
27
|
+
ma5 = self.history('sma_5', 3)
|
|
28
|
+
ma20 = self.history('sma_20', 3)
|
|
29
|
+
|
|
30
|
+
if len(ma5) < 2 or len(ma20) < 2:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
price = float(close['000001.SZ'])
|
|
34
|
+
if price <= 0:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
# 今日 5 日线 > 20 日线,且昨天 5 日线 <= 20 日线(上穿)
|
|
38
|
+
if (ma5['000001.SZ'].iloc[-1] > ma20['000001.SZ'].iloc[-1] and
|
|
39
|
+
ma5['000001.SZ'].iloc[-2] <= ma20['000001.SZ'].iloc[-2]):
|
|
40
|
+
|
|
41
|
+
if '000001.SZ' not in self.broker.positions:
|
|
42
|
+
size = self.sizing.percent(0.5, price)
|
|
43
|
+
if size > 0:
|
|
44
|
+
self.broker.buy('000001.SZ', price, size)
|
|
45
|
+
|
|
46
|
+
# 下穿卖出
|
|
47
|
+
elif (ma5['000001.SZ'].iloc[-1] < ma20['000001.SZ'].iloc[-1] and
|
|
48
|
+
ma5['000001.SZ'].iloc[-2] >= ma20['000001.SZ'].iloc[-2]):
|
|
49
|
+
|
|
50
|
+
if '000001.SZ' in self.broker.positions:
|
|
51
|
+
pos = self.broker.positions['000001.SZ']
|
|
52
|
+
self.broker.sell('000001.SZ', price, pos['size'])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == '__main__':
|
|
56
|
+
data = Data(
|
|
57
|
+
symbols=['000001.SZ'],
|
|
58
|
+
indicators={
|
|
59
|
+
'sma_5': ('sma', 5),
|
|
60
|
+
'sma_20': ('sma', 20),
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
bt = Backtest(data, MaCross())
|
|
64
|
+
bt.run(benchmark='000300.SH')
|
|
65
|
+
bt.summary()
|
|
66
|
+
bt.report(title='均线交叉 - 平安银行')
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 多股票
|
|
72
|
+
|
|
73
|
+
每只股票独立判断,谁出信号买谁:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
"""多股票均线交叉,每只股票判断自己的信号"""
|
|
77
|
+
from qka import Data, Strategy, Broker, Backtest
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MultiMaCross(Strategy):
|
|
81
|
+
def __init__(self):
|
|
82
|
+
super().__init__()
|
|
83
|
+
self.broker = Broker(initial_cash=1_000_000)
|
|
84
|
+
|
|
85
|
+
def on_bar(self, date):
|
|
86
|
+
close = self.get('close')
|
|
87
|
+
if close is None or close.empty:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# 取历史均线(多股票的 DataFrame)
|
|
91
|
+
ma5 = self.history('sma_5', 3)
|
|
92
|
+
ma20 = self.history('sma_20', 3)
|
|
93
|
+
|
|
94
|
+
if len(ma5) < 2 or len(ma20) < 2:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
for sym in close.index:
|
|
98
|
+
price = float(close[sym])
|
|
99
|
+
if price <= 0:
|
|
100
|
+
continue
|
|
101
|
+
if sym not in ma5.columns or sym not in ma20.columns:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# 上穿买入
|
|
105
|
+
if (ma5[sym].iloc[-1] > ma20[sym].iloc[-1] and
|
|
106
|
+
ma5[sym].iloc[-2] <= ma20[sym].iloc[-2]):
|
|
107
|
+
|
|
108
|
+
if sym not in self.broker.positions:
|
|
109
|
+
size = self.sizing.percent(0.1, price)
|
|
110
|
+
if size > 0:
|
|
111
|
+
self.broker.buy(sym, price, size)
|
|
112
|
+
|
|
113
|
+
# 下穿卖出
|
|
114
|
+
elif (ma5[sym].iloc[-1] < ma20[sym].iloc[-1] and
|
|
115
|
+
ma5[sym].iloc[-2] >= ma20[sym].iloc[-2]):
|
|
116
|
+
|
|
117
|
+
if sym in self.broker.positions:
|
|
118
|
+
pos = self.broker.positions[sym]
|
|
119
|
+
self.broker.sell(sym, price, pos['size'])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == '__main__':
|
|
123
|
+
data = Data(
|
|
124
|
+
symbols=['000001.SZ', '600000.SH', '000002.SZ'],
|
|
125
|
+
indicators={
|
|
126
|
+
'sma_5': ('sma', 5),
|
|
127
|
+
'sma_20': ('sma', 20),
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
bt = Backtest(data, MultiMaCross())
|
|
131
|
+
bt.run(benchmark='000300.SH')
|
|
132
|
+
bt.summary()
|
|
133
|
+
bt.report(title='多股票均线交叉')
|
|
134
|
+
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# 动量排序选股
|
|
2
|
+
|
|
3
|
+
每月末按过去 N 日收益率排序,选前几名买入。最经典的量化策略之一。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
"""每月末按动量排序,选前 20% 的股票买入"""
|
|
9
|
+
from qka import Data, Strategy, Broker, Backtest
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Momentum(Strategy):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.broker = Broker(initial_cash=1_000_000)
|
|
17
|
+
self.monthly_rebalance = False
|
|
18
|
+
self.holdings = []
|
|
19
|
+
|
|
20
|
+
def on_bar(self, date):
|
|
21
|
+
# 月末调仓
|
|
22
|
+
if date.day < 28:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# 每月只调一次(本周最后一个交易日)
|
|
26
|
+
if self.monthly_rebalance:
|
|
27
|
+
return
|
|
28
|
+
self.monthly_rebalance = True
|
|
29
|
+
|
|
30
|
+
# 下个月的第一天重置标记
|
|
31
|
+
if date.day >= 28:
|
|
32
|
+
pass # 保持标记,等跨月再重置
|
|
33
|
+
|
|
34
|
+
close = self.get('close')
|
|
35
|
+
if close is None or close.empty or len(close) < 5:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# 过去 60 个交易日的动量
|
|
39
|
+
hist = self.history('close', 60)
|
|
40
|
+
if hist is None or hist.empty:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# 算每只股票的收益率
|
|
44
|
+
ret = {}
|
|
45
|
+
for sym in hist.columns:
|
|
46
|
+
prices = hist[sym].dropna()
|
|
47
|
+
if len(prices) >= 40: # 至少 40 个有效数据
|
|
48
|
+
ret[sym] = prices.iloc[-1] / prices.iloc[0] - 1
|
|
49
|
+
|
|
50
|
+
if not ret:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# 排序,选前 20%
|
|
54
|
+
sorted_syms = sorted(ret, key=ret.get, reverse=True)
|
|
55
|
+
top_n = max(1, len(sorted_syms) // 5)
|
|
56
|
+
buy_list = sorted_syms[:top_n]
|
|
57
|
+
|
|
58
|
+
# 卖出不在列表里的
|
|
59
|
+
for sym in list(self.broker.positions.keys()):
|
|
60
|
+
if sym not in buy_list:
|
|
61
|
+
pos = self.broker.positions[sym]
|
|
62
|
+
price = float(close.get(sym, 0))
|
|
63
|
+
if price > 0:
|
|
64
|
+
self.broker.sell(sym, price, pos['size'])
|
|
65
|
+
|
|
66
|
+
# 买入列表里的新标的
|
|
67
|
+
cash_per_sym = self.broker.cash / len(buy_list)
|
|
68
|
+
for sym in buy_list:
|
|
69
|
+
if sym in self.broker.positions:
|
|
70
|
+
continue
|
|
71
|
+
price = float(close.get(sym, 0))
|
|
72
|
+
if price > 0:
|
|
73
|
+
size = self.sizing.fixed_amount(cash_per_sym, price)
|
|
74
|
+
if size > 0:
|
|
75
|
+
self.broker.buy(sym, price, size)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == '__main__':
|
|
79
|
+
data = Data(
|
|
80
|
+
symbols=['000001.SZ', '600000.SH', '000002.SZ',
|
|
81
|
+
'600036.SH', '601166.SH'],
|
|
82
|
+
)
|
|
83
|
+
bt = Backtest(data, Momentum())
|
|
84
|
+
bt.run(benchmark='000300.SH')
|
|
85
|
+
bt.summary()
|
|
86
|
+
bt.report(title='动量排序选股')
|
|
87
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# 多因子打分
|
|
2
|
+
|
|
3
|
+
用多个因子综合打分,选分最高的买。多因子策略的骨架。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
"""PE + 动量 + 波动率三因子打分选股"""
|
|
9
|
+
from qka import Data, Strategy, Broker, Backtest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MultiFactor(Strategy):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.broker = Broker(initial_cash=1_000_000)
|
|
16
|
+
|
|
17
|
+
def on_bar(self, date):
|
|
18
|
+
close = self.get('close')
|
|
19
|
+
if close is None or close.empty:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# 每月末调仓
|
|
23
|
+
if date.day < 28:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
rsi = self.get('rsi_14')
|
|
27
|
+
hist = self.history('rsi_14', 20)
|
|
28
|
+
if hist is None or hist.empty:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
scores = {}
|
|
32
|
+
for sym in close.index:
|
|
33
|
+
price = float(close[sym])
|
|
34
|
+
if price <= 0:
|
|
35
|
+
continue
|
|
36
|
+
if sym not in rsi.index or sym not in hist.columns:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
rsi_val = float(rsi[sym])
|
|
40
|
+
hist_series = hist[sym].dropna()
|
|
41
|
+
|
|
42
|
+
# 因子 1:RSI 适中(30-70 之间最好,太高中性,太低扣分)
|
|
43
|
+
score1 = 1.0 if 30 < rsi_val < 70 else 0.0
|
|
44
|
+
if rsi_val < 30:
|
|
45
|
+
score1 = -0.5
|
|
46
|
+
|
|
47
|
+
# 因子 2:RSI 趋势(最近 20 天 RSI 上升趋势)
|
|
48
|
+
if len(hist_series) >= 10:
|
|
49
|
+
recent = hist_series.iloc[-5:].mean()
|
|
50
|
+
earlier = hist_series.iloc[-10:-5].mean()
|
|
51
|
+
score2 = (recent - earlier) / earlier
|
|
52
|
+
score2 = max(-1, min(1, score2)) # 限制到 [-1, 1]
|
|
53
|
+
else:
|
|
54
|
+
score2 = 0
|
|
55
|
+
|
|
56
|
+
# 因子 3:波动率(低波动加分)
|
|
57
|
+
if len(hist_series) >= 10:
|
|
58
|
+
vol = hist_series.std()
|
|
59
|
+
score3 = max(-1, min(1, -vol / 20)) # 波动越低分越高
|
|
60
|
+
else:
|
|
61
|
+
score3 = 0
|
|
62
|
+
|
|
63
|
+
# 综合打分
|
|
64
|
+
scores[sym] = 0.4 * score1 + 0.4 * score2 + 0.2 * score3
|
|
65
|
+
|
|
66
|
+
if not scores:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# 选前 3 名
|
|
70
|
+
sorted_syms = sorted(scores, key=scores.get, reverse=True)
|
|
71
|
+
buy_list = sorted_syms[:3]
|
|
72
|
+
|
|
73
|
+
# 卖出不在名单里的
|
|
74
|
+
for sym in list(self.broker.positions.keys()):
|
|
75
|
+
if sym not in buy_list:
|
|
76
|
+
pos = self.broker.positions[sym]
|
|
77
|
+
price = float(close[sym])
|
|
78
|
+
if price > 0:
|
|
79
|
+
self.broker.sell(sym, price, pos['size'])
|
|
80
|
+
|
|
81
|
+
# 买入名单里的新标的
|
|
82
|
+
cash_per_sym = self.broker.cash / len(buy_list)
|
|
83
|
+
for sym in buy_list:
|
|
84
|
+
if sym in self.broker.positions:
|
|
85
|
+
continue
|
|
86
|
+
price = float(close[sym])
|
|
87
|
+
if price > 0:
|
|
88
|
+
size = self.sizing.fixed_amount(cash_per_sym, price)
|
|
89
|
+
if size > 0:
|
|
90
|
+
self.broker.buy(sym, price, size)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == '__main__':
|
|
94
|
+
data = Data(
|
|
95
|
+
symbols=['000001.SZ', '600000.SH', '000002.SZ',
|
|
96
|
+
'600036.SH', '601166.SH', '600519.SH',
|
|
97
|
+
'000858.SZ', '002415.SZ', '300750.SZ'],
|
|
98
|
+
indicators={
|
|
99
|
+
'rsi_14': ('rsi', 14),
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
bt = Backtest(data, MultiFactor())
|
|
103
|
+
bt.run(benchmark='000300.SH')
|
|
104
|
+
bt.summary()
|
|
105
|
+
bt.report(title='多因子打分选股')
|
|
106
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# RSI + ATR 风控
|
|
2
|
+
|
|
3
|
+
RSI 判断超买超卖,ATR 控制仓位大小。比固定股数更贴近实战。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
"""RSI 超卖买入 + ATR 仓位控制"""
|
|
9
|
+
from qka import Data, Strategy, Broker, Backtest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RsiAtrStrategy(Strategy):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.broker = Broker(initial_cash=1_000_000)
|
|
16
|
+
|
|
17
|
+
def on_bar(self, date):
|
|
18
|
+
close = self.get('close')
|
|
19
|
+
if close is None or close.empty:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
rsi = self.get('rsi_14')
|
|
23
|
+
atr = self.get('atr_14')
|
|
24
|
+
|
|
25
|
+
for sym in close.index:
|
|
26
|
+
price = float(close[sym])
|
|
27
|
+
if price <= 0:
|
|
28
|
+
continue
|
|
29
|
+
if sym not in rsi.index or sym not in atr.index:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
rsi_val = float(rsi[sym])
|
|
33
|
+
atr_val = float(atr[sym])
|
|
34
|
+
|
|
35
|
+
# 超卖(RSI < 30)且没有持仓 → 买入
|
|
36
|
+
if rsi_val < 30 and sym not in self.broker.positions:
|
|
37
|
+
# ATR 控制仓位:每笔风险不超总资金 2%
|
|
38
|
+
size = self.sizing.atr_risk(0.02, price, atr_val)
|
|
39
|
+
if size > 0:
|
|
40
|
+
self.broker.buy(sym, price, size)
|
|
41
|
+
|
|
42
|
+
# 超买(RSI > 70)且有持仓 → 卖出
|
|
43
|
+
elif rsi_val > 70 and sym in self.broker.positions:
|
|
44
|
+
pos = self.broker.positions[sym]
|
|
45
|
+
self.broker.sell(sym, price, pos['size'])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == '__main__':
|
|
49
|
+
data = Data(
|
|
50
|
+
symbols=['000001.SZ', '600000.SH', '000002.SZ'],
|
|
51
|
+
indicators={
|
|
52
|
+
'rsi_14': ('rsi', 14),
|
|
53
|
+
'atr_14': ('atr', 14),
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
bt = Backtest(data, RsiAtrStrategy())
|
|
57
|
+
bt.run(benchmark='000300.SH')
|
|
58
|
+
bt.summary()
|
|
59
|
+
bt.report(title='RSI + ATR 风控')
|
|
60
|
+
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|