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.

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