BackcastPro 0.1.0__tar.gz → 0.1.1__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.
Potentially problematic release.
This version of BackcastPro might be problematic. Click here for more details.
- {backcastpro-0.1.0 → backcastpro-0.1.1}/PKG-INFO +2 -3
- {backcastpro-0.1.0 → backcastpro-0.1.1}/README.md +0 -1
- {backcastpro-0.1.0 → backcastpro-0.1.1}/pyproject.toml +2 -2
- backcastpro-0.1.1/src/BackcastPro/__init__.py +14 -0
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro/_broker.py +160 -156
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro/_stats.py +22 -14
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro/backtest.py +94 -70
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro/order.py +10 -0
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro/position.py +1 -2
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro/strategy.py +12 -6
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro/trade.py +60 -28
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro.egg-info/PKG-INFO +2 -3
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro.egg-info/SOURCES.txt +1 -3
- backcastpro-0.1.0/src/BackcastPro/__init__.py +0 -15
- backcastpro-0.1.0/src/BackcastPro/data/JapanStock.py +0 -171
- backcastpro-0.1.0/src/BackcastPro/data/__init__.py +0 -7
- {backcastpro-0.1.0 → backcastpro-0.1.1}/setup.cfg +0 -0
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro.egg-info/dependency_links.txt +0 -0
- {backcastpro-0.1.0 → backcastpro-0.1.1}/src/BackcastPro.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BackcastPro
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: トレーディング戦略のためのPythonバックテストライブラリ
|
|
5
|
-
Author
|
|
5
|
+
Author: botterYosuke
|
|
6
6
|
Project-URL: Homepage, https://github.com/botterYosuke/BackcastPro/
|
|
7
7
|
Project-URL: Issues, https://github.com/botterYosuke/BackcastPro/issues
|
|
8
8
|
Project-URL: Logo, https://raw.githubusercontent.com/botterYosuke/BackcastPro/main/docs/img/logo.drawio.svg
|
|
@@ -45,7 +45,6 @@ py -m pip install -r requirements.txt
|
|
|
45
45
|
|
|
46
46
|
```python
|
|
47
47
|
from BackcastPro import Strategy, Backtest
|
|
48
|
-
from BackcastPro.data import DataReader, JapanStocks
|
|
49
48
|
|
|
50
49
|
# ここにトレーディング戦略の実装を記述
|
|
51
50
|
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BackcastPro をご利用いただきありがとうございます。
|
|
3
|
+
|
|
4
|
+
インストール後のご案内(インストール済みユーザー向け)
|
|
5
|
+
|
|
6
|
+
- ドキュメント総合トップ: [index.md](https://github.com/botterYosuke/BackcastPro/blob/main/docs/index.md)
|
|
7
|
+
- クイックスタート/チュートリアル: [tutorial.md](https://github.com/botterYosuke/BackcastPro/blob/main/docs/tutorial.md)
|
|
8
|
+
- APIリファレンス: [BackcastPro - APIリファレンス](https://botteryosuke.github.io/BackcastPro/namespacesrc_1_1BackcastPro.html)
|
|
9
|
+
- トラブルシューティング: [troubleshooting.md](https://github.com/botterYosuke/BackcastPro/blob/main/docs/troubleshooting.md)
|
|
10
|
+
|
|
11
|
+
※ 使い始めはチュートリアル → 詳細はAPIリファレンスをご参照ください。
|
|
12
|
+
"""
|
|
13
|
+
from .backtest import Backtest
|
|
14
|
+
from .strategy import Strategy
|
|
@@ -43,10 +43,9 @@ class _Broker:
|
|
|
43
43
|
ヘッジングモードの有効化。Trueの場合、反対方向のポジションを同時に保有できます。
|
|
44
44
|
exclusive_orders : bool
|
|
45
45
|
排他的注文モード。Trueの場合、新しい注文が前のポジションを自動的にクローズします。
|
|
46
|
-
index : pd.Index
|
|
47
|
-
時系列データのインデックス。エクイティカーブの記録に使用されます。
|
|
48
46
|
"""
|
|
49
|
-
|
|
47
|
+
|
|
48
|
+
# ヒント:
|
|
50
49
|
# 関数定義における`*`の意味
|
|
51
50
|
# - `*`以降の引数は、必ずキーワード引数として渡す必要がある
|
|
52
51
|
# - 位置引数として渡すことはできない
|
|
@@ -55,10 +54,10 @@ class _Broker:
|
|
|
55
54
|
# 2. 保守性: 引数の順序を変更しても既存のコードが壊れない
|
|
56
55
|
# 3. 可読性: 関数呼び出し時に何を渡しているかが分かりやすい
|
|
57
56
|
def __init__(self, *, data, cash, spread, commission, margin,
|
|
58
|
-
trade_on_close, hedging, exclusive_orders
|
|
57
|
+
trade_on_close, hedging, exclusive_orders):
|
|
59
58
|
assert cash > 0, f"cash should be > 0, is {cash}"
|
|
60
59
|
assert 0 < margin <= 1, f"margin should be between 0 and 1, is {margin}"
|
|
61
|
-
self._data: pd.DataFrame = data
|
|
60
|
+
self._data: dict[str, pd.DataFrame] = data
|
|
62
61
|
self._cash = cash
|
|
63
62
|
|
|
64
63
|
# 手数料の登録
|
|
@@ -83,7 +82,8 @@ class _Broker:
|
|
|
83
82
|
self._hedging = hedging
|
|
84
83
|
self._exclusive_orders = exclusive_orders
|
|
85
84
|
|
|
86
|
-
self._equity =
|
|
85
|
+
self._equity = []
|
|
86
|
+
self._current_time = None
|
|
87
87
|
self.orders: List[Order] = []
|
|
88
88
|
self.trades: List[Trade] = []
|
|
89
89
|
self.position = Position(self)
|
|
@@ -92,10 +92,8 @@ class _Broker:
|
|
|
92
92
|
def _commission_func(self, order_size, price):
|
|
93
93
|
return self._commission_fixed + abs(order_size) * price * self._commission_relative
|
|
94
94
|
|
|
95
|
-
def __repr__(self):
|
|
96
|
-
return f'<Broker: {self._cash:.0f}{self.position.pl:+.1f} ({len(self.trades)} trades)>'
|
|
97
|
-
|
|
98
95
|
def new_order(self,
|
|
96
|
+
code: str,
|
|
99
97
|
size: float,
|
|
100
98
|
limit: Optional[float] = None,
|
|
101
99
|
stop: Optional[float] = None,
|
|
@@ -115,7 +113,7 @@ class _Broker:
|
|
|
115
113
|
|
|
116
114
|
is_long = size > 0
|
|
117
115
|
assert size != 0, size
|
|
118
|
-
adjusted_price = self._adjusted_price(size)
|
|
116
|
+
adjusted_price = self._adjusted_price(code, size)
|
|
119
117
|
|
|
120
118
|
if is_long:
|
|
121
119
|
if not (sl or -np.inf) < (limit or stop or adjusted_price) < (tp or np.inf):
|
|
@@ -128,7 +126,7 @@ class _Broker:
|
|
|
128
126
|
"Short orders require: "
|
|
129
127
|
f"TP ({tp}) < LIMIT ({limit or stop or adjusted_price}) < SL ({sl})")
|
|
130
128
|
|
|
131
|
-
order = Order(self, size, limit, stop, sl, tp, trade, tag)
|
|
129
|
+
order = Order(self, code, size, limit, stop, sl, tp, trade, tag)
|
|
132
130
|
|
|
133
131
|
if not trade:
|
|
134
132
|
# 排他的注文(各新しい注文が前の注文/ポジションを自動クローズ)の場合、
|
|
@@ -145,17 +143,16 @@ class _Broker:
|
|
|
145
143
|
|
|
146
144
|
return order
|
|
147
145
|
|
|
148
|
-
|
|
149
|
-
def last_price(self) -> float:
|
|
146
|
+
def last_price(self, code: str) -> float:
|
|
150
147
|
""" Price at the last (current) close. """
|
|
151
|
-
return self._data.Close.iloc[-1]
|
|
148
|
+
return self._data[code].Close.iloc[-1]
|
|
152
149
|
|
|
153
|
-
def _adjusted_price(self, size=None, price=None) -> float:
|
|
150
|
+
def _adjusted_price(self, code: str, size=None, price=None) -> float:
|
|
154
151
|
"""
|
|
155
152
|
Long/short `price`, adjusted for spread.
|
|
156
153
|
In long positions, the adjusted price is a fraction higher, and vice versa.
|
|
157
154
|
"""
|
|
158
|
-
return (price or self.last_price) * (1 + copysign(self._spread, size))
|
|
155
|
+
return (price or self.last_price(code)) * (1 + copysign(self._spread, size))
|
|
159
156
|
|
|
160
157
|
@property
|
|
161
158
|
def equity(self) -> float:
|
|
@@ -175,35 +172,45 @@ class _Broker:
|
|
|
175
172
|
def commission(self):
|
|
176
173
|
return self._commission
|
|
177
174
|
|
|
178
|
-
def next(self):
|
|
179
|
-
|
|
175
|
+
def next(self, current_time: pd.Timestamp):
|
|
176
|
+
self._current_time = current_time
|
|
180
177
|
self._process_orders()
|
|
181
178
|
|
|
182
179
|
# エクイティカーブ用にアカウントエクイティを記録
|
|
183
180
|
equity = self.equity
|
|
184
|
-
self._equity
|
|
181
|
+
self._equity.append(equity)
|
|
185
182
|
|
|
186
183
|
# エクイティが負の場合、すべてを0に設定してシミュレーションを停止
|
|
187
184
|
if equity <= 0:
|
|
188
185
|
assert self.margin_available <= 0
|
|
189
186
|
for trade in self.trades:
|
|
190
|
-
|
|
187
|
+
price = self._data[trade.code].Close.iloc[-1]
|
|
188
|
+
self._close_trade(trade, price, self._current_time)
|
|
191
189
|
self._cash = 0
|
|
192
|
-
self._equity[i:] = 0
|
|
193
190
|
raise Exception
|
|
194
191
|
|
|
195
192
|
def _process_orders(self):
|
|
196
193
|
data = self._data
|
|
197
|
-
open, high, low = data.Open.iloc[-1], data.High.iloc[-1], data.Low.iloc[-1]
|
|
198
194
|
reprocess_orders = False
|
|
199
195
|
|
|
200
196
|
# 注文を処理
|
|
201
|
-
for order in list(self.orders): #
|
|
197
|
+
for order in list(self.orders): # 型: Order
|
|
202
198
|
|
|
203
199
|
# 関連するSL/TP注文は既に削除されている
|
|
204
200
|
if order not in self.orders:
|
|
205
201
|
continue
|
|
206
202
|
|
|
203
|
+
# 注文の銘柄データを取得
|
|
204
|
+
if order.code not in data:
|
|
205
|
+
continue
|
|
206
|
+
df = data[order.code]
|
|
207
|
+
|
|
208
|
+
# データの存在確認
|
|
209
|
+
if len(df) == 0 or df.index[-1] != self._current_time:
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
open, high, low = df.Open.iloc[-1], df.High.iloc[-1], df.Low.iloc[-1]
|
|
213
|
+
|
|
207
214
|
# ストップ条件が満たされたかチェック
|
|
208
215
|
stop_price = order.stop
|
|
209
216
|
if stop_price:
|
|
@@ -223,146 +230,143 @@ class _Broker:
|
|
|
223
230
|
# リミットがストップより先に満たされたと仮定する(つまり「カウントされる前に」)
|
|
224
231
|
is_limit_hit_before_stop = (is_limit_hit and
|
|
225
232
|
(order.limit <= (stop_price or -np.inf)
|
|
226
|
-
|
|
227
|
-
|
|
233
|
+
if order.is_long
|
|
234
|
+
else order.limit >= (stop_price or np.inf)))
|
|
228
235
|
if not is_limit_hit or is_limit_hit_before_stop:
|
|
229
236
|
continue
|
|
230
237
|
|
|
231
238
|
# stop_priceが設定されている場合、このバー内で満たされた
|
|
232
239
|
price = (min(stop_price or open, order.limit)
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
if order.is_long else
|
|
241
|
+
max(stop_price or open, order.limit))
|
|
235
242
|
else:
|
|
236
243
|
# 成行注文(Market-if-touched / market order)
|
|
237
244
|
# 条件付き注文は常に次の始値で
|
|
238
|
-
prev_close =
|
|
245
|
+
prev_close = df.Close.iloc[-2]
|
|
239
246
|
price = prev_close if self._trade_on_close and not order.is_contingent else open
|
|
240
247
|
if stop_price:
|
|
241
248
|
price = max(price, stop_price) if order.is_long else min(price, stop_price)
|
|
242
249
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
# trade.close()注文で、完了
|
|
270
|
-
assert abs(_prev_size) >= abs(size) >= 1
|
|
271
|
-
self.orders.remove(order)
|
|
272
|
-
continue
|
|
250
|
+
# エントリー/エグジットバーのインデックスを決定
|
|
251
|
+
is_market_order = not order.limit and not stop_price
|
|
252
|
+
|
|
253
|
+
# 注文がSL/TP注文の場合、それが依存していた既存の取引をクローズする必要がある
|
|
254
|
+
if order.parent_trade:
|
|
255
|
+
trade = order.parent_trade
|
|
256
|
+
_prev_size = trade.size
|
|
257
|
+
# order.sizeがtrade.sizeより「大きい」場合、この注文はtrade.close()注文で
|
|
258
|
+
# 取引の一部は事前にクローズされている
|
|
259
|
+
size = copysign(min(abs(_prev_size), abs(order.size)), order.size)
|
|
260
|
+
# この取引がまだクローズされていない場合(例:複数の`trade.close(.5)`呼び出し)
|
|
261
|
+
if trade in self.trades:
|
|
262
|
+
self._reduce_trade(trade, price, size, self._current_time)
|
|
263
|
+
assert order.size != -_prev_size or trade not in self.trades
|
|
264
|
+
if price == stop_price:
|
|
265
|
+
# 統計用にSLを注文に戻す
|
|
266
|
+
trade._sl_order._replace(stop_price=stop_price)
|
|
267
|
+
if order in (trade._sl_order,
|
|
268
|
+
trade._tp_order):
|
|
269
|
+
assert order.size == -trade.size
|
|
270
|
+
assert order not in self.orders # 取引がクローズされたときに削除される
|
|
271
|
+
else:
|
|
272
|
+
# trade.close()注文で、完了
|
|
273
|
+
assert abs(_prev_size) >= abs(size) >= 1
|
|
274
|
+
self.orders.remove(order)
|
|
275
|
+
continue
|
|
273
276
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
277
|
+
# そうでなければ、これは独立した取引
|
|
278
|
+
|
|
279
|
+
# 手数料(またはビッドアスクスプレッド)を含むように価格を調整
|
|
280
|
+
# ロングポジションでは調整価格が少し高くなり、その逆も同様
|
|
281
|
+
adjusted_price = self._adjusted_price(code=order.code, size=order.size, price=price)
|
|
282
|
+
adjusted_price_plus_commission = \
|
|
283
|
+
adjusted_price + self._commission(order.size, price) / abs(order.size)
|
|
284
|
+
|
|
285
|
+
# 注文サイズが比例的に指定された場合、
|
|
286
|
+
# マージンとスプレッド/手数料を考慮して、単位での真のサイズを事前計算
|
|
287
|
+
size = order.size
|
|
288
|
+
if -1 < size < 1:
|
|
289
|
+
size = copysign(int((self.margin_available * self._leverage * abs(size))
|
|
290
|
+
// adjusted_price_plus_commission), size)
|
|
291
|
+
# 単一ユニットでも十分な現金/マージンがない
|
|
292
|
+
if not size:
|
|
293
|
+
warnings.warn(
|
|
294
|
+
f'{self._current_time}: ブローカーは相対サイズの注文を'
|
|
295
|
+
f'不十分なマージンのためキャンセルしました。', category=UserWarning)
|
|
296
|
+
# XXX: 注文はブローカーによってキャンセルされる?
|
|
297
|
+
self.orders.remove(order)
|
|
298
|
+
continue
|
|
299
|
+
assert size == round(size)
|
|
300
|
+
need_size = int(size)
|
|
301
|
+
|
|
302
|
+
if not self._hedging:
|
|
303
|
+
# 既存の反対方向の取引をFIFOでクローズ/削減してポジションを埋める
|
|
304
|
+
# 既存の取引は調整価格でクローズされる(調整は購入時に既に行われているため)
|
|
305
|
+
for trade in list(self.trades):
|
|
306
|
+
if trade.is_long == order.is_long:
|
|
307
|
+
continue
|
|
308
|
+
assert trade.size * order.size < 0
|
|
309
|
+
|
|
310
|
+
# 注文サイズがこの反対方向の既存取引より大きい場合、
|
|
311
|
+
# 完全にクローズされる
|
|
312
|
+
if abs(need_size) >= abs(trade.size):
|
|
313
|
+
self._close_trade(trade, price, self._current_time)
|
|
314
|
+
need_size += trade.size
|
|
315
|
+
else:
|
|
316
|
+
# 既存の取引が新しい注文より大きい場合、
|
|
317
|
+
# 部分的にのみクローズされる
|
|
318
|
+
self._reduce_trade(trade, price, need_size, self._current_time)
|
|
319
|
+
need_size = 0
|
|
320
|
+
|
|
321
|
+
if not need_size:
|
|
322
|
+
break
|
|
323
|
+
|
|
324
|
+
# 注文をカバーするのに十分な流動性がない場合、ブローカーはそれをキャンセルする
|
|
325
|
+
if abs(need_size) * adjusted_price_plus_commission > \
|
|
326
|
+
self.margin_available * self._leverage:
|
|
294
327
|
self.orders.remove(order)
|
|
295
328
|
continue
|
|
296
|
-
assert size == round(size)
|
|
297
|
-
need_size = int(size)
|
|
298
|
-
|
|
299
|
-
if not self._hedging:
|
|
300
|
-
# 既存の反対方向の取引をFIFOでクローズ/削減してポジションを埋める
|
|
301
|
-
# 既存の取引は調整価格でクローズされる(調整は購入時に既に行われているため)
|
|
302
|
-
for trade in list(self.trades):
|
|
303
|
-
if trade.is_long == order.is_long:
|
|
304
|
-
continue
|
|
305
|
-
assert trade.size * order.size < 0
|
|
306
|
-
|
|
307
|
-
# 注文サイズがこの反対方向の既存取引より大きい場合、
|
|
308
|
-
# 完全にクローズされる
|
|
309
|
-
if abs(need_size) >= abs(trade.size):
|
|
310
|
-
self._close_trade(trade, price, time_index)
|
|
311
|
-
need_size += trade.size
|
|
312
|
-
else:
|
|
313
|
-
# 既存の取引が新しい注文より大きい場合、
|
|
314
|
-
# 部分的にのみクローズされる
|
|
315
|
-
self._reduce_trade(trade, price, need_size, time_index)
|
|
316
|
-
need_size = 0
|
|
317
329
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
330
|
+
# 新しい取引を開始
|
|
331
|
+
if need_size:
|
|
332
|
+
self._open_trade(order.code,
|
|
333
|
+
adjusted_price,
|
|
334
|
+
need_size,
|
|
335
|
+
order.sl,
|
|
336
|
+
order.tp,
|
|
337
|
+
self._current_time,
|
|
338
|
+
order.tag)
|
|
339
|
+
|
|
340
|
+
# 新しくキューに追加されたSL/TP注文を再処理する必要がある
|
|
341
|
+
# これにより、注文が開かれた同じバーでSLがヒットすることを可能にする
|
|
342
|
+
# https://github.com/kernc/backtesting.py/issues/119 を参照
|
|
343
|
+
if order.sl or order.tp:
|
|
344
|
+
if is_market_order:
|
|
345
|
+
reprocess_orders = True
|
|
346
|
+
# Order.stopとTPが同じバー内でヒットしたが、SLはヒットしなかった。この場合
|
|
347
|
+
# ストップとTPが同じ価格方向に進むため、曖昧ではない
|
|
348
|
+
elif stop_price and not order.limit and order.tp and (
|
|
349
|
+
(order.is_long and order.tp <= high and (order.sl or -np.inf) < low) or
|
|
350
|
+
(order.is_short and order.tp >= low and (order.sl or np.inf) > high)):
|
|
351
|
+
reprocess_orders = True
|
|
352
|
+
elif (low <= (order.sl or -np.inf) <= high or
|
|
353
|
+
low <= (order.tp or -np.inf) <= high):
|
|
354
|
+
warnings.warn(
|
|
355
|
+
f"({df.index[-1]}) 条件付きSL/TP注文が、その親ストップ/リミット注文が取引に"
|
|
356
|
+
"変換された同じバーで実行されることになります。"
|
|
357
|
+
"正確なローソク足内価格変動を断言できないため、"
|
|
358
|
+
"影響を受けるSL/TP注文は代わりに次の(マッチングする)価格/バーで"
|
|
359
|
+
"実行され、結果(この取引の)が幾分疑わしいものになります。"
|
|
360
|
+
"https://github.com/kernc/backtesting.py/issues/119 を参照",
|
|
361
|
+
UserWarning)
|
|
362
|
+
|
|
363
|
+
# 注文処理完了
|
|
324
364
|
self.orders.remove(order)
|
|
325
|
-
continue
|
|
326
|
-
|
|
327
|
-
# 新しい取引を開始
|
|
328
|
-
if need_size:
|
|
329
|
-
self._open_trade(adjusted_price,
|
|
330
|
-
need_size,
|
|
331
|
-
order.sl,
|
|
332
|
-
order.tp,
|
|
333
|
-
time_index,
|
|
334
|
-
order.tag)
|
|
335
|
-
|
|
336
|
-
# 新しくキューに追加されたSL/TP注文を再処理する必要がある
|
|
337
|
-
# これにより、注文が開かれた同じバーでSLがヒットすることを可能にする
|
|
338
|
-
# https://github.com/kernc/backtesting.py/issues/119 を参照
|
|
339
|
-
if order.sl or order.tp:
|
|
340
|
-
if is_market_order:
|
|
341
|
-
reprocess_orders = True
|
|
342
|
-
# Order.stopとTPが同じバー内でヒットしたが、SLはヒットしなかった。この場合
|
|
343
|
-
# ストップとTPが同じ価格方向に進むため、曖昧ではない
|
|
344
|
-
elif stop_price and not order.limit and order.tp and (
|
|
345
|
-
(order.is_long and order.tp <= high and (order.sl or -np.inf) < low) or
|
|
346
|
-
(order.is_short and order.tp >= low and (order.sl or np.inf) > high)):
|
|
347
|
-
reprocess_orders = True
|
|
348
|
-
elif (low <= (order.sl or -np.inf) <= high or
|
|
349
|
-
low <= (order.tp or -np.inf) <= high):
|
|
350
|
-
warnings.warn(
|
|
351
|
-
f"({data.index[-1]}) 条件付きSL/TP注文が、その親ストップ/リミット注文が取引に"
|
|
352
|
-
"変換された同じバーで実行されることになります。"
|
|
353
|
-
"正確なローソク足内価格変動を断言できないため、"
|
|
354
|
-
"影響を受けるSL/TP注文は代わりに次の(マッチングする)価格/バーで"
|
|
355
|
-
"実行され、結果(この取引の)が幾分疑わしいものになります。"
|
|
356
|
-
"https://github.com/kernc/backtesting.py/issues/119 を参照",
|
|
357
|
-
UserWarning)
|
|
358
|
-
|
|
359
|
-
# 注文処理完了
|
|
360
|
-
self.orders.remove(order)
|
|
361
365
|
|
|
362
366
|
if reprocess_orders:
|
|
363
367
|
self._process_orders()
|
|
364
368
|
|
|
365
|
-
def _reduce_trade(self, trade: Trade, price: float, size: float,
|
|
369
|
+
def _reduce_trade(self, trade: Trade, price: float, size: float, current_time: pd.Timestamp):
|
|
366
370
|
assert trade.size * size < 0
|
|
367
371
|
assert abs(trade.size) >= abs(size)
|
|
368
372
|
|
|
@@ -371,44 +375,44 @@ class _Broker:
|
|
|
371
375
|
if not size_left:
|
|
372
376
|
close_trade = trade
|
|
373
377
|
else:
|
|
374
|
-
#
|
|
378
|
+
# 既存の取引を削減...
|
|
375
379
|
trade._replace(size=size_left)
|
|
376
380
|
if trade._sl_order:
|
|
377
381
|
trade._sl_order._replace(size=-trade.size)
|
|
378
382
|
if trade._tp_order:
|
|
379
383
|
trade._tp_order._replace(size=-trade.size)
|
|
380
384
|
|
|
381
|
-
# ...
|
|
385
|
+
# ... その削減コピーをクローズすることによって
|
|
382
386
|
close_trade = trade._copy(size=-size, sl_order=None, tp_order=None)
|
|
383
387
|
self.trades.append(close_trade)
|
|
384
388
|
|
|
385
|
-
self._close_trade(close_trade, price,
|
|
389
|
+
self._close_trade(close_trade, price, current_time)
|
|
386
390
|
|
|
387
|
-
def _close_trade(self, trade: Trade, price: float,
|
|
391
|
+
def _close_trade(self, trade: Trade, price: float, current_time: pd.Timestamp):
|
|
388
392
|
self.trades.remove(trade)
|
|
389
393
|
if trade._sl_order:
|
|
390
394
|
self.orders.remove(trade._sl_order)
|
|
391
395
|
if trade._tp_order:
|
|
392
396
|
self.orders.remove(trade._tp_order)
|
|
393
397
|
|
|
394
|
-
closed_trade = trade._replace(exit_price=price,
|
|
398
|
+
closed_trade = trade._replace(exit_price=price, exit_time=current_time)
|
|
395
399
|
self.closed_trades.append(closed_trade)
|
|
396
|
-
#
|
|
400
|
+
# 取引終了時に手数料を再度適用
|
|
397
401
|
commission = self._commission(trade.size, price)
|
|
398
402
|
self._cash += trade.pl - commission
|
|
399
|
-
#
|
|
403
|
+
# 統計用にTradeインスタンスに手数料を保存
|
|
400
404
|
trade_open_commission = self._commission(closed_trade.size, closed_trade.entry_price)
|
|
401
|
-
#
|
|
402
|
-
#
|
|
405
|
+
# サイズが_reduce_trade()によって変更された可能性があるため、
|
|
406
|
+
# Trade開始時ではなくここで適用
|
|
403
407
|
closed_trade._commissions = commission + trade_open_commission
|
|
404
408
|
|
|
405
|
-
def _open_trade(self, price: float, size: int,
|
|
406
|
-
sl: Optional[float], tp: Optional[float],
|
|
407
|
-
trade = Trade(self, size, price,
|
|
409
|
+
def _open_trade(self, code: str, price: float, size: int,
|
|
410
|
+
sl: Optional[float], tp: Optional[float], current_time: pd.Timestamp, tag):
|
|
411
|
+
trade = Trade(self, code, size, price, current_time, tag)
|
|
408
412
|
self.trades.append(trade)
|
|
409
|
-
#
|
|
413
|
+
# 取引開始時にブローカー手数料を適用
|
|
410
414
|
self._cash -= self._commission(size, price)
|
|
411
|
-
#
|
|
415
|
+
# SL/TP(ブラケット)注文を作成。
|
|
412
416
|
if tp:
|
|
413
417
|
trade.tp = tp
|
|
414
418
|
if sl:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
from numbers import Number
|
|
4
5
|
from typing import TYPE_CHECKING, List, Union, cast
|
|
5
6
|
|
|
@@ -16,7 +17,7 @@ def compute_drawdown_duration_peaks(dd: pd.Series):
|
|
|
16
17
|
df = iloc.to_frame('iloc').assign(prev=iloc.shift())
|
|
17
18
|
df = df[df['iloc'] > df['prev'] + 1].astype(np.int64)
|
|
18
19
|
|
|
19
|
-
# 取引がないためドローダウンがない場合、pandas
|
|
20
|
+
# 取引がないためドローダウンがない場合、pandasの都合上以下を避けてnanシリーズを返す
|
|
20
21
|
if not len(df):
|
|
21
22
|
return (dd.replace(0, np.nan),) * 2
|
|
22
23
|
|
|
@@ -25,7 +26,6 @@ def compute_drawdown_duration_peaks(dd: pd.Series):
|
|
|
25
26
|
df = df.reindex(dd.index)
|
|
26
27
|
return df['duration'], df['peak_dd']
|
|
27
28
|
|
|
28
|
-
|
|
29
29
|
def geometric_mean(returns: pd.Series) -> float:
|
|
30
30
|
returns = returns.fillna(0) + 1
|
|
31
31
|
if np.any(returns <= 0):
|
|
@@ -33,20 +33,27 @@ def geometric_mean(returns: pd.Series) -> float:
|
|
|
33
33
|
return np.exp(np.log(returns).sum() / (len(returns) or np.nan)) - 1
|
|
34
34
|
|
|
35
35
|
def _data_period(index) -> Union[pd.Timedelta, Number]:
|
|
36
|
-
"""
|
|
36
|
+
"""データインデックスの期間をpd.Timedeltaとして返す"""
|
|
37
37
|
values = pd.Series(index[-100:])
|
|
38
38
|
return values.diff().dropna().median()
|
|
39
39
|
|
|
40
40
|
def compute_stats(
|
|
41
41
|
trades: Union[List['Trade'], pd.DataFrame],
|
|
42
42
|
equity: np.ndarray,
|
|
43
|
-
|
|
43
|
+
index: pd.DatetimeIndex,
|
|
44
44
|
strategy_instance: Strategy | None,
|
|
45
45
|
risk_free_rate: float = 0,
|
|
46
46
|
) -> pd.Series:
|
|
47
47
|
assert -1 < risk_free_rate < 1
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
|
|
50
|
+
# エクイティカーブとインデックスの長さを一致させる
|
|
51
|
+
if len(equity) > len(index):
|
|
52
|
+
equity = equity[:len(index)]
|
|
53
|
+
elif len(equity) < len(index):
|
|
54
|
+
# エクイティカーブが短い場合は、0で埋める
|
|
55
|
+
equity = np.concatenate([equity, np.full(len(index) - len(equity), 0)])
|
|
56
|
+
|
|
50
57
|
dd = 1 - equity / np.maximum.accumulate(equity)
|
|
51
58
|
dd_dur, dd_peaks = compute_drawdown_duration_peaks(pd.Series(dd, index=index))
|
|
52
59
|
|
|
@@ -60,8 +67,9 @@ def compute_stats(
|
|
|
60
67
|
trades_df: pd.DataFrame = trades
|
|
61
68
|
commissions = None # Not shown
|
|
62
69
|
else:
|
|
63
|
-
#
|
|
70
|
+
# Backtest.run()から直接来たデータ
|
|
64
71
|
trades_df = pd.DataFrame({
|
|
72
|
+
'Code': [t.code for t in trades],
|
|
65
73
|
'Size': [t.size for t in trades],
|
|
66
74
|
'EntryBar': [t.entry_bar for t in trades],
|
|
67
75
|
'ExitBar': [t.exit_bar for t in trades],
|
|
@@ -100,7 +108,7 @@ def compute_stats(
|
|
|
100
108
|
for t in trades_df.itertuples(index=False):
|
|
101
109
|
have_position[t.EntryBar:t.ExitBar + 1] = 1
|
|
102
110
|
|
|
103
|
-
s.loc['Exposure Time [%]'] = have_position.mean() * 100 #
|
|
111
|
+
s.loc['Exposure Time [%]'] = have_position.mean() * 100 # "n bars"時間単位、インデックス時間ではない
|
|
104
112
|
s.loc['Equity Final [$]'] = equity[-1]
|
|
105
113
|
s.loc['Equity Peak [$]'] = equity.max()
|
|
106
114
|
if commissions:
|
|
@@ -123,10 +131,10 @@ def compute_stats(
|
|
|
123
131
|
day_returns = equity_df['Equity'].resample(freq).last().dropna().pct_change()
|
|
124
132
|
gmean_day_return = geometric_mean(day_returns)
|
|
125
133
|
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
134
|
+
# 年率化リターンとリスク指標は、リターンが複利計算されるという(ほぼ正確な)
|
|
135
|
+
# 仮定に基づいて計算される。参照: https://dx.doi.org/10.2139/ssrn.3054517
|
|
136
|
+
# 我々の年率化リターンは`empyrical.annual_return(day_returns)`と一致するが、
|
|
137
|
+
# リスクは一致しない。彼らは以下のより単純なアプローチを使用している。
|
|
130
138
|
annualized_return = (1 + gmean_day_return)**annual_trading_days - 1
|
|
131
139
|
s.loc['Return (Ann.) [%]'] = annualized_return * 100
|
|
132
140
|
s.loc['Volatility (Ann.) [%]'] = np.sqrt((day_returns.var(ddof=int(bool(day_returns.shape))) + (1 + gmean_day_return)**2)**annual_trading_days - (1 + gmean_day_return)**(2 * annual_trading_days)) * 100 # noqa: E501
|
|
@@ -136,10 +144,10 @@ def compute_stats(
|
|
|
136
144
|
time_in_years = (s.loc['Duration'].days + s.loc['Duration'].seconds / 86400) / annual_trading_days
|
|
137
145
|
s.loc['CAGR [%]'] = ((s.loc['Equity Final [$]'] / equity[0])**(1 / time_in_years) - 1) * 100 if time_in_years else np.nan # noqa: E501
|
|
138
146
|
|
|
139
|
-
#
|
|
140
|
-
#
|
|
147
|
+
# 我々のSharpeは`empyrical.sharpe_ratio()`と一致しない。彼らは算術平均リターン
|
|
148
|
+
# と単純な標準偏差を使用するため
|
|
141
149
|
s.loc['Sharpe Ratio'] = (s.loc['Return (Ann.) [%]'] - risk_free_rate * 100) / (s.loc['Volatility (Ann.) [%]'] or np.nan) # noqa: E501
|
|
142
|
-
#
|
|
150
|
+
# 我々のSortinoは`empyrical.sortino_ratio()`と一致しない。彼らは算術平均リターンを使用するため
|
|
143
151
|
with np.errstate(divide='ignore'):
|
|
144
152
|
s.loc['Sortino Ratio'] = (annualized_return - risk_free_rate) / (np.sqrt(np.mean(day_returns.clip(-np.inf, 0)**2)) * np.sqrt(annual_trading_days)) # noqa: E501
|
|
145
153
|
max_dd = -np.nan_to_num(dd.max())
|