bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.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.
- bbstrader/__init__.py +27 -0
- bbstrader/__main__.py +92 -0
- bbstrader/api/__init__.py +96 -0
- bbstrader/api/handlers.py +245 -0
- bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
- bbstrader/api/metatrader_client.pyi +624 -0
- bbstrader/assets/bbs_.png +0 -0
- bbstrader/assets/bbstrader.ico +0 -0
- bbstrader/assets/bbstrader.png +0 -0
- bbstrader/assets/qs_metrics_1.png +0 -0
- bbstrader/btengine/__init__.py +54 -0
- bbstrader/btengine/backtest.py +358 -0
- bbstrader/btengine/data.py +737 -0
- bbstrader/btengine/event.py +229 -0
- bbstrader/btengine/execution.py +287 -0
- bbstrader/btengine/performance.py +408 -0
- bbstrader/btengine/portfolio.py +393 -0
- bbstrader/btengine/strategy.py +588 -0
- bbstrader/compat.py +28 -0
- bbstrader/config.py +100 -0
- bbstrader/core/__init__.py +27 -0
- bbstrader/core/data.py +628 -0
- bbstrader/core/strategy.py +466 -0
- bbstrader/metatrader/__init__.py +48 -0
- bbstrader/metatrader/_copier.py +720 -0
- bbstrader/metatrader/account.py +865 -0
- bbstrader/metatrader/broker.py +418 -0
- bbstrader/metatrader/copier.py +1487 -0
- bbstrader/metatrader/rates.py +495 -0
- bbstrader/metatrader/risk.py +667 -0
- bbstrader/metatrader/trade.py +1692 -0
- bbstrader/metatrader/utils.py +402 -0
- bbstrader/models/__init__.py +39 -0
- bbstrader/models/nlp.py +932 -0
- bbstrader/models/optimization.py +182 -0
- bbstrader/scripts.py +665 -0
- bbstrader/trading/__init__.py +33 -0
- bbstrader/trading/execution.py +1159 -0
- bbstrader/trading/strategy.py +362 -0
- bbstrader/trading/utils.py +69 -0
- bbstrader-2.0.3.dist-info/METADATA +396 -0
- bbstrader-2.0.3.dist-info/RECORD +45 -0
- bbstrader-2.0.3.dist-info/WHEEL +5 -0
- bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
- bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Literal, Optional, Union
|
|
4
|
+
|
|
5
|
+
__all__ = ["Event", "Events", "MarketEvent", "SignalEvent", "OrderEvent", "FillEvent"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Event:
|
|
9
|
+
"""
|
|
10
|
+
Event is base class providing an interface for all subsequent
|
|
11
|
+
(inherited) events, that will trigger further events in the
|
|
12
|
+
trading infrastructure.
|
|
13
|
+
Since in many implementations the Event objects will likely develop greater
|
|
14
|
+
complexity, it is thus being "future-proofed" by creating a class hierarchy.
|
|
15
|
+
The Event class is simply a way to ensure that all events have a common interface
|
|
16
|
+
and can be handled in a consistent manner.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Events(Enum):
|
|
23
|
+
MARKET = "MARKET"
|
|
24
|
+
SIGNAL = "SIGNAL"
|
|
25
|
+
ORDER = "ORDER"
|
|
26
|
+
FILL = "FILL"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MarketEvent(Event):
|
|
30
|
+
"""
|
|
31
|
+
Market Events are triggered when the outer while loop of the backtesting
|
|
32
|
+
system begins a new `"heartbeat"`. It occurs when the `DataHandler` object
|
|
33
|
+
receives a new update of market data for any symbols which are currently
|
|
34
|
+
being tracked. It is used to `trigger the Strategy object` generating
|
|
35
|
+
new `trading signals`. The event object simply contains an identification
|
|
36
|
+
that it is a market event, with no other structure.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Initialises the MarketEvent.
|
|
42
|
+
"""
|
|
43
|
+
self.type = Events.MARKET
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SignalEvent(Event):
|
|
47
|
+
"""
|
|
48
|
+
The `Strategy object` utilises market data to create new `SignalEvents`.
|
|
49
|
+
The SignalEvent contains a `strategy ID`, a `ticker symbol`, a `timestamp`
|
|
50
|
+
for when it was generated, a `direction` (long or short) and a `"strength"`
|
|
51
|
+
indicator (this is useful for mean reversion strategies) and the `quantiy`
|
|
52
|
+
to buy or sell. The `SignalEvents` are utilised by the `Portfolio object`
|
|
53
|
+
as advice for how to trade.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
strategy_id: int,
|
|
59
|
+
symbol: str,
|
|
60
|
+
datetime: datetime,
|
|
61
|
+
signal_type: Literal["LONG", "SHORT", "EXIT"],
|
|
62
|
+
quantity: Union[int, float] = 100,
|
|
63
|
+
strength: Union[int, float] = 1.0,
|
|
64
|
+
price: Optional[Union[int, float]] = None,
|
|
65
|
+
stoplimit: Optional[Union[int, float]] = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Initialises the SignalEvent.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
strategy_id (int): The unique identifier for the strategy that
|
|
72
|
+
generated the signal.
|
|
73
|
+
|
|
74
|
+
symbol (str): The ticker symbol, e.g. 'GOOG'.
|
|
75
|
+
datetime (datetime): The timestamp at which the signal was generated.
|
|
76
|
+
signal_type (str): 'LONG' or 'SHORT' or 'EXIT'.
|
|
77
|
+
quantity (int | float): An optional integer (or float) representing the order size.
|
|
78
|
+
strength (int | float): An adjustment factor "suggestion" used to scale
|
|
79
|
+
quantity at the portfolio level. Useful for pairs strategies.
|
|
80
|
+
price (int | float): An optional price to be used when the signal is generated.
|
|
81
|
+
stoplimit (int | float): An optional stop-limit price for the signal
|
|
82
|
+
"""
|
|
83
|
+
self.type = Events.SIGNAL
|
|
84
|
+
self.strategy_id = strategy_id
|
|
85
|
+
self.symbol = symbol
|
|
86
|
+
self.datetime = datetime
|
|
87
|
+
self.signal_type = signal_type
|
|
88
|
+
self.quantity = quantity
|
|
89
|
+
self.strength = strength
|
|
90
|
+
self.price = price
|
|
91
|
+
self.stoplimit = stoplimit
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OrderEvent(Event):
|
|
95
|
+
"""
|
|
96
|
+
When a Portfolio object receives `SignalEvents` it assesses them
|
|
97
|
+
in the wider context of the portfolio, in terms of risk and position sizing.
|
|
98
|
+
This ultimately leads to `OrderEvents` that will be sent to an `ExecutionHandler`.
|
|
99
|
+
|
|
100
|
+
The `OrderEvents` is slightly more complex than a `SignalEvents` since
|
|
101
|
+
it contains a quantity field in addition to the aforementioned properties
|
|
102
|
+
of SignalEvent. The quantity is determined by the Portfolio constraints.
|
|
103
|
+
In addition the OrderEvent has a `print_order()` method, used to output the
|
|
104
|
+
information to the console if necessary.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
symbol: str,
|
|
110
|
+
order_type: Literal["MKT", "LMT", "STP", "STPLMT"],
|
|
111
|
+
quantity: Union[int, float],
|
|
112
|
+
direction: Literal["BUY", "SELL"],
|
|
113
|
+
price: Optional[Union[int, float]] = None,
|
|
114
|
+
signal: Optional[str] = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Initialises the order type, setting whether it is
|
|
118
|
+
a Market order ('MKT') or Limit order ('LMT'), or Stop order ('STP').
|
|
119
|
+
a quantity (integral or float) and its direction ('BUY' or 'SELL').
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
symbol (str): The instrument to trade.
|
|
123
|
+
order_type (str): 'MKT' or 'LMT' for Market or Limit.
|
|
124
|
+
quantity (int | float): Non-negative number for quantity.
|
|
125
|
+
direction (str): 'BUY' or 'SELL' for long or short.
|
|
126
|
+
price (int | float): The price at which to order.
|
|
127
|
+
signal (str): The signal that generated the order.
|
|
128
|
+
"""
|
|
129
|
+
self.type = Events.ORDER
|
|
130
|
+
self.symbol = symbol
|
|
131
|
+
self.order_type = order_type
|
|
132
|
+
self.quantity = quantity
|
|
133
|
+
self.direction = direction
|
|
134
|
+
self.price = price
|
|
135
|
+
self.signal = signal
|
|
136
|
+
|
|
137
|
+
def print_order(self) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Outputs the values within the Order.
|
|
140
|
+
"""
|
|
141
|
+
print(
|
|
142
|
+
"Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s, Price=%s"
|
|
143
|
+
% (
|
|
144
|
+
self.symbol,
|
|
145
|
+
self.order_type,
|
|
146
|
+
self.quantity,
|
|
147
|
+
self.direction,
|
|
148
|
+
self.price,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class FillEvent(Event):
|
|
154
|
+
"""
|
|
155
|
+
When an `ExecutionHandler` receives an `OrderEvent` it must transact the order.
|
|
156
|
+
Once an order has been transacted it generates a `FillEvent`, which describes
|
|
157
|
+
the cost of purchase or sale as well as the transaction costs, such as fees
|
|
158
|
+
or slippage.
|
|
159
|
+
|
|
160
|
+
The `FillEvent` is the Event with the greatest complexity.
|
|
161
|
+
It contains a `timestamp` for when an order was filled, the `symbol`
|
|
162
|
+
of the order and the `exchange` it was executed on, the `quantity`
|
|
163
|
+
of shares transacted, the `actual price of the purchase` and the `commission
|
|
164
|
+
incurred`.
|
|
165
|
+
|
|
166
|
+
The commission is calculated using the Interactive Brokers commissions.
|
|
167
|
+
For US API orders this commission is `1.30 USD` minimum per order, with a flat
|
|
168
|
+
rate of either 0.013 USD or 0.08 USD per share depending upon whether
|
|
169
|
+
the trade size is below or above `500 units` of stock.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
timeindex: datetime,
|
|
175
|
+
symbol: str,
|
|
176
|
+
exchange: str,
|
|
177
|
+
quantity: Union[int, float],
|
|
178
|
+
direction: Literal["BUY", "SELL"],
|
|
179
|
+
fill_cost: Optional[Union[int, float]],
|
|
180
|
+
commission: Optional[float] = None,
|
|
181
|
+
order: Optional[str] = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Initialises the FillEvent object. Sets the symbol, exchange,
|
|
185
|
+
quantity, direction, cost of fill and an optional
|
|
186
|
+
commission.
|
|
187
|
+
|
|
188
|
+
If commission is not provided, the Fill object will
|
|
189
|
+
calculate it based on the trade size and Interactive
|
|
190
|
+
Brokers fees.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
timeindex (datetime): The bar-resolution when the order was filled.
|
|
194
|
+
symbol (str): The instrument which was filled.
|
|
195
|
+
exchange (str): The exchange where the order was filled.
|
|
196
|
+
quantity (int | float): The filled quantity.
|
|
197
|
+
direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
|
|
198
|
+
fill_cost (int | float): Price of the shares when filled.
|
|
199
|
+
commission (float | None): An optional commission sent from IB.
|
|
200
|
+
order (str): The order that this fill is related
|
|
201
|
+
"""
|
|
202
|
+
self.type = Events.FILL
|
|
203
|
+
self.timeindex = timeindex
|
|
204
|
+
self.symbol = symbol
|
|
205
|
+
self.exchange = exchange
|
|
206
|
+
self.quantity = quantity
|
|
207
|
+
self.direction = direction
|
|
208
|
+
self.fill_cost = fill_cost
|
|
209
|
+
# Calculate commission
|
|
210
|
+
if commission is None:
|
|
211
|
+
self.commission: float = self.calculate_ib_commission()
|
|
212
|
+
else:
|
|
213
|
+
self.commission = commission
|
|
214
|
+
self.order = order
|
|
215
|
+
|
|
216
|
+
def calculate_ib_commission(self) -> float:
|
|
217
|
+
"""
|
|
218
|
+
Calculates the fees of trading based on an Interactive
|
|
219
|
+
Brokers fee structure for API, in USD.
|
|
220
|
+
This does not include exchange or ECN fees.
|
|
221
|
+
Based on "US API Directed Orders":
|
|
222
|
+
https://www.interactivebrokers.com/en/index.php?f=commission&p=stocks2
|
|
223
|
+
"""
|
|
224
|
+
full_cost = 1.3
|
|
225
|
+
if self.quantity <= 500:
|
|
226
|
+
full_cost = max(1.3, 0.013 * self.quantity)
|
|
227
|
+
else:
|
|
228
|
+
full_cost = max(1.3, 0.008 * self.quantity)
|
|
229
|
+
return full_cost
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
from abc import ABCMeta, abstractmethod
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from bbstrader.btengine.data import DataHandler
|
|
8
|
+
from bbstrader.btengine.event import Events, FillEvent, OrderEvent
|
|
9
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
10
|
+
from bbstrader.metatrader.account import Account
|
|
11
|
+
from bbstrader.metatrader.utils import SymbolType
|
|
12
|
+
|
|
13
|
+
__all__ = ["ExecutionHandler", "SimExecutionHandler", "MT5ExecutionHandler"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger.add(
|
|
17
|
+
f"{BBSTRADER_DIR}/logs/execution.log",
|
|
18
|
+
enqueue=True,
|
|
19
|
+
level="INFO",
|
|
20
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExecutionHandler(metaclass=ABCMeta):
|
|
25
|
+
"""
|
|
26
|
+
The ExecutionHandler abstract class handles the interaction
|
|
27
|
+
between a set of order objects generated by a Portfolio and
|
|
28
|
+
the ultimate set of Fill objects that actually occur in the
|
|
29
|
+
market.
|
|
30
|
+
|
|
31
|
+
The handlers can be used to subclass simulated brokerages
|
|
32
|
+
or live brokerages, with identical interfaces. This allows
|
|
33
|
+
strategies to be backtested in a very similar manner to the
|
|
34
|
+
live trading engine.
|
|
35
|
+
|
|
36
|
+
The ExecutionHandler described here is exceedingly simple,
|
|
37
|
+
since it fills all orders at the current market price.
|
|
38
|
+
This is highly unrealistic, for other markets thant ``CFDs``
|
|
39
|
+
but serves as a good baseline for improvement.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def execute_order(self, event: OrderEvent) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Takes an Order event and executes it, producing
|
|
46
|
+
a Fill event that gets placed onto the Events queue.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
event (OrderEvent): Contains an Event object with order information.
|
|
50
|
+
"""
|
|
51
|
+
raise NotImplementedError("Should implement execute_order()")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SimExecutionHandler(ExecutionHandler):
|
|
55
|
+
"""
|
|
56
|
+
The simulated execution handler simply converts all order
|
|
57
|
+
objects into their equivalent fill objects automatically
|
|
58
|
+
without latency, slippage or fill-ratio issues.
|
|
59
|
+
|
|
60
|
+
This allows a straightforward "first go" test of any strategy,
|
|
61
|
+
before implementation with a more sophisticated execution
|
|
62
|
+
handler.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
events: "Queue[Union[FillEvent, OrderEvent]]",
|
|
68
|
+
data: DataHandler,
|
|
69
|
+
**kwargs: Any,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Initialises the handler, setting the event queues
|
|
73
|
+
up internally.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
events (Queue): The Queue of Event objects.
|
|
77
|
+
"""
|
|
78
|
+
self.events = events
|
|
79
|
+
self.bardata = data
|
|
80
|
+
self.logger = kwargs.get("logger") or logger
|
|
81
|
+
self.commissions = kwargs.get("commission")
|
|
82
|
+
self.exchange = kwargs.get("exchange", "ARCA")
|
|
83
|
+
|
|
84
|
+
def execute_order(self, event: OrderEvent) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Simply converts Order objects into Fill objects naively,
|
|
87
|
+
i.e. without any latency, slippage or fill ratio problems.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
event (OrderEvent): Contains an Event object with order information.
|
|
91
|
+
"""
|
|
92
|
+
if event.type == Events.ORDER:
|
|
93
|
+
dtime = self.bardata.get_latest_bar_datetime(event.symbol)
|
|
94
|
+
fill_event = FillEvent(
|
|
95
|
+
timeindex=dtime, # type: ignore
|
|
96
|
+
symbol=event.symbol,
|
|
97
|
+
exchange=self.exchange,
|
|
98
|
+
quantity=event.quantity,
|
|
99
|
+
direction=event.direction,
|
|
100
|
+
fill_cost=None,
|
|
101
|
+
commission=self.commissions,
|
|
102
|
+
order=event.signal,
|
|
103
|
+
)
|
|
104
|
+
self.events.put(fill_event)
|
|
105
|
+
price = event.price or 0.0
|
|
106
|
+
self.logger.info(
|
|
107
|
+
f"{event.direction} ORDER FILLED: SYMBOL={event.symbol}, "
|
|
108
|
+
f"QUANTITY={event.quantity}, PRICE @{round(price, 5)} EXCHANGE={fill_event.exchange}",
|
|
109
|
+
custom_time=fill_event.timeindex,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class MT5ExecutionHandler(ExecutionHandler):
|
|
114
|
+
"""
|
|
115
|
+
The main role of `MT5ExecutionHandler` class is to estimate the execution fees
|
|
116
|
+
for different asset classes on the MT5 terminal.
|
|
117
|
+
|
|
118
|
+
Generally we have four types of fees when we execute trades using the MT5 terminal
|
|
119
|
+
(commissions, swap, spread and other fees). But most of these fees depend on the specifications
|
|
120
|
+
of each instrument and the duration of the transaction for the swap for example.
|
|
121
|
+
|
|
122
|
+
Calculating the exact fees for each instrument would be a bit complex because our Backtest engine
|
|
123
|
+
and the Portfolio class do not take into account the duration of each trade to apply the appropriate
|
|
124
|
+
rate for the swap for example. So we have to use only the model of calculating the commissions
|
|
125
|
+
for each asset class and each instrument.
|
|
126
|
+
|
|
127
|
+
The second thing that must be taken into account on MT5 is the type of account offered by the broker.
|
|
128
|
+
Brokers have different account categories each with its specifications for each asset class and each instrument.
|
|
129
|
+
Again considering all these conditions would make our class very complex. So we took the `Raw Spread`
|
|
130
|
+
account fee calculation model from [Just Market](https://one.justmarkets.link/a/tufvj0xugm/registration/trader)
|
|
131
|
+
for indicies, forex, commodities and crypto. We used the [Admiral Market](https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets)
|
|
132
|
+
account fee calculation model from `Trade.MT5` account type for stocks and ETFs.
|
|
133
|
+
|
|
134
|
+
NOTE:
|
|
135
|
+
This class only works with `bbstrader.metatrader.data.MT5DataHandler` class.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
events: "Queue[Union[FillEvent, OrderEvent]]",
|
|
141
|
+
data: DataHandler,
|
|
142
|
+
**kwargs: Any,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Initialises the handler, setting the event queues up internally.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
events (Queue): The Queue of Event objects.
|
|
149
|
+
"""
|
|
150
|
+
self.events = events
|
|
151
|
+
self.bardata = data
|
|
152
|
+
self.logger = kwargs.get("logger") or logger
|
|
153
|
+
self.commissions = kwargs.get("commission")
|
|
154
|
+
self.exchange = kwargs.get("exchange", "MT5")
|
|
155
|
+
self.__account = Account(**kwargs)
|
|
156
|
+
|
|
157
|
+
def _calculate_lot(
|
|
158
|
+
self, symbol: str, quantity: Union[int, float], price: Union[int, float]
|
|
159
|
+
) -> float:
|
|
160
|
+
symbol_type = self.__account.get_symbol_type(symbol)
|
|
161
|
+
symbol_info = self.__account.get_symbol_info(symbol)
|
|
162
|
+
contract_size = symbol_info.trade_contract_size
|
|
163
|
+
|
|
164
|
+
lot = (quantity * price) / (contract_size * price)
|
|
165
|
+
if contract_size == 1:
|
|
166
|
+
lot = float(quantity)
|
|
167
|
+
if (
|
|
168
|
+
symbol_type
|
|
169
|
+
in (SymbolType.COMMODITIES, SymbolType.FUTURES, SymbolType.CRYPTO)
|
|
170
|
+
and contract_size > 1
|
|
171
|
+
):
|
|
172
|
+
lot = quantity / contract_size
|
|
173
|
+
if symbol_type == SymbolType.FOREX:
|
|
174
|
+
lot = float(quantity * price / contract_size)
|
|
175
|
+
return self.__account.broker.validate_lot_size(symbol, lot)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _estimate_total_fees(
|
|
179
|
+
self,
|
|
180
|
+
symbol: str,
|
|
181
|
+
lot: float,
|
|
182
|
+
qty: Union[int, float],
|
|
183
|
+
price: Union[int, float],
|
|
184
|
+
) -> float:
|
|
185
|
+
symbol_type = self.__account.get_symbol_type(symbol)
|
|
186
|
+
if symbol_type in (SymbolType.STOCKS, SymbolType.ETFs):
|
|
187
|
+
return self._estimate_stock_commission(symbol, qty, price)
|
|
188
|
+
elif symbol_type == SymbolType.FOREX:
|
|
189
|
+
return self._estimate_forex_commission(lot)
|
|
190
|
+
elif symbol_type == SymbolType.COMMODITIES:
|
|
191
|
+
return self._estimate_commodity_commission(lot)
|
|
192
|
+
elif symbol_type == SymbolType.INDICES:
|
|
193
|
+
return self._estimate_index_commission(lot)
|
|
194
|
+
elif symbol_type == SymbolType.FUTURES:
|
|
195
|
+
return self._estimate_futures_commission()
|
|
196
|
+
elif symbol_type == SymbolType.CRYPTO:
|
|
197
|
+
return self._estimate_crypto_commission()
|
|
198
|
+
else:
|
|
199
|
+
return 0.0
|
|
200
|
+
|
|
201
|
+
def _estimate_stock_commission(
|
|
202
|
+
self, symbol: str, qty: Union[int, float], price: Union[int, float]
|
|
203
|
+
) -> float:
|
|
204
|
+
# https://admiralmarkets.com/start-trading/contract-specifications?regulator=jsc
|
|
205
|
+
min_com = 1.0
|
|
206
|
+
min_aud = 8.0
|
|
207
|
+
min_dkk = 30.0
|
|
208
|
+
min_nok = min_sek = 10.0
|
|
209
|
+
us_com = 0.02 # per chare
|
|
210
|
+
ger_fr_uk_cm = 0.001 # percent
|
|
211
|
+
eu_asia_cm = 0.0015 # percent
|
|
212
|
+
if (
|
|
213
|
+
symbol in self.__account.get_stocks_from_country("USA")
|
|
214
|
+
or self.__account.get_symbol_type(symbol) == SymbolType.ETFs
|
|
215
|
+
and self.__account.get_currency_rates(symbol)["mc"] == "USD"
|
|
216
|
+
):
|
|
217
|
+
return max(min_com, qty * us_com)
|
|
218
|
+
elif (
|
|
219
|
+
symbol in self.__account.get_stocks_from_country("GBR")
|
|
220
|
+
or symbol in self.__account.get_stocks_from_country("FRA")
|
|
221
|
+
or symbol in self.__account.get_stocks_from_country("DEU")
|
|
222
|
+
or self.__account.get_symbol_type(symbol) == SymbolType.ETFs
|
|
223
|
+
and self.__account.get_currency_rates(symbol)["mc"] in ["GBP", "EUR"]
|
|
224
|
+
):
|
|
225
|
+
return max(min_com, qty * price * ger_fr_uk_cm)
|
|
226
|
+
else:
|
|
227
|
+
if self.__account.get_currency_rates(symbol)["mc"] == "AUD":
|
|
228
|
+
return max(min_aud, qty * price * eu_asia_cm)
|
|
229
|
+
elif self.__account.get_currency_rates(symbol)["mc"] == "DKK":
|
|
230
|
+
return max(min_dkk, qty * price * eu_asia_cm)
|
|
231
|
+
elif self.__account.get_currency_rates(symbol)["mc"] == "NOK":
|
|
232
|
+
return max(min_nok, qty * price * eu_asia_cm)
|
|
233
|
+
elif self.__account.get_currency_rates(symbol)["mc"] == "SEK":
|
|
234
|
+
return max(min_sek, qty * price * eu_asia_cm)
|
|
235
|
+
else:
|
|
236
|
+
return max(min_com, qty * price * eu_asia_cm)
|
|
237
|
+
|
|
238
|
+
def _estimate_forex_commission(self, lot: float) -> float:
|
|
239
|
+
return 3.0 * lot
|
|
240
|
+
|
|
241
|
+
def _estimate_commodity_commission(self, lot: float) -> float:
|
|
242
|
+
return 3.0 * lot
|
|
243
|
+
|
|
244
|
+
def _estimate_index_commission(self, lot: float) -> float:
|
|
245
|
+
return 0.25 * lot
|
|
246
|
+
|
|
247
|
+
def _estimate_futures_commission(self) -> float:
|
|
248
|
+
return 0.0
|
|
249
|
+
|
|
250
|
+
def _estimate_crypto_commission(self) -> float:
|
|
251
|
+
return 0.0
|
|
252
|
+
|
|
253
|
+
def execute_order(self, event: OrderEvent) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Executes an Order event by converting it into a Fill event.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
event (OrderEvent): Contains an Event object with order information.
|
|
259
|
+
"""
|
|
260
|
+
if event.type == Events.ORDER:
|
|
261
|
+
symbol = event.symbol
|
|
262
|
+
direction = event.direction
|
|
263
|
+
quantity = event.quantity
|
|
264
|
+
price = event.price
|
|
265
|
+
if price is None:
|
|
266
|
+
price = self.bardata.get_latest_bar_value(symbol, "close")
|
|
267
|
+
lot = self._calculate_lot(symbol, quantity, price)
|
|
268
|
+
fees = self._estimate_total_fees(symbol, lot, quantity, price)
|
|
269
|
+
dtime = self.bardata.get_latest_bar_datetime(symbol)
|
|
270
|
+
commission = self.commissions or fees
|
|
271
|
+
fill_event = FillEvent(
|
|
272
|
+
timeindex=dtime, # type: ignore
|
|
273
|
+
symbol=symbol,
|
|
274
|
+
exchange=self.exchange,
|
|
275
|
+
quantity=quantity,
|
|
276
|
+
direction=direction,
|
|
277
|
+
fill_cost=None,
|
|
278
|
+
commission=commission,
|
|
279
|
+
order=event.signal,
|
|
280
|
+
)
|
|
281
|
+
self.events.put(fill_event)
|
|
282
|
+
log_price = event.price or 0.0
|
|
283
|
+
self.logger.info(
|
|
284
|
+
f"{direction} ORDER FILLED: SYMBOL={symbol}, QUANTITY={quantity}, "
|
|
285
|
+
f"PRICE @{round(log_price, 5)} EXCHANGE={fill_event.exchange}",
|
|
286
|
+
custom_time=fill_event.timeindex,
|
|
287
|
+
)
|