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.

@@ -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,7 @@
1
+ """Data and utilities for testing."""
2
+
3
+ from .datareader import DataReader, JapanStocks
4
+
5
+
6
+ TOYOTA = DataReader('72030')
7
+
@@ -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))
@@ -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)>'