BackcastPro 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,90 +1,15 @@
1
1
  """
2
+ BackcastPro をご利用いただきありがとうございます。
2
3
 
3
- ![xkcd.com/1570](https://imgs.xkcd.com/comics/engineer_syllogism.png){: height=263}
4
+ インストール後のご案内(インストール済みユーザー向け)
4
5
 
5
- ## Manuals
6
+ - ドキュメント総合トップ: https://botteryosuke.github.io/BackcastPro/
7
+ - クイックスタート/チュートリアル: https://botteryosuke.github.io/BackcastPro/tutorial
8
+ - APIリファレンス: https://botteryosuke.github.io/BackcastPro/api-reference
9
+ - 高度な使い方: https://botteryosuke.github.io/BackcastPro/advanced-usage
10
+ - トラブルシューティング: https://botteryosuke.github.io/BackcastPro/troubleshooting
6
11
 
7
- * [**Quick Start User Guide**](../examples/Quick Start User Guide.html)
8
-
9
- ## Tutorials
10
-
11
- The tutorials encompass most framework features, so it's important
12
- and advisable to go through all of them. They are short.
13
-
14
- * [Library of Utilities and Composable Base Strategies](../examples/Strategies Library.html)
15
- * [Multiple Time Frames](../examples/Multiple Time Frames.html)
16
- * [**Parameter Heatmap & Optimization**](../examples/Parameter Heatmap & Optimization.html)
17
- * [Trading with Machine Learning](../examples/Trading with Machine Learning.html)
18
-
19
- These tutorials are also available as live Jupyter notebooks:
20
- [![Binder](https://mybinder.org/badge_logo.svg)][binder]
21
- [![Google Colab](https://colab.research.google.com/assets/colab-badge.png)][colab]
22
- <br>In Colab, you might have to `!pip install backtesting`.
23
-
24
- [binder]: \
25
- https://mybinder.org/v2/gh/kernc/backtesting.py/master?\
26
- urlpath=lab%2Ftree%2Fdoc%2Fexamples%2FQuick%20Start%20User%20Guide.ipynb
27
- [colab]: https://colab.research.google.com/github/kernc/backtesting.py/
28
-
29
- ## Video Tutorials
30
-
31
- * Some [**coverage on YouTube**](https://github.com/kernc/backtesting.py/discussions/677).
32
- * [YouTube search](https://www.youtube.com/results?q=%22backtesting.py%22)
33
-
34
- ## Example Strategies
35
-
36
- * (contributions welcome)
37
-
38
-
39
- .. tip::
40
- For an overview of recent changes, see
41
- [What's New, i.e. the **Change Log**](https://github.com/kernc/backtesting.py/blob/master/CHANGELOG.md).
42
-
43
-
44
- ## FAQ
45
-
46
- Some answers to frequent and popular questions can be found on the
47
- [issue tracker](https://github.com/kernc/backtesting.py/issues?q=label%3Aquestion+-label%3Ainvalid)
48
- or on the [discussion forum](https://github.com/kernc/backtesting.py/discussions) on GitHub.
49
- Please use the search!
50
-
51
- ## License
52
-
53
- This software is licensed under the terms of [AGPL 3.0]{: rel=license},
54
- meaning you can use it for any reasonable purpose and remain in
55
- complete ownership of all the excellent trading strategies you produce,
56
- but you are also encouraged to make sure any upgrades to _Backtesting.py_
57
- itself find their way back to the community.
58
-
59
- [AGPL 3.0]: https://www.gnu.org/licenses/agpl-3.0.html
60
-
61
- # API Reference Documentation
12
+ 使い始めはチュートリアル 詳細はAPIリファレンスをご参照ください。
62
13
  """
63
- try:
64
- from ._version import version as __version__
65
- except ImportError:
66
- __version__ = '?.?.?' # Package not installed
67
-
68
- from . import lib # noqa: F401
69
- from ._plotting import set_bokeh_output # noqa: F401
70
- from .backtesting import Backtest, Strategy # noqa: F401
71
-
72
-
73
- # Add overridable backtesting.Pool used for parallel optimization
74
- def Pool(processes=None, initializer=None, initargs=()):
75
- import multiprocessing as mp
76
- if mp.get_start_method() == 'spawn':
77
- import warnings
78
- warnings.warn(
79
- "If you want to use multi-process optimization with "
80
- "`multiprocessing.get_start_method() == 'spawn'` (e.g. on Windows),"
81
- "set `backtesting.Pool = multiprocessing.Pool` (or of the desired context) "
82
- "and hide `bt.optimize()` call behind a `if __name__ == '__main__'` guard. "
83
- "Currently using thread-based paralellism, "
84
- "which might be slightly slower for non-numpy / non-GIL-releasing code. "
85
- "See https://github.com/kernc/backtesting.py/issues/1256",
86
- category=RuntimeWarning, stacklevel=3)
87
- from multiprocessing.dummy import Pool
88
- return Pool(processes, initializer, initargs)
89
- else:
90
- return mp.Pool(processes, initializer, initargs)
14
+ from .backtest import Backtest
15
+ from .strategy import Strategy
BackcastPro/_broker.py ADDED
@@ -0,0 +1,415 @@
1
+ """
2
+ ブローカー管理モジュール。
3
+ """
4
+
5
+ import warnings
6
+ from math import copysign
7
+ from typing import List, Optional, TYPE_CHECKING
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from .order import Order
13
+ from .position import Position
14
+ from .trade import Trade
15
+
16
+ if TYPE_CHECKING:
17
+ pass
18
+
19
+
20
+ class _Broker:
21
+ """
22
+ バックテストにおける証券取引の実行、注文管理、ポジション管理、損益計算を担当します。
23
+ 実際の証券会社のブローカー機能をシミュレートし、リアルな取引環境を提供します。
24
+
25
+ Parameters
26
+ ----------
27
+ data : pd.DataFrame
28
+ 取引対象の価格データ。Open, High, Low, Closeの列を持つ必要があります。
29
+ cash : float
30
+ 初期現金残高。正の値である必要があります。
31
+ spread : float
32
+ ビッドアスクスプレッド(買値と売値の差)。取引コストとして使用されます。
33
+ commission : float or tuple or callable
34
+ 手数料の設定方法:
35
+ - float: 相対手数料(例: 0.001 = 0.1%)
36
+ - tuple: (固定手数料, 相対手数料) の組み合わせ
37
+ - callable: カスタム手数料計算関数 (size, price) -> 手数料
38
+ margin : float
39
+ 必要証拠金率(0 < margin <= 1)。レバレッジ = 1/margin として計算されます。
40
+ trade_on_close : bool
41
+ 取引を終値で実行するかどうか。Trueの場合、次の始値ではなく現在の終値で取引します。
42
+ hedging : bool
43
+ ヘッジングモードの有効化。Trueの場合、反対方向のポジションを同時に保有できます。
44
+ exclusive_orders : bool
45
+ 排他的注文モード。Trueの場合、新しい注文が前のポジションを自動的にクローズします。
46
+ index : pd.Index
47
+ 時系列データのインデックス。エクイティカーブの記録に使用されます。
48
+ """
49
+ # Tips:
50
+ # 関数定義における`*`の意味
51
+ # - `*`以降の引数は、必ずキーワード引数として渡す必要がある
52
+ # - 位置引数として渡すことはできない
53
+ # なぜキーワード専用引数を使うのか?
54
+ # 1. APIの明確性: 引数の意味が明確になる
55
+ # 2. 保守性: 引数の順序を変更しても既存のコードが壊れない
56
+ # 3. 可読性: 関数呼び出し時に何を渡しているかが分かりやすい
57
+ def __init__(self, *, data, cash, spread, commission, margin,
58
+ trade_on_close, hedging, exclusive_orders, index):
59
+ assert cash > 0, f"cash should be > 0, is {cash}"
60
+ assert 0 < margin <= 1, f"margin should be between 0 and 1, is {margin}"
61
+ self._data: pd.DataFrame = data
62
+ self._cash = cash
63
+
64
+ # 手数料の登録
65
+ if callable(commission):
66
+ # 関数`commission`が呼び出し可能な場合
67
+ self._commission = commission
68
+ else:
69
+ try:
70
+ self._commission_fixed, self._commission_relative = commission
71
+ except TypeError:
72
+ self._commission_fixed, self._commission_relative = 0, commission
73
+ assert self._commission_fixed >= 0, 'Need fixed cash commission in $ >= 0'
74
+ assert -.1 <= self._commission_relative < .1, \
75
+ ("commission should be between -10% "
76
+ f"(e.g. market-maker's rebates) and 10% (fees), is {self._commission_relative}")
77
+ self._commission = self._commission_func
78
+
79
+
80
+ self._spread = spread
81
+ self._leverage = 1 / margin
82
+ self._trade_on_close = trade_on_close
83
+ self._hedging = hedging
84
+ self._exclusive_orders = exclusive_orders
85
+
86
+ self._equity = np.tile(np.nan, len(index))
87
+ self.orders: List[Order] = []
88
+ self.trades: List[Trade] = []
89
+ self.position = Position(self)
90
+ self.closed_trades: List[Trade] = []
91
+
92
+ def _commission_func(self, order_size, price):
93
+ return self._commission_fixed + abs(order_size) * price * self._commission_relative
94
+
95
+ def __repr__(self):
96
+ return f'<Broker: {self._cash:.0f}{self.position.pl:+.1f} ({len(self.trades)} trades)>'
97
+
98
+ def new_order(self,
99
+ size: float,
100
+ limit: Optional[float] = None,
101
+ stop: Optional[float] = None,
102
+ sl: Optional[float] = None,
103
+ tp: Optional[float] = None,
104
+ tag: object = None,
105
+ *,
106
+ trade: Optional[Trade] = None) -> Order:
107
+ """
108
+ Argument size indicates whether the order is long or short
109
+ """
110
+ size = float(size)
111
+ stop = stop and float(stop)
112
+ limit = limit and float(limit)
113
+ sl = sl and float(sl)
114
+ tp = tp and float(tp)
115
+
116
+ is_long = size > 0
117
+ assert size != 0, size
118
+ adjusted_price = self._adjusted_price(size)
119
+
120
+ if is_long:
121
+ if not (sl or -np.inf) < (limit or stop or adjusted_price) < (tp or np.inf):
122
+ raise ValueError(
123
+ "Long orders require: "
124
+ f"SL ({sl}) < LIMIT ({limit or stop or adjusted_price}) < TP ({tp})")
125
+ else:
126
+ if not (tp or -np.inf) < (limit or stop or adjusted_price) < (sl or np.inf):
127
+ raise ValueError(
128
+ "Short orders require: "
129
+ f"TP ({tp}) < LIMIT ({limit or stop or adjusted_price}) < SL ({sl})")
130
+
131
+ order = Order(self, size, limit, stop, sl, tp, trade, tag)
132
+
133
+ if not trade:
134
+ # 排他的注文(各新しい注文が前の注文/ポジションを自動クローズ)の場合、
135
+ # 事前にすべての非条件付き注文をキャンセルし、すべてのオープン取引をクローズ
136
+ if self._exclusive_orders:
137
+ for o in self.orders:
138
+ if not o.is_contingent:
139
+ o.cancel()
140
+ for t in self.trades:
141
+ t.close()
142
+
143
+ # 新しい注文を注文キューに配置、SL注文が最初に処理されるようにする
144
+ self.orders.insert(0 if trade and stop else len(self.orders), order)
145
+
146
+ return order
147
+
148
+ @property
149
+ def last_price(self) -> float:
150
+ """ Price at the last (current) close. """
151
+ return self._data.Close.iloc[-1]
152
+
153
+ def _adjusted_price(self, size=None, price=None) -> float:
154
+ """
155
+ Long/short `price`, adjusted for spread.
156
+ In long positions, the adjusted price is a fraction higher, and vice versa.
157
+ """
158
+ return (price or self.last_price) * (1 + copysign(self._spread, size))
159
+
160
+ @property
161
+ def equity(self) -> float:
162
+ return self._cash + sum(trade.pl for trade in self.trades)
163
+
164
+ @property
165
+ def margin_available(self) -> float:
166
+ # https://github.com/QuantConnect/Lean/pull/3768 から
167
+ margin_used = sum(trade.value / self._leverage for trade in self.trades)
168
+ return max(0, self.equity - margin_used)
169
+
170
+ @property
171
+ def cash(self):
172
+ return self._cash
173
+
174
+ @property
175
+ def commission(self):
176
+ return self._commission
177
+
178
+ def next(self):
179
+ i = self._i = len(self._data) - 1
180
+ self._process_orders()
181
+
182
+ # エクイティカーブ用にアカウントエクイティを記録
183
+ equity = self.equity
184
+ self._equity[i] = equity
185
+
186
+ # エクイティが負の場合、すべてを0に設定してシミュレーションを停止
187
+ if equity <= 0:
188
+ assert self.margin_available <= 0
189
+ for trade in self.trades:
190
+ self._close_trade(trade, self._data.Close.iloc[-1], i)
191
+ self._cash = 0
192
+ self._equity[i:] = 0
193
+ raise Exception
194
+
195
+ def _process_orders(self):
196
+ data = self._data
197
+ open, high, low = data.Open.iloc[-1], data.High.iloc[-1], data.Low.iloc[-1]
198
+ reprocess_orders = False
199
+
200
+ # 注文を処理
201
+ for order in list(self.orders): # type: Order
202
+
203
+ # 関連するSL/TP注文は既に削除されている
204
+ if order not in self.orders:
205
+ continue
206
+
207
+ # ストップ条件が満たされたかチェック
208
+ stop_price = order.stop
209
+ if stop_price:
210
+ is_stop_hit = ((high >= stop_price) if order.is_long else (low <= stop_price))
211
+ if not is_stop_hit:
212
+ continue
213
+
214
+ # ストップ価格に達すると、ストップ注文は成行/指値注文になる
215
+ # https://www.sec.gov/fast-answers/answersstopordhtm.html
216
+ order._replace(stop_price=None)
217
+
218
+ # 購入価格を決定
219
+ # 指値注文が約定可能かチェック
220
+ if order.limit:
221
+ is_limit_hit = low <= order.limit if order.is_long else high >= order.limit
222
+ # ストップとリミットが同じバー内で満たされた場合、悲観的に
223
+ # リミットがストップより先に満たされたと仮定する(つまり「カウントされる前に」)
224
+ is_limit_hit_before_stop = (is_limit_hit and
225
+ (order.limit <= (stop_price or -np.inf)
226
+ if order.is_long
227
+ else order.limit >= (stop_price or np.inf)))
228
+ if not is_limit_hit or is_limit_hit_before_stop:
229
+ continue
230
+
231
+ # stop_priceが設定されている場合、このバー内で満たされた
232
+ price = (min(stop_price or open, order.limit)
233
+ if order.is_long else
234
+ max(stop_price or open, order.limit))
235
+ else:
236
+ # 成行注文(Market-if-touched / market order)
237
+ # 条件付き注文は常に次の始値で
238
+ prev_close = data.Close.iloc[-2]
239
+ price = prev_close if self._trade_on_close and not order.is_contingent else open
240
+ if stop_price:
241
+ price = max(price, stop_price) if order.is_long else min(price, stop_price)
242
+
243
+ # エントリー/エグジットバーのインデックスを決定
244
+ is_market_order = not order.limit and not stop_price
245
+ time_index = (
246
+ (self._i - 1)
247
+ if is_market_order and self._trade_on_close and not order.is_contingent else
248
+ self._i)
249
+
250
+ # 注文がSL/TP注文の場合、それが依存していた既存の取引をクローズする必要がある
251
+ if order.parent_trade:
252
+ trade = order.parent_trade
253
+ _prev_size = trade.size
254
+ # order.sizeがtrade.sizeより「大きい」場合、この注文はtrade.close()注文で
255
+ # 取引の一部は事前にクローズされている
256
+ size = copysign(min(abs(_prev_size), abs(order.size)), order.size)
257
+ # この取引がまだクローズされていない場合(例:複数の`trade.close(.5)`呼び出し)
258
+ if trade in self.trades:
259
+ self._reduce_trade(trade, price, size, time_index)
260
+ assert order.size != -_prev_size or trade not in self.trades
261
+ if price == stop_price:
262
+ # 統計用にSLを注文に戻す
263
+ trade._sl_order._replace(stop_price=stop_price)
264
+ if order in (trade._sl_order,
265
+ trade._tp_order):
266
+ assert order.size == -trade.size
267
+ assert order not in self.orders # 取引がクローズされたときに削除される
268
+ else:
269
+ # trade.close()注文で、完了
270
+ assert abs(_prev_size) >= abs(size) >= 1
271
+ self.orders.remove(order)
272
+ continue
273
+
274
+ # そうでなければ、これは独立した取引
275
+
276
+ # 手数料(またはビッドアスクスプレッド)を含むように価格を調整
277
+ # ロングポジションでは調整価格が少し高くなり、その逆も同様
278
+ adjusted_price = self._adjusted_price(order.size, price)
279
+ adjusted_price_plus_commission = \
280
+ adjusted_price + self._commission(order.size, price) / abs(order.size)
281
+
282
+ # 注文サイズが比例的に指定された場合、
283
+ # マージンとスプレッド/手数料を考慮して、単位での真のサイズを事前計算
284
+ size = order.size
285
+ if -1 < size < 1:
286
+ size = copysign(int((self.margin_available * self._leverage * abs(size))
287
+ // adjusted_price_plus_commission), size)
288
+ # 単一ユニットでも十分な現金/マージンがない
289
+ if not size:
290
+ warnings.warn(
291
+ f'time={self._i}: ブローカーは相対サイズの注文を'
292
+ f'不十分なマージンのためキャンセルしました。', category=UserWarning)
293
+ # XXX: 注文はブローカーによってキャンセルされる?
294
+ self.orders.remove(order)
295
+ 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
+
318
+ if not need_size:
319
+ break
320
+
321
+ # 注文をカバーするのに十分な流動性がない場合、ブローカーはそれをキャンセルする
322
+ if abs(need_size) * adjusted_price_plus_commission > \
323
+ self.margin_available * self._leverage:
324
+ 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
+
362
+ if reprocess_orders:
363
+ self._process_orders()
364
+
365
+ def _reduce_trade(self, trade: Trade, price: float, size: float, time_index: int):
366
+ assert trade.size * size < 0
367
+ assert abs(trade.size) >= abs(size)
368
+
369
+ size_left = trade.size + size
370
+ assert size_left * trade.size >= 0
371
+ if not size_left:
372
+ close_trade = trade
373
+ else:
374
+ # Reduce existing trade ...
375
+ trade._replace(size=size_left)
376
+ if trade._sl_order:
377
+ trade._sl_order._replace(size=-trade.size)
378
+ if trade._tp_order:
379
+ trade._tp_order._replace(size=-trade.size)
380
+
381
+ # ... by closing a reduced copy of it
382
+ close_trade = trade._copy(size=-size, sl_order=None, tp_order=None)
383
+ self.trades.append(close_trade)
384
+
385
+ self._close_trade(close_trade, price, time_index)
386
+
387
+ def _close_trade(self, trade: Trade, price: float, time_index: int):
388
+ self.trades.remove(trade)
389
+ if trade._sl_order:
390
+ self.orders.remove(trade._sl_order)
391
+ if trade._tp_order:
392
+ self.orders.remove(trade._tp_order)
393
+
394
+ closed_trade = trade._replace(exit_price=price, exit_bar=time_index)
395
+ self.closed_trades.append(closed_trade)
396
+ # Apply commission one more time at trade exit
397
+ commission = self._commission(trade.size, price)
398
+ self._cash += trade.pl - commission
399
+ # Save commissions on Trade instance for stats
400
+ trade_open_commission = self._commission(closed_trade.size, closed_trade.entry_price)
401
+ # applied here instead of on Trade open because size could have changed
402
+ # by way of _reduce_trade()
403
+ closed_trade._commissions = commission + trade_open_commission
404
+
405
+ def _open_trade(self, price: float, size: int,
406
+ sl: Optional[float], tp: Optional[float], time_index: int, tag):
407
+ trade = Trade(self, size, price, time_index, tag)
408
+ self.trades.append(trade)
409
+ # Apply broker commission at trade open
410
+ self._cash -= self._commission(size, price)
411
+ # Create SL/TP (bracket) orders.
412
+ if tp:
413
+ trade.tp = tp
414
+ if sl:
415
+ trade.sl = sl