BackcastPro 0.0.5__py3-none-any.whl → 0.1.1__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
@@ -3,11 +3,10 @@ BackcastPro をご利用いただきありがとうございます。
3
3
 
4
4
  インストール後のご案内(インストール済みユーザー向け)
5
5
 
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
+ - ドキュメント総合トップ: [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)
11
10
 
12
11
  ※ 使い始めはチュートリアル → 詳細はAPIリファレンスをご参照ください。
13
12
  """
BackcastPro/_broker.py CHANGED
@@ -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
- # Tips:
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, index):
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 = np.tile(np.nan, len(index))
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
- @property
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
- i = self._i = len(self._data) - 1
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[i] = 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
- self._close_trade(trade, self._data.Close.iloc[-1], i)
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): # type: Order
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
- if order.is_long
227
- else order.limit >= (stop_price or np.inf)))
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
- if order.is_long else
234
- max(stop_price or open, order.limit))
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 = data.Close.iloc[-2]
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
- 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
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
- 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: 注文はブローカーによってキャンセルされる?
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
- if not need_size:
319
- break
320
-
321
- # 注文をカバーするのに十分な流動性がない場合、ブローカーはそれをキャンセルする
322
- if abs(need_size) * adjusted_price_plus_commission > \
323
- self.margin_available * self._leverage:
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, time_index: int):
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
- # Reduce existing trade ...
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
- # ... by closing a reduced copy of it
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, time_index)
389
+ self._close_trade(close_trade, price, current_time)
386
390
 
387
- def _close_trade(self, trade: Trade, price: float, time_index: int):
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, exit_bar=time_index)
398
+ closed_trade = trade._replace(exit_price=price, exit_time=current_time)
395
399
  self.closed_trades.append(closed_trade)
396
- # Apply commission one more time at trade exit
400
+ # 取引終了時に手数料を再度適用
397
401
  commission = self._commission(trade.size, price)
398
402
  self._cash += trade.pl - commission
399
- # Save commissions on Trade instance for stats
403
+ # 統計用にTradeインスタンスに手数料を保存
400
404
  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()
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], time_index: int, tag):
407
- trade = Trade(self, size, price, time_index, tag)
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
- # Apply broker commission at trade open
413
+ # 取引開始時にブローカー手数料を適用
410
414
  self._cash -= self._commission(size, price)
411
- # Create SL/TP (bracket) orders.
415
+ # SL/TP(ブラケット)注文を作成。
412
416
  if tp:
413
417
  trade.tp = tp
414
418
  if sl:
BackcastPro/_stats.py CHANGED
@@ -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のために以下を回避し、nanシリーズを返す
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
- """Return data index period as pd.Timedelta"""
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
- ohlc_data: pd.DataFrame,
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
- index = ohlc_data.index
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
- # Came straight from Backtest.run()
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 # In "n bars" time, not index time
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
- # Annualized return and risk metrics are computed based on the (mostly correct)
127
- # assumption that the returns are compounded. See: https://dx.doi.org/10.2139/ssrn.3054517
128
- # Our annualized return matches `empyrical.annual_return(day_returns)` whereas
129
- # our risk doesn't; they use the simpler approach below.
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
- # Our Sharpe mismatches `empyrical.sharpe_ratio()` because they use arithmetic mean return
140
- # and simple standard deviation
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
- # Our Sortino mismatches `empyrical.sortino_ratio()` because they use arithmetic mean return
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())