BackcastPro 0.3.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.
- BackcastPro/__init__.py +28 -0
- BackcastPro/_broker.py +430 -0
- BackcastPro/_stats.py +177 -0
- BackcastPro/api/__init__.py +4 -0
- BackcastPro/api/board.py +130 -0
- BackcastPro/api/chart.py +527 -0
- BackcastPro/api/db_manager.py +283 -0
- BackcastPro/api/db_stocks_board.py +428 -0
- BackcastPro/api/db_stocks_daily.py +507 -0
- BackcastPro/api/db_stocks_info.py +260 -0
- BackcastPro/api/lib/__init__.py +4 -0
- BackcastPro/api/lib/e_api.py +588 -0
- BackcastPro/api/lib/jquants.py +384 -0
- BackcastPro/api/lib/kabusap.py +222 -0
- BackcastPro/api/lib/stooq.py +409 -0
- BackcastPro/api/lib/util.py +38 -0
- BackcastPro/api/stocks_board.py +77 -0
- BackcastPro/api/stocks_info.py +88 -0
- BackcastPro/api/stocks_price.py +131 -0
- BackcastPro/backtest.py +594 -0
- BackcastPro/order.py +161 -0
- BackcastPro/position.py +60 -0
- BackcastPro/trade.py +227 -0
- backcastpro-0.3.4.dist-info/METADATA +112 -0
- backcastpro-0.3.4.dist-info/RECORD +26 -0
- backcastpro-0.3.4.dist-info/WHEEL +4 -0
BackcastPro/backtest.py
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""
|
|
2
|
+
バックテスト管理モジュール。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import warnings
|
|
7
|
+
from functools import partial
|
|
8
|
+
from numbers import Number
|
|
9
|
+
from typing import Callable, List, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
try:
|
|
14
|
+
import plotly.graph_objects as go
|
|
15
|
+
HAS_PLOTLY = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
HAS_PLOTLY = False
|
|
18
|
+
|
|
19
|
+
from ._broker import _Broker
|
|
20
|
+
from ._stats import compute_stats
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Backtest:
|
|
24
|
+
"""
|
|
25
|
+
特定のデータに対してバックテストを実行します。
|
|
26
|
+
|
|
27
|
+
バックテストを初期化します。
|
|
28
|
+
初期化後、`Backtest.run_with_strategy`メソッドを呼び出して実行します。
|
|
29
|
+
|
|
30
|
+
`data`は以下の列を持つ`pd.DataFrame`です:
|
|
31
|
+
`Open`, `High`, `Low`, `Close`, および(オプションで)`Volume`。
|
|
32
|
+
列が不足している場合は、利用可能なものに設定してください。
|
|
33
|
+
例:
|
|
34
|
+
|
|
35
|
+
df['Open'] = df['High'] = df['Low'] = df['Close']
|
|
36
|
+
|
|
37
|
+
渡されたデータフレームには、戦略で使用できる追加の列
|
|
38
|
+
(例:センチメント情報)を含めることができます。
|
|
39
|
+
DataFrameのインデックスは、datetimeインデックス(タイムスタンプ)または
|
|
40
|
+
単調増加の範囲インデックス(期間のシーケンス)のいずれかです。
|
|
41
|
+
|
|
42
|
+
`cash`は開始時の初期現金です。
|
|
43
|
+
|
|
44
|
+
`spread`は一定のビッドアスクスプレッド率(価格に対する相対値)です。
|
|
45
|
+
例:平均スプレッドがアスク価格の約0.2‰である手数料なしの
|
|
46
|
+
外国為替取引では`0.0002`に設定してください。
|
|
47
|
+
|
|
48
|
+
`commission`は手数料率です。例:ブローカーの手数料が
|
|
49
|
+
注文価値の1%の場合、commissionを`0.01`に設定してください。
|
|
50
|
+
手数料は2回適用されます:取引開始時と取引終了時です。
|
|
51
|
+
単一の浮動小数点値に加えて、`commission`は浮動小数点値の
|
|
52
|
+
タプル`(fixed, relative)`にすることもできます。例:ブローカーが
|
|
53
|
+
最低$100 + 1%を請求する場合は`(100, .01)`に設定してください。
|
|
54
|
+
さらに、`commission`は呼び出し可能な
|
|
55
|
+
`func(order_size: int, price: float) -> float`
|
|
56
|
+
(注:ショート注文では注文サイズは負の値)にすることもでき、
|
|
57
|
+
より複雑な手数料構造をモデル化するために使用できます。
|
|
58
|
+
負の手数料値はマーケットメーカーのリベートとして解釈されます。
|
|
59
|
+
|
|
60
|
+
`margin`はレバレッジアカウントの必要証拠金(比率)です。
|
|
61
|
+
初期証拠金と維持証拠金の区別はありません。
|
|
62
|
+
ブローカーが許可する50:1レバレッジなどでバックテストを実行するには、
|
|
63
|
+
marginを`0.02`(1 / レバレッジ)に設定してください。
|
|
64
|
+
|
|
65
|
+
`trade_on_close`が`True`の場合、成行注文は
|
|
66
|
+
次のバーの始値ではなく、現在のバーの終値で約定されます。
|
|
67
|
+
|
|
68
|
+
`hedging`が`True`の場合、両方向の取引を同時に許可します。
|
|
69
|
+
`False`の場合、反対方向の注文は既存の取引を
|
|
70
|
+
[FIFO]方式で最初にクローズします。
|
|
71
|
+
|
|
72
|
+
`exclusive_orders`が`True`の場合、各新しい注文は前の
|
|
73
|
+
取引/ポジションを自動クローズし、各時点で最大1つの取引
|
|
74
|
+
(ロングまたはショート)のみが有効になります。
|
|
75
|
+
|
|
76
|
+
`finalize_trades`が`True`の場合、バックテスト終了時に
|
|
77
|
+
まだ[アクティブで継続中]の取引は最後のバーでクローズされ、
|
|
78
|
+
計算されたバックテスト統計に貢献します。
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self,
|
|
82
|
+
data: dict[str, pd.DataFrame] = None,
|
|
83
|
+
*,
|
|
84
|
+
cash: float = 10_000,
|
|
85
|
+
spread: float = .0,
|
|
86
|
+
commission: Union[float, Tuple[float, float]] = .0,
|
|
87
|
+
margin: float = 1.,
|
|
88
|
+
trade_on_close=False,
|
|
89
|
+
hedging=False,
|
|
90
|
+
exclusive_orders=False,
|
|
91
|
+
finalize_trades=False,
|
|
92
|
+
):
|
|
93
|
+
|
|
94
|
+
if not isinstance(spread, Number):
|
|
95
|
+
raise TypeError('`spread` must be a float value, percent of '
|
|
96
|
+
'entry order price')
|
|
97
|
+
if not isinstance(commission, (Number, tuple)) and not callable(commission):
|
|
98
|
+
raise TypeError('`commission` must be a float percent of order value, '
|
|
99
|
+
'a tuple of `(fixed, relative)` commission, '
|
|
100
|
+
'or a function that takes `(order_size, price)`'
|
|
101
|
+
'and returns commission dollar value')
|
|
102
|
+
|
|
103
|
+
self.set_data(data)
|
|
104
|
+
|
|
105
|
+
# partialとは、関数の一部の引数を事前に固定して、新しい関数を作成します。
|
|
106
|
+
# これにより、後で残りの引数だけを渡せば関数を実行できるようになります。
|
|
107
|
+
# 1. _Brokerクラスのコンストラクタの引数の一部(cash, spread, commissionなど)を事前に固定
|
|
108
|
+
# 2. 新しい関数(実際には呼び出し可能オブジェクト)を作成
|
|
109
|
+
# 3. 後で残りの引数(おそらくdataなど)を渡すだけで_Brokerのインスタンスを作成できるようにする
|
|
110
|
+
self._broker_factory = partial[_Broker](
|
|
111
|
+
_Broker, cash=cash, spread=spread, commission=commission, margin=margin,
|
|
112
|
+
trade_on_close=trade_on_close, hedging=hedging,
|
|
113
|
+
exclusive_orders=exclusive_orders
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self._results: Optional[pd.Series] = None
|
|
117
|
+
self._finalize_trades = bool(finalize_trades)
|
|
118
|
+
|
|
119
|
+
# ステップ実行用の状態管理
|
|
120
|
+
self._broker_instance: Optional[_Broker] = None
|
|
121
|
+
self._step_index = 0
|
|
122
|
+
self._is_started = False
|
|
123
|
+
self._is_finished = False
|
|
124
|
+
self._current_data: dict[str, pd.DataFrame] = {}
|
|
125
|
+
|
|
126
|
+
# パフォーマンス最適化: 各銘柄の index position マッピング
|
|
127
|
+
self._index_positions: dict[str, dict] = {}
|
|
128
|
+
|
|
129
|
+
# 自動的にstart()を呼び出す
|
|
130
|
+
if data is not None:
|
|
131
|
+
self.start()
|
|
132
|
+
|
|
133
|
+
def _validate_and_prepare_df(self, df: pd.DataFrame, code: str) -> pd.DataFrame:
|
|
134
|
+
"""
|
|
135
|
+
単一のDataFrameをバリデーションし、準備します。
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
df: バリデーションするDataFrame
|
|
139
|
+
code: データの識別子(エラーメッセージ用)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
バリデーション済みのDataFrame(コピー)
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
TypeError: DataFrameでない場合
|
|
146
|
+
ValueError: 必要な列がない場合、またはNaN値が含まれる場合
|
|
147
|
+
"""
|
|
148
|
+
if not isinstance(df, pd.DataFrame):
|
|
149
|
+
raise TypeError(f"`data[{code}]` must be a pandas.DataFrame with columns")
|
|
150
|
+
|
|
151
|
+
# データフレームのコピーを作成
|
|
152
|
+
df = df.copy()
|
|
153
|
+
|
|
154
|
+
# インデックスをdatetimeインデックスに変換
|
|
155
|
+
if (not isinstance(df.index, pd.DatetimeIndex) and
|
|
156
|
+
not isinstance(df.index, pd.RangeIndex) and
|
|
157
|
+
# 大部分が大きな数値の数値インデックス
|
|
158
|
+
(df.index.is_numeric() and
|
|
159
|
+
(df.index > pd.Timestamp('1975').timestamp()).mean() > .8)):
|
|
160
|
+
try:
|
|
161
|
+
df.index = pd.to_datetime(df.index, infer_datetime_format=True)
|
|
162
|
+
except ValueError:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
# Volume列がない場合は追加
|
|
166
|
+
if 'Volume' not in df:
|
|
167
|
+
df['Volume'] = np.nan
|
|
168
|
+
|
|
169
|
+
# 空のDataFrameチェック
|
|
170
|
+
if len(df) == 0:
|
|
171
|
+
raise ValueError(f'OHLC `data[{code}]` is empty')
|
|
172
|
+
|
|
173
|
+
# 必要な列の確認
|
|
174
|
+
if len(df.columns.intersection({'Open', 'High', 'Low', 'Close', 'Volume'})) != 5:
|
|
175
|
+
raise ValueError(f"`data[{code}]` must be a pandas.DataFrame with columns "
|
|
176
|
+
"'Open', 'High', 'Low', 'Close', and (optionally) 'Volume'")
|
|
177
|
+
|
|
178
|
+
# NaN値の確認
|
|
179
|
+
if df[['Open', 'High', 'Low', 'Close']].isnull().values.any():
|
|
180
|
+
raise ValueError('Some OHLC values are missing (NaN). '
|
|
181
|
+
'Please strip those lines with `df.dropna()` or '
|
|
182
|
+
'fill them in with `df.interpolate()` or whatever.')
|
|
183
|
+
|
|
184
|
+
# インデックスのソート確認
|
|
185
|
+
if not df.index.is_monotonic_increasing:
|
|
186
|
+
warnings.warn(f'data[{code}] index is not sorted in ascending order. Sorting.',
|
|
187
|
+
stacklevel=3)
|
|
188
|
+
df = df.sort_index()
|
|
189
|
+
|
|
190
|
+
# インデックスの型警告
|
|
191
|
+
if not isinstance(df.index, pd.DatetimeIndex):
|
|
192
|
+
warnings.warn(f'data[{code}] index is not datetime. Assuming simple periods, '
|
|
193
|
+
'but `pd.DateTimeIndex` is advised.',
|
|
194
|
+
stacklevel=3)
|
|
195
|
+
|
|
196
|
+
return df
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def set_data(self, data):
|
|
200
|
+
self._data = None
|
|
201
|
+
if data is None:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
data = data.copy()
|
|
205
|
+
|
|
206
|
+
# 各DataFrameをバリデーションして準備
|
|
207
|
+
for code, df in data.items():
|
|
208
|
+
data[code] = self._validate_and_prepare_df(df, code)
|
|
209
|
+
|
|
210
|
+
# 辞書dataに含まれる全てのdf.index一覧を作成
|
|
211
|
+
# df.indexが不一致の場合のために、どれかに固有値があれば抽出しておくため
|
|
212
|
+
self.index: pd.DatetimeIndex = pd.DatetimeIndex(sorted({idx for df in data.values() for idx in df.index}))
|
|
213
|
+
|
|
214
|
+
self._data: dict[str, pd.DataFrame] = data
|
|
215
|
+
|
|
216
|
+
def set_cash(self, cash):
|
|
217
|
+
self._broker_factory.keywords['cash'] = cash
|
|
218
|
+
|
|
219
|
+
# =========================================================================
|
|
220
|
+
# ステップ実行 API
|
|
221
|
+
# =========================================================================
|
|
222
|
+
|
|
223
|
+
def start(self) -> 'Backtest':
|
|
224
|
+
"""バックテストを開始準備する"""
|
|
225
|
+
if self._data is None:
|
|
226
|
+
raise ValueError("data が設定されていません")
|
|
227
|
+
|
|
228
|
+
self._broker_instance = self._broker_factory(data=self._data)
|
|
229
|
+
self._step_index = 0
|
|
230
|
+
self._is_started = True
|
|
231
|
+
self._is_finished = False
|
|
232
|
+
self._current_data = {}
|
|
233
|
+
self._results = None
|
|
234
|
+
|
|
235
|
+
# パフォーマンス最適化: 各銘柄の index → position マッピングを事前計算
|
|
236
|
+
self._index_positions = {}
|
|
237
|
+
for code, df in self._data.items():
|
|
238
|
+
self._index_positions[code] = {
|
|
239
|
+
ts: i for i, ts in enumerate(df.index)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
def step(self) -> bool:
|
|
245
|
+
"""
|
|
246
|
+
1ステップ(1バー)進める。
|
|
247
|
+
|
|
248
|
+
【タイミング】
|
|
249
|
+
- step(t) 実行時、data[:t] が見える状態になる
|
|
250
|
+
- 注文は broker.next(t) 内で処理される
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
bool: まだ続行可能なら True、終了なら False
|
|
254
|
+
"""
|
|
255
|
+
if not self._is_started:
|
|
256
|
+
raise RuntimeError("start() を呼び出してください")
|
|
257
|
+
|
|
258
|
+
if self._is_finished:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
if self._step_index >= len(self.index):
|
|
262
|
+
self._is_finished = True
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
current_time = self.index[self._step_index]
|
|
266
|
+
|
|
267
|
+
with np.errstate(invalid='ignore'):
|
|
268
|
+
# パフォーマンス最適化: iloc ベースで slicing
|
|
269
|
+
for code, df in self._data.items():
|
|
270
|
+
if current_time in self._index_positions[code]:
|
|
271
|
+
pos = self._index_positions[code][current_time]
|
|
272
|
+
self._current_data[code] = df.iloc[:pos + 1]
|
|
273
|
+
# current_time がこの銘柄に存在しない場合は前の状態を維持
|
|
274
|
+
|
|
275
|
+
# ブローカー処理(注文の約定)
|
|
276
|
+
try:
|
|
277
|
+
self._broker_instance._data = self._current_data
|
|
278
|
+
self._broker_instance.next(current_time)
|
|
279
|
+
except Exception:
|
|
280
|
+
self._is_finished = True
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
self._step_index += 1
|
|
284
|
+
|
|
285
|
+
if self._step_index >= len(self.index):
|
|
286
|
+
self._is_finished = True
|
|
287
|
+
|
|
288
|
+
return not self._is_finished
|
|
289
|
+
|
|
290
|
+
def reset(self) -> 'Backtest':
|
|
291
|
+
"""バックテストをリセットして最初から"""
|
|
292
|
+
self._broker_instance = self._broker_factory(data=self._data)
|
|
293
|
+
self._step_index = 0
|
|
294
|
+
self._is_finished = False
|
|
295
|
+
self._current_data = {}
|
|
296
|
+
self._results = None
|
|
297
|
+
return self
|
|
298
|
+
|
|
299
|
+
def goto(self, step: int, strategy: Callable[['Backtest'], None] = None) -> 'Backtest':
|
|
300
|
+
"""
|
|
301
|
+
指定ステップまで進める(スライダー連携用)
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
step: 目標のステップ番号(1-indexed、0以下は1に丸められる)
|
|
305
|
+
strategy: 各ステップで呼び出す戦略関数(省略可)
|
|
306
|
+
※ strategy は step() の **前** に呼ばれます
|
|
307
|
+
|
|
308
|
+
Note:
|
|
309
|
+
step < 現在位置 の場合、reset() してから再実行します。
|
|
310
|
+
"""
|
|
311
|
+
step = max(1, min(step, len(self.index)))
|
|
312
|
+
|
|
313
|
+
# 現在より前に戻る場合はリセット
|
|
314
|
+
if step < self._step_index:
|
|
315
|
+
self.reset()
|
|
316
|
+
|
|
317
|
+
# 目標まで進める(戦略を適用しながら)
|
|
318
|
+
while self._step_index < step and not self._is_finished:
|
|
319
|
+
if strategy:
|
|
320
|
+
strategy(self)
|
|
321
|
+
self.step()
|
|
322
|
+
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
# =========================================================================
|
|
326
|
+
# 売買 API
|
|
327
|
+
# =========================================================================
|
|
328
|
+
|
|
329
|
+
def buy(self, *,
|
|
330
|
+
code: str = None,
|
|
331
|
+
size: float = None,
|
|
332
|
+
limit: Optional[float] = None,
|
|
333
|
+
stop: Optional[float] = None,
|
|
334
|
+
sl: Optional[float] = None,
|
|
335
|
+
tp: Optional[float] = None,
|
|
336
|
+
tag: object = None):
|
|
337
|
+
"""
|
|
338
|
+
買い注文を発注する。
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
code: 銘柄コード(1銘柄のみの場合は省略可)
|
|
342
|
+
size: 注文数量(省略時は利用可能資金の99.99%)
|
|
343
|
+
limit: 指値価格
|
|
344
|
+
stop: 逆指値価格
|
|
345
|
+
sl: ストップロス価格
|
|
346
|
+
tp: テイクプロフィット価格
|
|
347
|
+
tag: 注文理由(例: "dip_buy", "breakout")→ チャートに表示可能
|
|
348
|
+
"""
|
|
349
|
+
if not self._is_started:
|
|
350
|
+
raise RuntimeError("start() を呼び出してください")
|
|
351
|
+
|
|
352
|
+
if code is None:
|
|
353
|
+
if len(self._data) == 1:
|
|
354
|
+
code = list(self._data.keys())[0]
|
|
355
|
+
else:
|
|
356
|
+
raise ValueError("複数銘柄がある場合はcodeを指定してください")
|
|
357
|
+
|
|
358
|
+
if size is None:
|
|
359
|
+
size = 1 - sys.float_info.epsilon
|
|
360
|
+
|
|
361
|
+
return self._broker_instance.new_order(code, size, limit, stop, sl, tp, tag)
|
|
362
|
+
|
|
363
|
+
def sell(self, *,
|
|
364
|
+
code: str = None,
|
|
365
|
+
size: float = None,
|
|
366
|
+
limit: Optional[float] = None,
|
|
367
|
+
stop: Optional[float] = None,
|
|
368
|
+
sl: Optional[float] = None,
|
|
369
|
+
tp: Optional[float] = None,
|
|
370
|
+
tag: object = None):
|
|
371
|
+
"""
|
|
372
|
+
売り注文を発注する。
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
code: 銘柄コード(1銘柄のみの場合は省略可)
|
|
376
|
+
size: 注文数量(省略時は利用可能資金の99.99%)
|
|
377
|
+
limit: 指値価格
|
|
378
|
+
stop: 逆指値価格
|
|
379
|
+
sl: ストップロス価格
|
|
380
|
+
tp: テイクプロフィット価格
|
|
381
|
+
tag: 注文理由(例: "profit_take", "stop_loss")→ チャートに表示可能
|
|
382
|
+
"""
|
|
383
|
+
if not self._is_started:
|
|
384
|
+
raise RuntimeError("start() を呼び出してください")
|
|
385
|
+
|
|
386
|
+
if code is None:
|
|
387
|
+
if len(self._data) == 1:
|
|
388
|
+
code = list(self._data.keys())[0]
|
|
389
|
+
else:
|
|
390
|
+
raise ValueError("複数銘柄がある場合はcodeを指定してください")
|
|
391
|
+
|
|
392
|
+
if size is None:
|
|
393
|
+
size = 1 - sys.float_info.epsilon
|
|
394
|
+
|
|
395
|
+
return self._broker_instance.new_order(code, -size, limit, stop, sl, tp, tag)
|
|
396
|
+
|
|
397
|
+
# =========================================================================
|
|
398
|
+
# 可視化
|
|
399
|
+
# =========================================================================
|
|
400
|
+
|
|
401
|
+
def chart(self, code: str = None, height: int = 500, show_tags: bool = True):
|
|
402
|
+
"""
|
|
403
|
+
現在時点までのローソク足チャートを生成(売買マーカー付き)
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
code: 銘柄コード
|
|
407
|
+
height: チャートの高さ
|
|
408
|
+
show_tags: 売買理由(tag)をチャートに表示するか
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
plotly.graph_objects.Figure
|
|
412
|
+
"""
|
|
413
|
+
if not HAS_PLOTLY:
|
|
414
|
+
raise ImportError("plotly がインストールされていません。pip install plotly を実行してください。")
|
|
415
|
+
|
|
416
|
+
if code is None:
|
|
417
|
+
if len(self._data) == 1:
|
|
418
|
+
code = list(self._data.keys())[0]
|
|
419
|
+
else:
|
|
420
|
+
raise ValueError("複数銘柄がある場合はcodeを指定してください")
|
|
421
|
+
|
|
422
|
+
if not self._is_started or self._broker_instance is None:
|
|
423
|
+
return go.Figure()
|
|
424
|
+
|
|
425
|
+
if code not in self._current_data or len(self._current_data[code]) == 0:
|
|
426
|
+
return go.Figure()
|
|
427
|
+
|
|
428
|
+
df = self._current_data[code]
|
|
429
|
+
|
|
430
|
+
# 全取引(アクティブ + 決済済み)を取得
|
|
431
|
+
all_trades = list(self._broker_instance.closed_trades) + list(self._broker_instance.trades)
|
|
432
|
+
|
|
433
|
+
# chart_by_df を呼び出してチャートを生成
|
|
434
|
+
from .api.chart import chart_by_df
|
|
435
|
+
return chart_by_df(
|
|
436
|
+
df,
|
|
437
|
+
trades=all_trades,
|
|
438
|
+
height=height,
|
|
439
|
+
show_tags=show_tags,
|
|
440
|
+
show_volume=False,
|
|
441
|
+
title=f"{code} - {self.current_time}",
|
|
442
|
+
code=code,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# =========================================================================
|
|
446
|
+
# ステップ実行用プロパティ
|
|
447
|
+
# =========================================================================
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def data(self) -> dict[str, pd.DataFrame]:
|
|
451
|
+
"""現在時点までのデータ"""
|
|
452
|
+
return self._current_data
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def position(self) -> int:
|
|
456
|
+
"""
|
|
457
|
+
現在のポジションサイズ(全銘柄合計)
|
|
458
|
+
|
|
459
|
+
⚠️ 注意: 複数銘柄を扱う場合は position_of(code) を使用してください。
|
|
460
|
+
このプロパティは後方互換性のために残されています。
|
|
461
|
+
"""
|
|
462
|
+
if not self._is_started or self._broker_instance is None:
|
|
463
|
+
return 0
|
|
464
|
+
return self._broker_instance.position.size
|
|
465
|
+
|
|
466
|
+
def position_of(self, code: str) -> int:
|
|
467
|
+
"""
|
|
468
|
+
指定銘柄のポジションサイズ(推奨)
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
code: 銘柄コード
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
int: ポジションサイズ(正: ロング、負: ショート、0: ノーポジ)
|
|
475
|
+
"""
|
|
476
|
+
if not self._is_started or self._broker_instance is None:
|
|
477
|
+
return 0
|
|
478
|
+
return sum(t.size for t in self._broker_instance.trades if t.code == code)
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def equity(self) -> float:
|
|
482
|
+
"""現在の資産"""
|
|
483
|
+
if not self._is_started or self._broker_instance is None:
|
|
484
|
+
return self._broker_factory.keywords.get('cash', 0)
|
|
485
|
+
return self._broker_instance.equity
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def is_finished(self) -> bool:
|
|
489
|
+
"""完了したかどうか"""
|
|
490
|
+
return self._is_finished
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def current_time(self) -> Optional[pd.Timestamp]:
|
|
494
|
+
"""現在の日時"""
|
|
495
|
+
if self._step_index == 0:
|
|
496
|
+
return None
|
|
497
|
+
return self.index[self._step_index - 1]
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def progress(self) -> float:
|
|
501
|
+
"""進捗率(0.0〜1.0)"""
|
|
502
|
+
if len(self.index) == 0:
|
|
503
|
+
return 0.0
|
|
504
|
+
return self._step_index / len(self.index)
|
|
505
|
+
|
|
506
|
+
@property
|
|
507
|
+
def trades(self) -> List:
|
|
508
|
+
"""アクティブな取引リスト"""
|
|
509
|
+
if not self._is_started or self._broker_instance is None:
|
|
510
|
+
return []
|
|
511
|
+
return list(self._broker_instance.trades)
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def closed_trades(self) -> List:
|
|
515
|
+
"""決済済み取引リスト"""
|
|
516
|
+
if not self._is_started or self._broker_instance is None:
|
|
517
|
+
return []
|
|
518
|
+
return list(self._broker_instance.closed_trades)
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def orders(self) -> List:
|
|
522
|
+
"""未約定の注文リスト"""
|
|
523
|
+
if not self._is_started or self._broker_instance is None:
|
|
524
|
+
return []
|
|
525
|
+
return list(self._broker_instance.orders)
|
|
526
|
+
|
|
527
|
+
# =========================================================================
|
|
528
|
+
# finalize / run
|
|
529
|
+
# =========================================================================
|
|
530
|
+
|
|
531
|
+
def finalize(self) -> pd.Series:
|
|
532
|
+
"""統計を計算して結果を返す"""
|
|
533
|
+
if self._results is not None:
|
|
534
|
+
return self._results
|
|
535
|
+
|
|
536
|
+
if not self._is_started:
|
|
537
|
+
raise RuntimeError("バックテストが開始されていません")
|
|
538
|
+
|
|
539
|
+
broker = self._broker_instance
|
|
540
|
+
|
|
541
|
+
if self._finalize_trades:
|
|
542
|
+
for trade in reversed(broker.trades):
|
|
543
|
+
trade.close()
|
|
544
|
+
if self._step_index > 0:
|
|
545
|
+
broker.next(self.index[self._step_index - 1])
|
|
546
|
+
elif len(broker.trades):
|
|
547
|
+
warnings.warn(
|
|
548
|
+
'バックテスト終了時に一部の取引がオープンのままです。'
|
|
549
|
+
'`Backtest(..., finalize_trades=True)`を使用してクローズし、'
|
|
550
|
+
'統計に含めてください。', stacklevel=2)
|
|
551
|
+
|
|
552
|
+
# インデックスが空の場合のガード
|
|
553
|
+
result_index = self.index[:self._step_index] if self._step_index > 0 else self.index[:1]
|
|
554
|
+
|
|
555
|
+
equity = pd.Series(broker._equity).bfill().fillna(broker._cash).values
|
|
556
|
+
self._results = compute_stats(
|
|
557
|
+
trades=broker.closed_trades,
|
|
558
|
+
equity=np.array(equity),
|
|
559
|
+
index=result_index,
|
|
560
|
+
strategy_instance=None,
|
|
561
|
+
risk_free_rate=0.0,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return self._results
|
|
565
|
+
|
|
566
|
+
def run_with_strategy(self, strategy_func: Callable[['Backtest'], None] = None) -> pd.Series:
|
|
567
|
+
"""
|
|
568
|
+
バックテストを最後まで実行(ステップ実行API版)
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
strategy_func: 各ステップで呼び出す関数 (bt) -> None
|
|
572
|
+
"""
|
|
573
|
+
if not self._is_started:
|
|
574
|
+
self.start()
|
|
575
|
+
|
|
576
|
+
while not self._is_finished:
|
|
577
|
+
if strategy_func:
|
|
578
|
+
strategy_func(self)
|
|
579
|
+
self.step()
|
|
580
|
+
|
|
581
|
+
return self.finalize()
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def cash(self):
|
|
585
|
+
"""現在の現金残高"""
|
|
586
|
+
if self._is_started and self._broker_instance is not None:
|
|
587
|
+
return self._broker_instance.cash
|
|
588
|
+
# partialで初期化されている場合、初期化時のcash値を返す
|
|
589
|
+
return self._broker_factory.keywords.get('cash', 0)
|
|
590
|
+
|
|
591
|
+
@property
|
|
592
|
+
def commission(self):
|
|
593
|
+
# partialで初期化されている場合、初期化時のcommission値を返す
|
|
594
|
+
return self._broker_factory.keywords.get('commission', 0)
|