BackcastPro 0.0.2__tar.gz → 0.0.4__tar.gz
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-0.0.4/PKG-INFO +69 -0
- backcastpro-0.0.4/README.md +55 -0
- {backcastpro-0.0.2 → backcastpro-0.0.4}/pyproject.toml +4 -4
- backcastpro-0.0.4/src/BackcastPro/__init__.py +15 -0
- backcastpro-0.0.4/src/BackcastPro/_broker.py +415 -0
- {backcastpro-0.0.2 → backcastpro-0.0.4}/src/BackcastPro/_stats.py +169 -212
- backcastpro-0.0.4/src/BackcastPro/backtest.py +293 -0
- backcastpro-0.0.4/src/BackcastPro/data/JapanStock.py +171 -0
- backcastpro-0.0.4/src/BackcastPro/data/__init__.py +7 -0
- backcastpro-0.0.4/src/BackcastPro/order.py +151 -0
- backcastpro-0.0.4/src/BackcastPro/position.py +61 -0
- backcastpro-0.0.4/src/BackcastPro/strategy.py +174 -0
- backcastpro-0.0.4/src/BackcastPro/trade.py +195 -0
- backcastpro-0.0.4/src/BackcastPro.egg-info/PKG-INFO +69 -0
- {backcastpro-0.0.2 → backcastpro-0.0.4}/src/BackcastPro.egg-info/SOURCES.txt +8 -7
- backcastpro-0.0.2/PKG-INFO +0 -53
- backcastpro-0.0.2/README.md +0 -39
- backcastpro-0.0.2/src/BackcastPro/__init__.py +0 -90
- backcastpro-0.0.2/src/BackcastPro/_plotting.py +0 -785
- backcastpro-0.0.2/src/BackcastPro/_util.py +0 -337
- backcastpro-0.0.2/src/BackcastPro/backtesting.py +0 -1763
- backcastpro-0.0.2/src/BackcastPro/lib.py +0 -646
- backcastpro-0.0.2/src/BackcastPro/test/__init__.py +0 -29
- backcastpro-0.0.2/src/BackcastPro/test/__main__.py +0 -7
- backcastpro-0.0.2/src/BackcastPro/test/_test.py +0 -1174
- backcastpro-0.0.2/src/BackcastPro.egg-info/PKG-INFO +0 -53
- {backcastpro-0.0.2 → backcastpro-0.0.4}/setup.cfg +0 -0
- {backcastpro-0.0.2 → backcastpro-0.0.4}/src/BackcastPro.egg-info/dependency_links.txt +0 -0
- {backcastpro-0.0.2 → backcastpro-0.0.4}/src/BackcastPro.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: BackcastPro
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: トレーディング戦略のためのPythonバックテストライブラリ
|
|
5
|
+
Author-email: botterYosuke <yosuke.sasazawa@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://BackcastPro.github.io/BackcastPro/
|
|
7
|
+
Project-URL: Issues, https://github.com/BackcastPro/BackcastPro/issues
|
|
8
|
+
Project-URL: Logo, https://raw.githubusercontent.com/BackcastPro/BackcastPro/main/docs/img/logo.drawio.svg
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# <img src="docs/img/logo.drawio.svg" alt="BackcastPro Logo" width="40" height="24"> BackcastPro
|
|
16
|
+
|
|
17
|
+
トレーディング戦略のためのPythonバックテストライブラリ。
|
|
18
|
+
|
|
19
|
+
## インストール(Windows)
|
|
20
|
+
|
|
21
|
+
### PyPIから(エンドユーザー向け)
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
py -m pip install BackcastPro
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 開発用インストール
|
|
28
|
+
|
|
29
|
+
開発用に、リポジトリをクローンして開発モードでインストールします。
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
git clone <repository-url>
|
|
33
|
+
cd BackcastPro
|
|
34
|
+
py -m venv .venv
|
|
35
|
+
.\.venv\Scripts\Activate.ps1
|
|
36
|
+
py -m pip install -e .
|
|
37
|
+
py -m pip install -r requirements.txt
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**開発モードインストール(py -m pip install -e .)**
|
|
41
|
+
- プロジェクトを開発モードでインストールします
|
|
42
|
+
- `src` ディレクトリが自動的に Python パスに追加されます
|
|
43
|
+
|
|
44
|
+
## 使用方法
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from BackcastPro import Strategy, Backtest
|
|
48
|
+
from BackcastPro.data import DataReader, JapanStocks
|
|
49
|
+
|
|
50
|
+
# ここにトレーディング戦略の実装を記述
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## ドキュメント
|
|
54
|
+
|
|
55
|
+
- [ドキュメント一覧](./docs/index.md)
|
|
56
|
+
- [チュートリアル](./docs/tutorial.md)
|
|
57
|
+
- [APIリファレンス](./docs/api-reference.md)
|
|
58
|
+
- [高度な使い方](./docs/advanced-usage.md)
|
|
59
|
+
- [トラブルシューティング](./docs/troubleshooting.md)
|
|
60
|
+
- [開発者ガイド](./docs/developer-guide.md)
|
|
61
|
+
- [PyPIへのデプロイ方法](./docs/how-to-deploy-to-PyPI.md)
|
|
62
|
+
- [サンプル](./docs/examples/)
|
|
63
|
+
|
|
64
|
+
## バグ報告 / サポート
|
|
65
|
+
|
|
66
|
+
- バグ報告や要望は GitHub Issues へ
|
|
67
|
+
- 質問は Discord コミュニティへ([招待リンク](https://discord.gg/fzJTbpzE))
|
|
68
|
+
- 使い方はドキュメントをご参照ください
|
|
69
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# <img src="docs/img/logo.drawio.svg" alt="BackcastPro Logo" width="40" height="24"> BackcastPro
|
|
2
|
+
|
|
3
|
+
トレーディング戦略のためのPythonバックテストライブラリ。
|
|
4
|
+
|
|
5
|
+
## インストール(Windows)
|
|
6
|
+
|
|
7
|
+
### PyPIから(エンドユーザー向け)
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
py -m pip install BackcastPro
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 開発用インストール
|
|
14
|
+
|
|
15
|
+
開発用に、リポジトリをクローンして開発モードでインストールします。
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
git clone <repository-url>
|
|
19
|
+
cd BackcastPro
|
|
20
|
+
py -m venv .venv
|
|
21
|
+
.\.venv\Scripts\Activate.ps1
|
|
22
|
+
py -m pip install -e .
|
|
23
|
+
py -m pip install -r requirements.txt
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**開発モードインストール(py -m pip install -e .)**
|
|
27
|
+
- プロジェクトを開発モードでインストールします
|
|
28
|
+
- `src` ディレクトリが自動的に Python パスに追加されます
|
|
29
|
+
|
|
30
|
+
## 使用方法
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from BackcastPro import Strategy, Backtest
|
|
34
|
+
from BackcastPro.data import DataReader, JapanStocks
|
|
35
|
+
|
|
36
|
+
# ここにトレーディング戦略の実装を記述
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## ドキュメント
|
|
40
|
+
|
|
41
|
+
- [ドキュメント一覧](./docs/index.md)
|
|
42
|
+
- [チュートリアル](./docs/tutorial.md)
|
|
43
|
+
- [APIリファレンス](./docs/api-reference.md)
|
|
44
|
+
- [高度な使い方](./docs/advanced-usage.md)
|
|
45
|
+
- [トラブルシューティング](./docs/troubleshooting.md)
|
|
46
|
+
- [開発者ガイド](./docs/developer-guide.md)
|
|
47
|
+
- [PyPIへのデプロイ方法](./docs/how-to-deploy-to-PyPI.md)
|
|
48
|
+
- [サンプル](./docs/examples/)
|
|
49
|
+
|
|
50
|
+
## バグ報告 / サポート
|
|
51
|
+
|
|
52
|
+
- バグ報告や要望は GitHub Issues へ
|
|
53
|
+
- 質問は Discord コミュニティへ([招待リンク](https://discord.gg/fzJTbpzE))
|
|
54
|
+
- 使い方はドキュメントをご参照ください
|
|
55
|
+
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "BackcastPro"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.4"
|
|
4
4
|
authors = [
|
|
5
|
-
{ name="
|
|
5
|
+
{ name="botterYosuke", email="yosuke.sasazawa@gmail.com" },
|
|
6
6
|
]
|
|
7
|
-
description = "
|
|
7
|
+
description = "トレーディング戦略のためのPythonバックテストライブラリ"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
classifiers = [
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
[project.urls]
|
|
17
17
|
Homepage = "https://BackcastPro.github.io/BackcastPro/"
|
|
18
18
|
Issues = "https://github.com/BackcastPro/BackcastPro/issues"
|
|
19
|
-
Logo = "https://raw.githubusercontent.com/BackcastPro/BackcastPro/main/docs/img/
|
|
19
|
+
Logo = "https://raw.githubusercontent.com/BackcastPro/BackcastPro/main/docs/img/logo.drawio.svg"
|
|
20
20
|
|
|
21
21
|
[tool.setuptools]
|
|
22
22
|
include-package-data = true
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BackcastPro をご利用いただきありがとうございます。
|
|
3
|
+
|
|
4
|
+
インストール後のご案内(インストール済みユーザー向け)
|
|
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
|
|
11
|
+
|
|
12
|
+
※ 使い始めはチュートリアル → 詳細はAPIリファレンスをご参照ください。
|
|
13
|
+
"""
|
|
14
|
+
from .backtest import Backtest
|
|
15
|
+
from .strategy import Strategy
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ブローカー管理モジュール。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from math import copysign
|
|
7
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from .order import Order
|
|
13
|
+
from .position import Position
|
|
14
|
+
from .trade import Trade
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Broker:
|
|
21
|
+
"""
|
|
22
|
+
バックテストにおける証券取引の実行、注文管理、ポジション管理、損益計算を担当します。
|
|
23
|
+
実際の証券会社のブローカー機能をシミュレートし、リアルな取引環境を提供します。
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
data : pd.DataFrame
|
|
28
|
+
取引対象の価格データ。Open, High, Low, Closeの列を持つ必要があります。
|
|
29
|
+
cash : float
|
|
30
|
+
初期現金残高。正の値である必要があります。
|
|
31
|
+
spread : float
|
|
32
|
+
ビッドアスクスプレッド(買値と売値の差)。取引コストとして使用されます。
|
|
33
|
+
commission : float or tuple or callable
|
|
34
|
+
手数料の設定方法:
|
|
35
|
+
- float: 相対手数料(例: 0.001 = 0.1%)
|
|
36
|
+
- tuple: (固定手数料, 相対手数料) の組み合わせ
|
|
37
|
+
- callable: カスタム手数料計算関数 (size, price) -> 手数料
|
|
38
|
+
margin : float
|
|
39
|
+
必要証拠金率(0 < margin <= 1)。レバレッジ = 1/margin として計算されます。
|
|
40
|
+
trade_on_close : bool
|
|
41
|
+
取引を終値で実行するかどうか。Trueの場合、次の始値ではなく現在の終値で取引します。
|
|
42
|
+
hedging : bool
|
|
43
|
+
ヘッジングモードの有効化。Trueの場合、反対方向のポジションを同時に保有できます。
|
|
44
|
+
exclusive_orders : bool
|
|
45
|
+
排他的注文モード。Trueの場合、新しい注文が前のポジションを自動的にクローズします。
|
|
46
|
+
index : pd.Index
|
|
47
|
+
時系列データのインデックス。エクイティカーブの記録に使用されます。
|
|
48
|
+
"""
|
|
49
|
+
# Tips:
|
|
50
|
+
# 関数定義における`*`の意味
|
|
51
|
+
# - `*`以降の引数は、必ずキーワード引数として渡す必要がある
|
|
52
|
+
# - 位置引数として渡すことはできない
|
|
53
|
+
# なぜキーワード専用引数を使うのか?
|
|
54
|
+
# 1. APIの明確性: 引数の意味が明確になる
|
|
55
|
+
# 2. 保守性: 引数の順序を変更しても既存のコードが壊れない
|
|
56
|
+
# 3. 可読性: 関数呼び出し時に何を渡しているかが分かりやすい
|
|
57
|
+
def __init__(self, *, data, cash, spread, commission, margin,
|
|
58
|
+
trade_on_close, hedging, exclusive_orders, index):
|
|
59
|
+
assert cash > 0, f"cash should be > 0, is {cash}"
|
|
60
|
+
assert 0 < margin <= 1, f"margin should be between 0 and 1, is {margin}"
|
|
61
|
+
self._data: pd.DataFrame = data
|
|
62
|
+
self._cash = cash
|
|
63
|
+
|
|
64
|
+
# 手数料の登録
|
|
65
|
+
if callable(commission):
|
|
66
|
+
# 関数`commission`が呼び出し可能な場合
|
|
67
|
+
self._commission = commission
|
|
68
|
+
else:
|
|
69
|
+
try:
|
|
70
|
+
self._commission_fixed, self._commission_relative = commission
|
|
71
|
+
except TypeError:
|
|
72
|
+
self._commission_fixed, self._commission_relative = 0, commission
|
|
73
|
+
assert self._commission_fixed >= 0, 'Need fixed cash commission in $ >= 0'
|
|
74
|
+
assert -.1 <= self._commission_relative < .1, \
|
|
75
|
+
("commission should be between -10% "
|
|
76
|
+
f"(e.g. market-maker's rebates) and 10% (fees), is {self._commission_relative}")
|
|
77
|
+
self._commission = self._commission_func
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
self._spread = spread
|
|
81
|
+
self._leverage = 1 / margin
|
|
82
|
+
self._trade_on_close = trade_on_close
|
|
83
|
+
self._hedging = hedging
|
|
84
|
+
self._exclusive_orders = exclusive_orders
|
|
85
|
+
|
|
86
|
+
self._equity = np.tile(np.nan, len(index))
|
|
87
|
+
self.orders: List[Order] = []
|
|
88
|
+
self.trades: List[Trade] = []
|
|
89
|
+
self.position = Position(self)
|
|
90
|
+
self.closed_trades: List[Trade] = []
|
|
91
|
+
|
|
92
|
+
def _commission_func(self, order_size, price):
|
|
93
|
+
return self._commission_fixed + abs(order_size) * price * self._commission_relative
|
|
94
|
+
|
|
95
|
+
def __repr__(self):
|
|
96
|
+
return f'<Broker: {self._cash:.0f}{self.position.pl:+.1f} ({len(self.trades)} trades)>'
|
|
97
|
+
|
|
98
|
+
def new_order(self,
|
|
99
|
+
size: float,
|
|
100
|
+
limit: Optional[float] = None,
|
|
101
|
+
stop: Optional[float] = None,
|
|
102
|
+
sl: Optional[float] = None,
|
|
103
|
+
tp: Optional[float] = None,
|
|
104
|
+
tag: object = None,
|
|
105
|
+
*,
|
|
106
|
+
trade: Optional[Trade] = None) -> Order:
|
|
107
|
+
"""
|
|
108
|
+
Argument size indicates whether the order is long or short
|
|
109
|
+
"""
|
|
110
|
+
size = float(size)
|
|
111
|
+
stop = stop and float(stop)
|
|
112
|
+
limit = limit and float(limit)
|
|
113
|
+
sl = sl and float(sl)
|
|
114
|
+
tp = tp and float(tp)
|
|
115
|
+
|
|
116
|
+
is_long = size > 0
|
|
117
|
+
assert size != 0, size
|
|
118
|
+
adjusted_price = self._adjusted_price(size)
|
|
119
|
+
|
|
120
|
+
if is_long:
|
|
121
|
+
if not (sl or -np.inf) < (limit or stop or adjusted_price) < (tp or np.inf):
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"Long orders require: "
|
|
124
|
+
f"SL ({sl}) < LIMIT ({limit or stop or adjusted_price}) < TP ({tp})")
|
|
125
|
+
else:
|
|
126
|
+
if not (tp or -np.inf) < (limit or stop or adjusted_price) < (sl or np.inf):
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"Short orders require: "
|
|
129
|
+
f"TP ({tp}) < LIMIT ({limit or stop or adjusted_price}) < SL ({sl})")
|
|
130
|
+
|
|
131
|
+
order = Order(self, size, limit, stop, sl, tp, trade, tag)
|
|
132
|
+
|
|
133
|
+
if not trade:
|
|
134
|
+
# 排他的注文(各新しい注文が前の注文/ポジションを自動クローズ)の場合、
|
|
135
|
+
# 事前にすべての非条件付き注文をキャンセルし、すべてのオープン取引をクローズ
|
|
136
|
+
if self._exclusive_orders:
|
|
137
|
+
for o in self.orders:
|
|
138
|
+
if not o.is_contingent:
|
|
139
|
+
o.cancel()
|
|
140
|
+
for t in self.trades:
|
|
141
|
+
t.close()
|
|
142
|
+
|
|
143
|
+
# 新しい注文を注文キューに配置、SL注文が最初に処理されるようにする
|
|
144
|
+
self.orders.insert(0 if trade and stop else len(self.orders), order)
|
|
145
|
+
|
|
146
|
+
return order
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def last_price(self) -> float:
|
|
150
|
+
""" Price at the last (current) close. """
|
|
151
|
+
return self._data.Close.iloc[-1]
|
|
152
|
+
|
|
153
|
+
def _adjusted_price(self, size=None, price=None) -> float:
|
|
154
|
+
"""
|
|
155
|
+
Long/short `price`, adjusted for spread.
|
|
156
|
+
In long positions, the adjusted price is a fraction higher, and vice versa.
|
|
157
|
+
"""
|
|
158
|
+
return (price or self.last_price) * (1 + copysign(self._spread, size))
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def equity(self) -> float:
|
|
162
|
+
return self._cash + sum(trade.pl for trade in self.trades)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def margin_available(self) -> float:
|
|
166
|
+
# https://github.com/QuantConnect/Lean/pull/3768 から
|
|
167
|
+
margin_used = sum(trade.value / self._leverage for trade in self.trades)
|
|
168
|
+
return max(0, self.equity - margin_used)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def cash(self):
|
|
172
|
+
return self._cash
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def commission(self):
|
|
176
|
+
return self._commission
|
|
177
|
+
|
|
178
|
+
def next(self):
|
|
179
|
+
i = self._i = len(self._data) - 1
|
|
180
|
+
self._process_orders()
|
|
181
|
+
|
|
182
|
+
# エクイティカーブ用にアカウントエクイティを記録
|
|
183
|
+
equity = self.equity
|
|
184
|
+
self._equity[i] = equity
|
|
185
|
+
|
|
186
|
+
# エクイティが負の場合、すべてを0に設定してシミュレーションを停止
|
|
187
|
+
if equity <= 0:
|
|
188
|
+
assert self.margin_available <= 0
|
|
189
|
+
for trade in self.trades:
|
|
190
|
+
self._close_trade(trade, self._data.Close.iloc[-1], i)
|
|
191
|
+
self._cash = 0
|
|
192
|
+
self._equity[i:] = 0
|
|
193
|
+
raise Exception
|
|
194
|
+
|
|
195
|
+
def _process_orders(self):
|
|
196
|
+
data = self._data
|
|
197
|
+
open, high, low = data.Open.iloc[-1], data.High.iloc[-1], data.Low.iloc[-1]
|
|
198
|
+
reprocess_orders = False
|
|
199
|
+
|
|
200
|
+
# 注文を処理
|
|
201
|
+
for order in list(self.orders): # type: Order
|
|
202
|
+
|
|
203
|
+
# 関連するSL/TP注文は既に削除されている
|
|
204
|
+
if order not in self.orders:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
# ストップ条件が満たされたかチェック
|
|
208
|
+
stop_price = order.stop
|
|
209
|
+
if stop_price:
|
|
210
|
+
is_stop_hit = ((high >= stop_price) if order.is_long else (low <= stop_price))
|
|
211
|
+
if not is_stop_hit:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
# ストップ価格に達すると、ストップ注文は成行/指値注文になる
|
|
215
|
+
# https://www.sec.gov/fast-answers/answersstopordhtm.html
|
|
216
|
+
order._replace(stop_price=None)
|
|
217
|
+
|
|
218
|
+
# 購入価格を決定
|
|
219
|
+
# 指値注文が約定可能かチェック
|
|
220
|
+
if order.limit:
|
|
221
|
+
is_limit_hit = low <= order.limit if order.is_long else high >= order.limit
|
|
222
|
+
# ストップとリミットが同じバー内で満たされた場合、悲観的に
|
|
223
|
+
# リミットがストップより先に満たされたと仮定する(つまり「カウントされる前に」)
|
|
224
|
+
is_limit_hit_before_stop = (is_limit_hit and
|
|
225
|
+
(order.limit <= (stop_price or -np.inf)
|
|
226
|
+
if order.is_long
|
|
227
|
+
else order.limit >= (stop_price or np.inf)))
|
|
228
|
+
if not is_limit_hit or is_limit_hit_before_stop:
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# stop_priceが設定されている場合、このバー内で満たされた
|
|
232
|
+
price = (min(stop_price or open, order.limit)
|
|
233
|
+
if order.is_long else
|
|
234
|
+
max(stop_price or open, order.limit))
|
|
235
|
+
else:
|
|
236
|
+
# 成行注文(Market-if-touched / market order)
|
|
237
|
+
# 条件付き注文は常に次の始値で
|
|
238
|
+
prev_close = data.Close.iloc[-2]
|
|
239
|
+
price = prev_close if self._trade_on_close and not order.is_contingent else open
|
|
240
|
+
if stop_price:
|
|
241
|
+
price = max(price, stop_price) if order.is_long else min(price, stop_price)
|
|
242
|
+
|
|
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
|
|
273
|
+
|
|
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: 注文はブローカーによってキャンセルされる?
|
|
294
|
+
self.orders.remove(order)
|
|
295
|
+
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
|
+
|
|
318
|
+
if not need_size:
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
# 注文をカバーするのに十分な流動性がない場合、ブローカーはそれをキャンセルする
|
|
322
|
+
if abs(need_size) * adjusted_price_plus_commission > \
|
|
323
|
+
self.margin_available * self._leverage:
|
|
324
|
+
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
|
+
|
|
362
|
+
if reprocess_orders:
|
|
363
|
+
self._process_orders()
|
|
364
|
+
|
|
365
|
+
def _reduce_trade(self, trade: Trade, price: float, size: float, time_index: int):
|
|
366
|
+
assert trade.size * size < 0
|
|
367
|
+
assert abs(trade.size) >= abs(size)
|
|
368
|
+
|
|
369
|
+
size_left = trade.size + size
|
|
370
|
+
assert size_left * trade.size >= 0
|
|
371
|
+
if not size_left:
|
|
372
|
+
close_trade = trade
|
|
373
|
+
else:
|
|
374
|
+
# Reduce existing trade ...
|
|
375
|
+
trade._replace(size=size_left)
|
|
376
|
+
if trade._sl_order:
|
|
377
|
+
trade._sl_order._replace(size=-trade.size)
|
|
378
|
+
if trade._tp_order:
|
|
379
|
+
trade._tp_order._replace(size=-trade.size)
|
|
380
|
+
|
|
381
|
+
# ... by closing a reduced copy of it
|
|
382
|
+
close_trade = trade._copy(size=-size, sl_order=None, tp_order=None)
|
|
383
|
+
self.trades.append(close_trade)
|
|
384
|
+
|
|
385
|
+
self._close_trade(close_trade, price, time_index)
|
|
386
|
+
|
|
387
|
+
def _close_trade(self, trade: Trade, price: float, time_index: int):
|
|
388
|
+
self.trades.remove(trade)
|
|
389
|
+
if trade._sl_order:
|
|
390
|
+
self.orders.remove(trade._sl_order)
|
|
391
|
+
if trade._tp_order:
|
|
392
|
+
self.orders.remove(trade._tp_order)
|
|
393
|
+
|
|
394
|
+
closed_trade = trade._replace(exit_price=price, exit_bar=time_index)
|
|
395
|
+
self.closed_trades.append(closed_trade)
|
|
396
|
+
# Apply commission one more time at trade exit
|
|
397
|
+
commission = self._commission(trade.size, price)
|
|
398
|
+
self._cash += trade.pl - commission
|
|
399
|
+
# Save commissions on Trade instance for stats
|
|
400
|
+
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()
|
|
403
|
+
closed_trade._commissions = commission + trade_open_commission
|
|
404
|
+
|
|
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)
|
|
408
|
+
self.trades.append(trade)
|
|
409
|
+
# Apply broker commission at trade open
|
|
410
|
+
self._cash -= self._commission(size, price)
|
|
411
|
+
# Create SL/TP (bracket) orders.
|
|
412
|
+
if tp:
|
|
413
|
+
trade.tp = tp
|
|
414
|
+
if sl:
|
|
415
|
+
trade.sl = sl
|