BackcastPro 0.0.1__py3-none-any.whl → 0.0.3__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 +8 -0
- BackcastPro/_broker.py +390 -0
- BackcastPro/_stats.py +169 -0
- BackcastPro/backtest.py +269 -0
- BackcastPro/data/__init__.py +7 -0
- BackcastPro/data/datareader.py +168 -0
- BackcastPro/order.py +154 -0
- BackcastPro/position.py +61 -0
- BackcastPro/strategy.py +174 -0
- BackcastPro/trade.py +195 -0
- backcastpro-0.0.3.dist-info/METADATA +59 -0
- backcastpro-0.0.3.dist-info/RECORD +14 -0
- BackcastPro/example.py +0 -2
- backcastpro-0.0.1.dist-info/METADATA +0 -18
- backcastpro-0.0.1.dist-info/RECORD +0 -6
- {backcastpro-0.0.1.dist-info → backcastpro-0.0.3.dist-info}/WHEEL +0 -0
- {backcastpro-0.0.1.dist-info → backcastpro-0.0.3.dist-info}/top_level.txt +0 -0
BackcastPro/backtest.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
バックテスト管理モジュール。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from functools import partial
|
|
7
|
+
from numbers import Number
|
|
8
|
+
from typing import Optional, Tuple, Type, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from ._broker import _Broker
|
|
14
|
+
from ._stats import compute_stats
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Backtest:
|
|
18
|
+
"""
|
|
19
|
+
特定のデータに対して特定の(パラメータ化された)戦略をバックテストします。
|
|
20
|
+
|
|
21
|
+
バックテストを初期化します。テストするデータと戦略が必要です。
|
|
22
|
+
初期化後、バックテストインスタンスを実行するために
|
|
23
|
+
`Backtest.run`メソッドを呼び出す。
|
|
24
|
+
|
|
25
|
+
`data`は以下の列を持つ`pd.DataFrame`です:
|
|
26
|
+
`Open`, `High`, `Low`, `Close`, および(オプションで)`Volume`。
|
|
27
|
+
列が不足している場合は、利用可能なものに設定してください。
|
|
28
|
+
例:
|
|
29
|
+
|
|
30
|
+
df['Open'] = df['High'] = df['Low'] = df['Close']
|
|
31
|
+
|
|
32
|
+
渡されたデータフレームには、戦略で使用できる追加の列
|
|
33
|
+
(例:センチメント情報)を含めることができます。
|
|
34
|
+
DataFrameのインデックスは、datetimeインデックス(タイムスタンプ)または
|
|
35
|
+
単調増加の範囲インデックス(期間のシーケンス)のいずれかです。
|
|
36
|
+
|
|
37
|
+
`strategy`は`Strategy`の
|
|
38
|
+
_サブクラス_(インスタンスではありません)です。
|
|
39
|
+
|
|
40
|
+
`cash`は開始時の初期現金です。
|
|
41
|
+
|
|
42
|
+
`spread`は一定のビッドアスクスプレッド率(価格に対する相対値)です。
|
|
43
|
+
例:平均スプレッドがアスク価格の約0.2‰である手数料なしの
|
|
44
|
+
外国為替取引では`0.0002`に設定してください。
|
|
45
|
+
|
|
46
|
+
`commission`は手数料率です。例:ブローカーの手数料が
|
|
47
|
+
注文価値の1%の場合、commissionを`0.01`に設定してください。
|
|
48
|
+
手数料は2回適用されます:取引開始時と取引終了時です。
|
|
49
|
+
単一の浮動小数点値に加えて、`commission`は浮動小数点値の
|
|
50
|
+
タプル`(fixed, relative)`にすることもできます。例:ブローカーが
|
|
51
|
+
最低$100 + 1%を請求する場合は`(100, .01)`に設定してください。
|
|
52
|
+
さらに、`commission`は呼び出し可能な
|
|
53
|
+
`func(order_size: int, price: float) -> float`
|
|
54
|
+
(注:ショート注文では注文サイズは負の値)にすることもでき、
|
|
55
|
+
より複雑な手数料構造をモデル化するために使用できます。
|
|
56
|
+
負の手数料値はマーケットメーカーのリベートとして解釈されます。
|
|
57
|
+
|
|
58
|
+
`margin`はレバレッジアカウントの必要証拠金(比率)です。
|
|
59
|
+
初期証拠金と維持証拠金の区別はありません。
|
|
60
|
+
ブローカーが許可する50:1レバレッジなどでバックテストを実行するには、
|
|
61
|
+
marginを`0.02`(1 / レバレッジ)に設定してください。
|
|
62
|
+
|
|
63
|
+
`trade_on_close`が`True`の場合、成行注文は
|
|
64
|
+
次のバーの始値ではなく、現在のバーの終値で約定されます。
|
|
65
|
+
|
|
66
|
+
`hedging`が`True`の場合、両方向の取引を同時に許可します。
|
|
67
|
+
`False`の場合、反対方向の注文は既存の取引を
|
|
68
|
+
[FIFO]方式で最初にクローズします。
|
|
69
|
+
|
|
70
|
+
`exclusive_orders`が`True`の場合、各新しい注文は前の
|
|
71
|
+
取引/ポジションを自動クローズし、各時点で最大1つの取引
|
|
72
|
+
(ロングまたはショート)のみが有効になります。
|
|
73
|
+
|
|
74
|
+
`finalize_trades`が`True`の場合、バックテスト終了時に
|
|
75
|
+
まだ[アクティブで継続中]の取引は最後のバーでクローズされ、
|
|
76
|
+
計算されたバックテスト統計に貢献します。
|
|
77
|
+
"""
|
|
78
|
+
def __init__(self,
|
|
79
|
+
data: pd.DataFrame,
|
|
80
|
+
strategy: Type,
|
|
81
|
+
*,
|
|
82
|
+
cash: float = 10_000,
|
|
83
|
+
spread: float = .0,
|
|
84
|
+
commission: Union[float, Tuple[float, float]] = .0,
|
|
85
|
+
margin: float = 1.,
|
|
86
|
+
trade_on_close=False,
|
|
87
|
+
hedging=False,
|
|
88
|
+
exclusive_orders=False,
|
|
89
|
+
finalize_trades=False,
|
|
90
|
+
):
|
|
91
|
+
# 循環インポートを避けるためにここでインポート
|
|
92
|
+
from .strategy import Strategy
|
|
93
|
+
|
|
94
|
+
if not (isinstance(strategy, type) and issubclass(strategy, Strategy)):
|
|
95
|
+
raise TypeError('`strategy` must be a Strategy sub-type')
|
|
96
|
+
if not isinstance(data, pd.DataFrame):
|
|
97
|
+
raise TypeError("`data` must be a pandas.DataFrame with columns")
|
|
98
|
+
if not isinstance(spread, Number):
|
|
99
|
+
raise TypeError('`spread` must be a float value, percent of '
|
|
100
|
+
'entry order price')
|
|
101
|
+
if not isinstance(commission, (Number, tuple)) and not callable(commission):
|
|
102
|
+
raise TypeError('`commission` must be a float percent of order value, '
|
|
103
|
+
'a tuple of `(fixed, relative)` commission, '
|
|
104
|
+
'or a function that takes `(order_size, price)`'
|
|
105
|
+
'and returns commission dollar value')
|
|
106
|
+
|
|
107
|
+
data = data.copy(deep=False)
|
|
108
|
+
|
|
109
|
+
# インデックスをdatetimeインデックスに変換
|
|
110
|
+
if (not isinstance(data.index, pd.DatetimeIndex) and
|
|
111
|
+
not isinstance(data.index, pd.RangeIndex) and
|
|
112
|
+
# 大部分が大きな数値の数値インデックス
|
|
113
|
+
(data.index.is_numeric() and
|
|
114
|
+
(data.index > pd.Timestamp('1975').timestamp()).mean() > .8)):
|
|
115
|
+
try:
|
|
116
|
+
data.index = pd.to_datetime(data.index, infer_datetime_format=True)
|
|
117
|
+
except ValueError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
if 'Volume' not in data:
|
|
121
|
+
data['Volume'] = np.nan
|
|
122
|
+
|
|
123
|
+
if len(data) == 0:
|
|
124
|
+
raise ValueError('OHLC `data` is empty')
|
|
125
|
+
if len(data.columns.intersection({'Open', 'High', 'Low', 'Close', 'Volume'})) != 5:
|
|
126
|
+
raise ValueError("`data` must be a pandas.DataFrame with columns "
|
|
127
|
+
"'Open', 'High', 'Low', 'Close', and (optionally) 'Volume'")
|
|
128
|
+
if data[['Open', 'High', 'Low', 'Close']].isnull().values.any():
|
|
129
|
+
raise ValueError('Some OHLC values are missing (NaN). '
|
|
130
|
+
'Please strip those lines with `df.dropna()` or '
|
|
131
|
+
'fill them in with `df.interpolate()` or whatever.')
|
|
132
|
+
if np.any(data['Close'] > cash):
|
|
133
|
+
warnings.warn('Some prices are larger than initial cash value. Note that fractional '
|
|
134
|
+
'trading is not supported by this class. If you want to trade Bitcoin, '
|
|
135
|
+
'increase initial cash, or trade μBTC or satoshis instead (see e.g. class '
|
|
136
|
+
'`backtesting.lib.FractionalBacktest`.',
|
|
137
|
+
stacklevel=2)
|
|
138
|
+
if not data.index.is_monotonic_increasing:
|
|
139
|
+
warnings.warn('Data index is not sorted in ascending order. Sorting.',
|
|
140
|
+
stacklevel=2)
|
|
141
|
+
data = data.sort_index()
|
|
142
|
+
if not isinstance(data.index, pd.DatetimeIndex):
|
|
143
|
+
warnings.warn('Data index is not datetime. Assuming simple periods, '
|
|
144
|
+
'but `pd.DateTimeIndex` is advised.',
|
|
145
|
+
stacklevel=2)
|
|
146
|
+
|
|
147
|
+
self._data: pd.DataFrame = data
|
|
148
|
+
|
|
149
|
+
# partialとは、関数の一部の引数を事前に固定して、新しい関数を作成します。
|
|
150
|
+
# これにより、後で残りの引数だけを渡せば関数を実行できるようになります。
|
|
151
|
+
# 1. _Brokerクラスのコンストラクタの引数の一部(cash, spread, commissionなど)を事前に固定
|
|
152
|
+
# 2. 新しい関数(実際には呼び出し可能オブジェクト)を作成
|
|
153
|
+
# 3. 後で残りの引数(おそらくdataなど)を渡すだけで_Brokerのインスタンスを作成できるようにする
|
|
154
|
+
self._broker = partial(
|
|
155
|
+
_Broker, cash=cash, spread=spread, commission=commission, margin=margin,
|
|
156
|
+
trade_on_close=trade_on_close, hedging=hedging,
|
|
157
|
+
exclusive_orders=exclusive_orders, index=data.index,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
self._strategy = strategy
|
|
161
|
+
self._results: Optional[pd.Series] = None
|
|
162
|
+
self._finalize_trades = bool(finalize_trades)
|
|
163
|
+
|
|
164
|
+
def run(self) -> pd.Series:
|
|
165
|
+
"""
|
|
166
|
+
バックテストを実行します。結果と統計を含む `pd.Series` を返します。
|
|
167
|
+
|
|
168
|
+
キーワード引数は戦略パラメータとして解釈されます。
|
|
169
|
+
|
|
170
|
+
>>> Backtest(GOOG, SmaCross).run()
|
|
171
|
+
Start 2004-08-19 00:00:00
|
|
172
|
+
End 2013-03-01 00:00:00
|
|
173
|
+
Duration 3116 days 00:00:00
|
|
174
|
+
Exposure Time [%] 96.74115
|
|
175
|
+
Equity Final [$] 51422.99
|
|
176
|
+
Equity Peak [$] 75787.44
|
|
177
|
+
Return [%] 414.2299
|
|
178
|
+
Buy & Hold Return [%] 703.45824
|
|
179
|
+
Return (Ann.) [%] 21.18026
|
|
180
|
+
Volatility (Ann.) [%] 36.49391
|
|
181
|
+
CAGR [%] 14.15984
|
|
182
|
+
Sharpe Ratio 0.58038
|
|
183
|
+
Sortino Ratio 1.08479
|
|
184
|
+
Calmar Ratio 0.44144
|
|
185
|
+
Alpha [%] 394.37391
|
|
186
|
+
Beta 0.03803
|
|
187
|
+
Max. Drawdown [%] -47.98013
|
|
188
|
+
Avg. Drawdown [%] -5.92585
|
|
189
|
+
Max. Drawdown Duration 584 days 00:00:00
|
|
190
|
+
Avg. Drawdown Duration 41 days 00:00:00
|
|
191
|
+
# Trades 66
|
|
192
|
+
Win Rate [%] 46.9697
|
|
193
|
+
Best Trade [%] 53.59595
|
|
194
|
+
Worst Trade [%] -18.39887
|
|
195
|
+
Avg. Trade [%] 2.53172
|
|
196
|
+
Max. Trade Duration 183 days 00:00:00
|
|
197
|
+
Avg. Trade Duration 46 days 00:00:00
|
|
198
|
+
Profit Factor 2.16795
|
|
199
|
+
Expectancy [%] 3.27481
|
|
200
|
+
SQN 1.07662
|
|
201
|
+
Kelly Criterion 0.15187
|
|
202
|
+
_strategy SmaCross
|
|
203
|
+
_equity_curve Eq...
|
|
204
|
+
_trades Size EntryB...
|
|
205
|
+
dtype: object
|
|
206
|
+
|
|
207
|
+
.. warning::
|
|
208
|
+
異なる戦略パラメータに対して異なる結果が得られる場合があります。
|
|
209
|
+
例:50本と200本のSMAを使用する場合、取引シミュレーションは
|
|
210
|
+
201本目から開始されます。実際の遅延の長さは、最も遅延する
|
|
211
|
+
`Strategy.I`インジケーターのルックバック期間に等しくなります。
|
|
212
|
+
明らかに、これは結果に影響を与える可能性があります。
|
|
213
|
+
"""
|
|
214
|
+
# 循環インポートを避けるためにここでインポート
|
|
215
|
+
from .strategy import Strategy
|
|
216
|
+
|
|
217
|
+
data = self._data.copy(deep=False)
|
|
218
|
+
broker: _Broker = self._broker(data=data)
|
|
219
|
+
strategy: Strategy = self._strategy(broker, data)
|
|
220
|
+
|
|
221
|
+
strategy.init()
|
|
222
|
+
|
|
223
|
+
# インジケーターがまだ「ウォームアップ」中の最初の数本のキャンドルをスキップ
|
|
224
|
+
# 少なくとも2つのエントリが利用可能になるように+1
|
|
225
|
+
start = 1
|
|
226
|
+
|
|
227
|
+
# "invalid value encountered in ..."警告を無効化。比較
|
|
228
|
+
# np.nan >= 3は無効ではない;Falseです。
|
|
229
|
+
with np.errstate(invalid='ignore'):
|
|
230
|
+
|
|
231
|
+
for i in range(start, len(self._data)):
|
|
232
|
+
# 注文処理とブローカー関連の処理
|
|
233
|
+
data = self._data.iloc[:i]
|
|
234
|
+
try:
|
|
235
|
+
broker._data = data
|
|
236
|
+
broker.next()
|
|
237
|
+
except:
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
# 次のティック、バークローズ直前
|
|
241
|
+
strategy._data = data
|
|
242
|
+
strategy.next()
|
|
243
|
+
else:
|
|
244
|
+
if self._finalize_trades is True:
|
|
245
|
+
# 統計を生成するために残っているオープン取引をクローズ
|
|
246
|
+
for trade in reversed(broker.trades):
|
|
247
|
+
trade.close()
|
|
248
|
+
|
|
249
|
+
# HACK: 最後の戦略イテレーションで配置されたクローズ注文を処理するために
|
|
250
|
+
# ブローカーを最後にもう一度実行。最後のブローカーイテレーションと同じOHLC値を使用。
|
|
251
|
+
if start < len(self._data):
|
|
252
|
+
broker.next()
|
|
253
|
+
elif len(broker.trades):
|
|
254
|
+
warnings.warn(
|
|
255
|
+
'バックテスト終了時に一部の取引がオープンのままです。'
|
|
256
|
+
'`Backtest(..., finalize_trades=True)`を使用してクローズし、'
|
|
257
|
+
'統計に含めてください。', stacklevel=2)
|
|
258
|
+
|
|
259
|
+
equity = pd.Series(broker._equity).bfill().fillna(broker._cash).values
|
|
260
|
+
self._results = compute_stats(
|
|
261
|
+
trades=broker.closed_trades,
|
|
262
|
+
equity=equity,
|
|
263
|
+
ohlc_data=self._data,
|
|
264
|
+
strategy_instance=strategy,
|
|
265
|
+
risk_free_rate=0.0,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return self._results
|
|
269
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Data reader for fetching stock price data from API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import requests
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from typing import Union
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
# Load environment variables from .env file in project root
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
BACKCASTPRO_API_URL= 'http://backcastpro.i234.me'
|
|
14
|
+
|
|
15
|
+
def DataReader(code: str,
|
|
16
|
+
start_date: Union[str, datetime, None] = None,
|
|
17
|
+
end_date: Union[str, datetime, None] = None) -> pd.DataFrame:
|
|
18
|
+
"""
|
|
19
|
+
Fetch stock price data from API.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
code (str): Stock code (e.g., '7203' for Toyota)
|
|
23
|
+
start_date (Union[str, datetime, None], optional): Start date for data retrieval.
|
|
24
|
+
If None, defaults to 1 year ago.
|
|
25
|
+
end_date (Union[str, datetime, None], optional): End date for data retrieval.
|
|
26
|
+
If None, defaults to today.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
pd.DataFrame: Stock price data with columns like 'Open', 'High', 'Low', 'Close', 'Volume'
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
requests.RequestException: If API request fails
|
|
33
|
+
ValueError: If dates are invalid or API returns error
|
|
34
|
+
"""
|
|
35
|
+
# Set default dates if not provided
|
|
36
|
+
if end_date is None:
|
|
37
|
+
end_date = datetime.now()
|
|
38
|
+
if start_date is None:
|
|
39
|
+
start_date = end_date - timedelta(days=365)
|
|
40
|
+
|
|
41
|
+
# Convert datetime objects to string format if needed
|
|
42
|
+
if isinstance(start_date, datetime):
|
|
43
|
+
start_date_str = start_date.strftime('%Y-%m-%d')
|
|
44
|
+
else:
|
|
45
|
+
start_date_str = str(start_date)
|
|
46
|
+
|
|
47
|
+
if isinstance(end_date, datetime):
|
|
48
|
+
end_date_str = end_date.strftime('%Y-%m-%d')
|
|
49
|
+
else:
|
|
50
|
+
end_date_str = str(end_date)
|
|
51
|
+
|
|
52
|
+
# Construct API URL
|
|
53
|
+
base_url = os.getenv('BACKCASTPRO_API_URL')
|
|
54
|
+
if not base_url:
|
|
55
|
+
base_url = BACKCASTPRO_API_URL
|
|
56
|
+
|
|
57
|
+
# Ensure base_url doesn't end with slash and path starts with slash
|
|
58
|
+
base_url = base_url.rstrip('/')
|
|
59
|
+
url = f"{base_url}/api/stocks/price?code={code}&start_date={start_date_str}&end_date={end_date_str}"
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Make API request
|
|
63
|
+
response = requests.get(url, timeout=30)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
|
|
66
|
+
# Parse JSON response
|
|
67
|
+
data = response.json()
|
|
68
|
+
|
|
69
|
+
# Convert to DataFrame
|
|
70
|
+
if isinstance(data, dict):
|
|
71
|
+
if 'price_data' in data:
|
|
72
|
+
df = pd.DataFrame(data['price_data'])
|
|
73
|
+
elif 'data' in data:
|
|
74
|
+
df = pd.DataFrame(data['data'])
|
|
75
|
+
elif 'prices' in data:
|
|
76
|
+
df = pd.DataFrame(data['prices'])
|
|
77
|
+
elif 'results' in data:
|
|
78
|
+
df = pd.DataFrame(data['results'])
|
|
79
|
+
else:
|
|
80
|
+
# If it's a single dict, wrap it in a list
|
|
81
|
+
df = pd.DataFrame([data])
|
|
82
|
+
elif isinstance(data, list):
|
|
83
|
+
# If response is directly a list
|
|
84
|
+
df = pd.DataFrame(data)
|
|
85
|
+
else:
|
|
86
|
+
raise ValueError(f"Unexpected response format: {type(data)}")
|
|
87
|
+
|
|
88
|
+
# Ensure proper datetime index
|
|
89
|
+
if 'Date' in df.columns:
|
|
90
|
+
df['Date'] = pd.to_datetime(df['Date'])
|
|
91
|
+
df.set_index('Date', inplace=True)
|
|
92
|
+
elif 'date' in df.columns:
|
|
93
|
+
df['Date'] = pd.to_datetime(df['date'])
|
|
94
|
+
df.set_index('Date', inplace=True)
|
|
95
|
+
elif df.index.name is None or df.index.name == 'index':
|
|
96
|
+
# If no date column, try to parse index as datetime
|
|
97
|
+
try:
|
|
98
|
+
df.index = pd.to_datetime(df.index)
|
|
99
|
+
except:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
# Ensure numeric columns are properly typed
|
|
103
|
+
numeric_columns = ['Open', 'High', 'Low', 'Close', 'Volume']
|
|
104
|
+
for col in numeric_columns:
|
|
105
|
+
if col in df.columns:
|
|
106
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
107
|
+
|
|
108
|
+
return df
|
|
109
|
+
|
|
110
|
+
except requests.exceptions.RequestException as e:
|
|
111
|
+
raise requests.RequestException(f"Failed to fetch data from API: {e}")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise ValueError(f"Error processing API response: {e}")
|
|
114
|
+
|
|
115
|
+
def JapanStocks() -> pd.DataFrame:
|
|
116
|
+
"""
|
|
117
|
+
日本株の銘柄リストを取得
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
pd.DataFrame: 日本株の銘柄リスト(コード、名前、市場、セクター等)
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
requests.RequestException: If API request fails
|
|
124
|
+
ValueError: If API returns error
|
|
125
|
+
"""
|
|
126
|
+
# Construct API URL
|
|
127
|
+
base_url = os.getenv('BACKCASTPRO_API_URL')
|
|
128
|
+
if not base_url:
|
|
129
|
+
base_url = BACKCASTPRO_API_URL
|
|
130
|
+
|
|
131
|
+
# Ensure base_url doesn't end with slash and path starts with slash
|
|
132
|
+
base_url = base_url.rstrip('/')
|
|
133
|
+
url = f"{base_url}/api/stocks"
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Make API request
|
|
137
|
+
response = requests.get(url, timeout=30)
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
|
|
140
|
+
# Parse JSON response
|
|
141
|
+
data = response.json()
|
|
142
|
+
|
|
143
|
+
# Convert to DataFrame
|
|
144
|
+
if isinstance(data, dict) and 'data' in data:
|
|
145
|
+
df = pd.DataFrame(data['data'])
|
|
146
|
+
elif isinstance(data, list):
|
|
147
|
+
df = pd.DataFrame(data)
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError(f"Unexpected response format: {type(data)}")
|
|
150
|
+
|
|
151
|
+
# Ensure proper column names and types
|
|
152
|
+
if 'code' in df.columns:
|
|
153
|
+
df['code'] = df['code'].astype(str)
|
|
154
|
+
if 'name' in df.columns:
|
|
155
|
+
df['name'] = df['name'].astype(str)
|
|
156
|
+
if 'market' in df.columns:
|
|
157
|
+
df['market'] = df['market'].astype(str)
|
|
158
|
+
if 'sector' in df.columns:
|
|
159
|
+
df['sector'] = df['sector'].astype(str)
|
|
160
|
+
if 'currency' in df.columns:
|
|
161
|
+
df['currency'] = df['currency'].astype(str)
|
|
162
|
+
|
|
163
|
+
return df
|
|
164
|
+
|
|
165
|
+
except requests.exceptions.RequestException as e:
|
|
166
|
+
raise requests.RequestException(f"Failed to fetch data from API: {e}")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
raise ValueError(f"Error processing API response: {e}")
|
BackcastPro/order.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
注文管理モジュール。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ._broker import _Broker
|
|
9
|
+
from .trade import Trade
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Order:
|
|
13
|
+
"""
|
|
14
|
+
`Strategy.buy()`と`Strategy.sell()`を通じて新しい注文を出します。
|
|
15
|
+
`Strategy.orders`を通じて既存の注文を照会します。
|
|
16
|
+
|
|
17
|
+
注文が実行または[約定]されると、`Trade`が発生します。
|
|
18
|
+
|
|
19
|
+
出されたがまだ約定されていない注文の側面を変更したい場合は、
|
|
20
|
+
キャンセルして新しい注文を出してください。
|
|
21
|
+
|
|
22
|
+
すべての出された注文は[取消注文まで有効]です。
|
|
23
|
+
|
|
24
|
+
[filled]: https://www.investopedia.com/terms/f/fill.asp
|
|
25
|
+
[Good 'Til Canceled]: https://www.investopedia.com/terms/g/gtc.asp
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, broker: '_Broker',
|
|
28
|
+
size: float,
|
|
29
|
+
limit_price: Optional[float] = None,
|
|
30
|
+
stop_price: Optional[float] = None,
|
|
31
|
+
sl_price: Optional[float] = None,
|
|
32
|
+
tp_price: Optional[float] = None,
|
|
33
|
+
parent_trade: Optional['Trade'] = None,
|
|
34
|
+
tag: object = None):
|
|
35
|
+
self.__broker = broker
|
|
36
|
+
assert size != 0
|
|
37
|
+
self.__size = size
|
|
38
|
+
self.__limit_price = limit_price
|
|
39
|
+
self.__stop_price = stop_price
|
|
40
|
+
self.__sl_price = sl_price
|
|
41
|
+
self.__tp_price = tp_price
|
|
42
|
+
self.__parent_trade = parent_trade
|
|
43
|
+
self.__tag = tag
|
|
44
|
+
|
|
45
|
+
def _replace(self, **kwargs):
|
|
46
|
+
for k, v in kwargs.items():
|
|
47
|
+
setattr(self, f'_{self.__class__.__qualname__}__{k}', v)
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cancel(self):
|
|
52
|
+
"""注文をキャンセルします。"""
|
|
53
|
+
self.__broker.orders.remove(self)
|
|
54
|
+
trade = self.__parent_trade
|
|
55
|
+
if trade:
|
|
56
|
+
if self is trade._sl_order:
|
|
57
|
+
trade._replace(sl_order=None)
|
|
58
|
+
elif self is trade._tp_order:
|
|
59
|
+
trade._replace(tp_order=None)
|
|
60
|
+
else:
|
|
61
|
+
pass # Order placed by Trade.close()
|
|
62
|
+
|
|
63
|
+
# Fields getters
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def size(self) -> float:
|
|
67
|
+
"""
|
|
68
|
+
注文サイズ(ショート注文の場合は負の値)。
|
|
69
|
+
|
|
70
|
+
サイズが0と1の間の値の場合、現在利用可能な流動性(現金 + `Position.pl` - 使用済みマージン)の
|
|
71
|
+
割合として解釈されます。
|
|
72
|
+
1以上の値は絶対的なユニット数を示します。
|
|
73
|
+
"""
|
|
74
|
+
return self.__size
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def limit(self) -> Optional[float]:
|
|
78
|
+
"""
|
|
79
|
+
[指値注文]の注文指値価格、または[成行注文]の場合はNone(次に利用可能な価格で約定)。
|
|
80
|
+
|
|
81
|
+
[limit orders]: https://www.investopedia.com/terms/l/limitorder.asp
|
|
82
|
+
[market orders]: https://www.investopedia.com/terms/m/marketorder.asp
|
|
83
|
+
"""
|
|
84
|
+
return self.__limit_price
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def stop(self) -> Optional[float]:
|
|
88
|
+
"""
|
|
89
|
+
[ストップリミット/ストップ成行]注文の注文ストップ価格。
|
|
90
|
+
ストップが設定されていない場合、またはストップ価格が既にヒットした場合はNone。
|
|
91
|
+
|
|
92
|
+
[_]: https://www.investopedia.com/terms/s/stoporder.asp
|
|
93
|
+
"""
|
|
94
|
+
return self.__stop_price
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def sl(self) -> Optional[float]:
|
|
98
|
+
"""
|
|
99
|
+
ストップロス価格。設定されている場合、この注文の実行後に`Trade`に対して
|
|
100
|
+
新しい条件付きストップ成行注文が配置されます。
|
|
101
|
+
`Trade.sl`も参照してください。
|
|
102
|
+
"""
|
|
103
|
+
return self.__sl_price
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def tp(self) -> Optional[float]:
|
|
107
|
+
"""
|
|
108
|
+
テイクプロフィット価格。設定されている場合、この注文の実行後に`Trade`に対して
|
|
109
|
+
新しい条件付き指値注文が配置されます。
|
|
110
|
+
`Trade.tp`も参照してください。
|
|
111
|
+
"""
|
|
112
|
+
return self.__tp_price
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def parent_trade(self):
|
|
116
|
+
return self.__parent_trade
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def tag(self):
|
|
120
|
+
"""
|
|
121
|
+
任意の値(文字列など)。設定されている場合、この注文と関連する`Trade`の
|
|
122
|
+
追跡が可能になります(`Trade.tag`を参照)。
|
|
123
|
+
"""
|
|
124
|
+
return self.__tag
|
|
125
|
+
|
|
126
|
+
__pdoc__ = {'Order.parent_trade': False}
|
|
127
|
+
|
|
128
|
+
# Extra properties
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def is_long(self):
|
|
132
|
+
"""注文がロングの場合(注文サイズが正)にTrueを返します。"""
|
|
133
|
+
return self.__size > 0
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def is_short(self):
|
|
137
|
+
"""注文がショートの場合(注文サイズが負)にTrueを返します。"""
|
|
138
|
+
return self.__size < 0
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def is_contingent(self):
|
|
142
|
+
"""
|
|
143
|
+
[条件付き]注文、つまりアクティブな取引に配置された[OCO]ストップロスおよび
|
|
144
|
+
テイクプロフィットブラケット注文の場合にTrueを返します。
|
|
145
|
+
親`Trade`がクローズされると、残りの条件付き注文はキャンセルされます。
|
|
146
|
+
|
|
147
|
+
`Trade.sl`と`Trade.tp`を通じて条件付き注文を変更できます。
|
|
148
|
+
|
|
149
|
+
[contingent]: https://www.investopedia.com/terms/c/contingentorder.asp
|
|
150
|
+
[OCO]: https://www.investopedia.com/terms/o/oco.asp
|
|
151
|
+
"""
|
|
152
|
+
return bool((parent := self.__parent_trade) and
|
|
153
|
+
(self is parent._sl_order or
|
|
154
|
+
self is parent._tp_order))
|
BackcastPro/position.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ポジション管理モジュール。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ._broker import _Broker
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Position:
|
|
12
|
+
"""
|
|
13
|
+
現在保有している資産ポジション。
|
|
14
|
+
`backtesting.backtesting.Strategy.next`内で
|
|
15
|
+
`backtesting.backtesting.Strategy.position`として利用可能です。
|
|
16
|
+
ブール値コンテキストで使用できます。例:
|
|
17
|
+
|
|
18
|
+
if self.position:
|
|
19
|
+
... # ポジションがあります(ロングまたはショート)
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, broker: '_Broker'):
|
|
22
|
+
self.__broker = broker
|
|
23
|
+
|
|
24
|
+
def __bool__(self):
|
|
25
|
+
return self.size != 0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def size(self) -> float:
|
|
29
|
+
"""資産単位でのポジションサイズ。ショートポジションの場合は負の値。"""
|
|
30
|
+
return sum(trade.size for trade in self.__broker.trades)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def pl(self) -> float:
|
|
34
|
+
"""現在のポジションの利益(正)または損失(負)を現金単位で。"""
|
|
35
|
+
return sum(trade.pl for trade in self.__broker.trades)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def pl_pct(self) -> float:
|
|
39
|
+
"""現在のポジションの利益(正)または損失(負)をパーセントで。"""
|
|
40
|
+
total_invested = sum(trade.entry_price * abs(trade.size) for trade in self.__broker.trades)
|
|
41
|
+
return (self.pl / total_invested) * 100 if total_invested else 0
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_long(self) -> bool:
|
|
45
|
+
"""ポジションがロング(ポジションサイズが正)の場合True。"""
|
|
46
|
+
return self.size > 0
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def is_short(self) -> bool:
|
|
50
|
+
"""ポジションがショート(ポジションサイズが負)の場合True。"""
|
|
51
|
+
return self.size < 0
|
|
52
|
+
|
|
53
|
+
def close(self, portion: float = 1.):
|
|
54
|
+
"""
|
|
55
|
+
各アクティブな取引の「一部」を決済することで、ポジションの一部を決済します。詳細は「Trade.close」を参照してください。
|
|
56
|
+
"""
|
|
57
|
+
for trade in self.__broker.trades:
|
|
58
|
+
trade.close(portion)
|
|
59
|
+
|
|
60
|
+
def __repr__(self):
|
|
61
|
+
return f'<Position: {self.size} ({len(self.__broker.trades)} trades)>'
|