bbstrader 0.2.97__py3-none-any.whl → 0.2.99__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 bbstrader might be problematic. Click here for more details.
- bbstrader/__main__.py +1 -1
- bbstrader/btengine/backtest.py +7 -7
- bbstrader/btengine/event.py +12 -4
- bbstrader/btengine/execution.py +3 -3
- bbstrader/btengine/portfolio.py +3 -3
- bbstrader/btengine/strategy.py +12 -5
- bbstrader/metatrader/account.py +30 -6
- bbstrader/metatrader/analysis.py +98 -0
- bbstrader/metatrader/copier.py +77 -57
- bbstrader/metatrader/trade.py +82 -118
- bbstrader/metatrader/utils.py +16 -0
- bbstrader/models/factors.py +97 -97
- bbstrader/trading/execution.py +686 -566
- bbstrader/trading/scripts.py +10 -11
- bbstrader/trading/strategies.py +13 -5
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/METADATA +12 -16
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/RECORD +21 -20
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/top_level.txt +0 -0
bbstrader/__main__.py
CHANGED
|
@@ -16,7 +16,7 @@ USAGE_TEXT = """
|
|
|
16
16
|
Modules:
|
|
17
17
|
copier: Copy trades from one MetaTrader account to another or multiple accounts
|
|
18
18
|
backtest: Backtest a strategy, see bbstrader.btengine.backtest.run_backtest
|
|
19
|
-
execution: Execute a strategy, see bbstrader.trading.execution.
|
|
19
|
+
execution: Execute a strategy, see bbstrader.trading.execution.Mt5ExecutionEngine
|
|
20
20
|
|
|
21
21
|
python -m bbstrader --run <module> --help for more information on the module
|
|
22
22
|
"""
|
bbstrader/btengine/backtest.py
CHANGED
|
@@ -4,7 +4,7 @@ from datetime import datetime
|
|
|
4
4
|
from typing import List, Literal, Optional
|
|
5
5
|
|
|
6
6
|
from tabulate import tabulate
|
|
7
|
-
|
|
7
|
+
from bbstrader.btengine.event import Events
|
|
8
8
|
from bbstrader.btengine.data import DataHandler
|
|
9
9
|
from bbstrader.btengine.execution import ExecutionHandler, SimExecutionHandler
|
|
10
10
|
from bbstrader.btengine.portfolio import Portfolio
|
|
@@ -165,19 +165,19 @@ class BacktestEngine(Backtest):
|
|
|
165
165
|
break
|
|
166
166
|
else:
|
|
167
167
|
if event is not None:
|
|
168
|
-
if event.type ==
|
|
168
|
+
if event.type == Events.MARKET:
|
|
169
169
|
self.strategy.calculate_signals(event)
|
|
170
170
|
self.portfolio.update_timeindex(event)
|
|
171
171
|
|
|
172
|
-
elif event.type ==
|
|
172
|
+
elif event.type == Events.SIGNAL:
|
|
173
173
|
self.signals += 1
|
|
174
174
|
self.portfolio.update_signal(event)
|
|
175
175
|
|
|
176
|
-
elif event.type ==
|
|
176
|
+
elif event.type == Events.ORDER:
|
|
177
177
|
self.orders += 1
|
|
178
178
|
self.execution_handler.execute_order(event)
|
|
179
179
|
|
|
180
|
-
elif event.type ==
|
|
180
|
+
elif event.type == Events.FILL:
|
|
181
181
|
self.fills += 1
|
|
182
182
|
self.portfolio.update_fill(event)
|
|
183
183
|
self.strategy.update_trades_from_fill(event)
|
|
@@ -354,7 +354,7 @@ def run_backtest_with(engine: Literal["bbstrader", "cerebro", "zipline"], **kwar
|
|
|
354
354
|
)
|
|
355
355
|
elif engine == "cerebro":
|
|
356
356
|
# TODO:
|
|
357
|
-
|
|
357
|
+
raise NotImplementedError("cerebro engine is not supported yet")
|
|
358
358
|
elif engine == "zipline":
|
|
359
359
|
# TODO:
|
|
360
|
-
|
|
360
|
+
raise NotImplementedError("zipline engine is not supported yet")
|
bbstrader/btengine/event.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
2
3
|
from typing import Literal
|
|
3
4
|
|
|
4
5
|
__all__ = ["Event", "MarketEvent", "SignalEvent", "OrderEvent", "FillEvent"]
|
|
@@ -18,6 +19,13 @@ class Event(object):
|
|
|
18
19
|
...
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
class Events(Enum):
|
|
23
|
+
MARKET = "MARKET"
|
|
24
|
+
SIGNAL = "SIGNAL"
|
|
25
|
+
ORDER = "ORDER"
|
|
26
|
+
FILL = "FILL"
|
|
27
|
+
|
|
28
|
+
|
|
21
29
|
class MarketEvent(Event):
|
|
22
30
|
"""
|
|
23
31
|
Market Events are triggered when the outer while loop of the backtesting
|
|
@@ -32,7 +40,7 @@ class MarketEvent(Event):
|
|
|
32
40
|
"""
|
|
33
41
|
Initialises the MarketEvent.
|
|
34
42
|
"""
|
|
35
|
-
self.type =
|
|
43
|
+
self.type = Events.MARKET
|
|
36
44
|
|
|
37
45
|
|
|
38
46
|
class SignalEvent(Event):
|
|
@@ -72,7 +80,7 @@ class SignalEvent(Event):
|
|
|
72
80
|
price (int | float): An optional price to be used when the signal is generated.
|
|
73
81
|
stoplimit (int | float): An optional stop-limit price for the signal
|
|
74
82
|
"""
|
|
75
|
-
self.type =
|
|
83
|
+
self.type = Events.SIGNAL
|
|
76
84
|
self.strategy_id = strategy_id
|
|
77
85
|
self.symbol = symbol
|
|
78
86
|
self.datetime = datetime
|
|
@@ -118,7 +126,7 @@ class OrderEvent(Event):
|
|
|
118
126
|
price (int | float): The price at which to order.
|
|
119
127
|
signal (str): The signal that generated the order.
|
|
120
128
|
"""
|
|
121
|
-
self.type =
|
|
129
|
+
self.type = Events.ORDER
|
|
122
130
|
self.symbol = symbol
|
|
123
131
|
self.order_type = order_type
|
|
124
132
|
self.quantity = quantity
|
|
@@ -191,7 +199,7 @@ class FillEvent(Event):
|
|
|
191
199
|
commission (float | None): An optional commission sent from IB.
|
|
192
200
|
order (str): The order that this fill is related
|
|
193
201
|
"""
|
|
194
|
-
self.type =
|
|
202
|
+
self.type = Events.FILL
|
|
195
203
|
self.timeindex = timeindex
|
|
196
204
|
self.symbol = symbol
|
|
197
205
|
self.exchange = exchange
|
bbstrader/btengine/execution.py
CHANGED
|
@@ -4,7 +4,7 @@ from queue import Queue
|
|
|
4
4
|
from loguru import logger
|
|
5
5
|
|
|
6
6
|
from bbstrader.btengine.data import DataHandler
|
|
7
|
-
from bbstrader.btengine.event import FillEvent, OrderEvent
|
|
7
|
+
from bbstrader.btengine.event import Events, FillEvent, OrderEvent
|
|
8
8
|
from bbstrader.config import BBSTRADER_DIR
|
|
9
9
|
from bbstrader.metatrader.account import Account
|
|
10
10
|
|
|
@@ -80,7 +80,7 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
80
80
|
Args:
|
|
81
81
|
event (OrderEvent): Contains an Event object with order information.
|
|
82
82
|
"""
|
|
83
|
-
if event.type ==
|
|
83
|
+
if event.type == Events.ORDER:
|
|
84
84
|
dtime = self.bardata.get_latest_bar_datetime(event.symbol)
|
|
85
85
|
fill_event = FillEvent(
|
|
86
86
|
timeindex=dtime,
|
|
@@ -233,7 +233,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
233
233
|
Args:
|
|
234
234
|
event (OrderEvent): Contains an Event object with order information.
|
|
235
235
|
"""
|
|
236
|
-
if event.type ==
|
|
236
|
+
if event.type == Events.ORDER:
|
|
237
237
|
symbol = event.symbol
|
|
238
238
|
direction = event.direction
|
|
239
239
|
quantity = event.quantity
|
bbstrader/btengine/portfolio.py
CHANGED
|
@@ -6,7 +6,7 @@ import pandas as pd
|
|
|
6
6
|
import quantstats as qs
|
|
7
7
|
|
|
8
8
|
from bbstrader.btengine.data import DataHandler
|
|
9
|
-
from bbstrader.btengine.event import FillEvent, MarketEvent, OrderEvent, SignalEvent
|
|
9
|
+
from bbstrader.btengine.event import Events, FillEvent, MarketEvent, OrderEvent, SignalEvent
|
|
10
10
|
from bbstrader.btengine.performance import (
|
|
11
11
|
create_drawdowns,
|
|
12
12
|
create_sharpe_ratio,
|
|
@@ -282,7 +282,7 @@ class Portfolio(object):
|
|
|
282
282
|
Updates the portfolio current positions and holdings
|
|
283
283
|
from a FillEvent.
|
|
284
284
|
"""
|
|
285
|
-
if event.type ==
|
|
285
|
+
if event.type == Events.FILL:
|
|
286
286
|
self.update_positions_from_fill(event)
|
|
287
287
|
self.update_holdings_from_fill(event)
|
|
288
288
|
|
|
@@ -337,7 +337,7 @@ class Portfolio(object):
|
|
|
337
337
|
Acts on a SignalEvent to generate new orders
|
|
338
338
|
based on the portfolio logic.
|
|
339
339
|
"""
|
|
340
|
-
if event.type ==
|
|
340
|
+
if event.type == Events.SIGNAL:
|
|
341
341
|
order_event = self.generate_order(event)
|
|
342
342
|
self.events.put(order_event)
|
|
343
343
|
|
bbstrader/btengine/strategy.py
CHANGED
|
@@ -10,15 +10,15 @@ import pytz
|
|
|
10
10
|
from loguru import logger
|
|
11
11
|
|
|
12
12
|
from bbstrader.btengine.data import DataHandler
|
|
13
|
-
from bbstrader.btengine.event import FillEvent, SignalEvent
|
|
13
|
+
from bbstrader.btengine.event import Events, FillEvent, SignalEvent
|
|
14
14
|
from bbstrader.config import BBSTRADER_DIR
|
|
15
|
-
from bbstrader.metatrader.trade import TradeSignal
|
|
16
15
|
from bbstrader.metatrader.account import (
|
|
17
16
|
Account,
|
|
18
17
|
AdmiralMarktsGroup,
|
|
19
18
|
PepperstoneGroupLimited,
|
|
20
19
|
)
|
|
21
20
|
from bbstrader.metatrader.rates import Rates
|
|
21
|
+
from bbstrader.metatrader.trade import TradeSignal
|
|
22
22
|
from bbstrader.models.optimization import optimized_weights
|
|
23
23
|
|
|
24
24
|
__all__ = ["Strategy", "MT5Strategy"]
|
|
@@ -67,7 +67,7 @@ class MT5Strategy(Strategy):
|
|
|
67
67
|
"""
|
|
68
68
|
A `MT5Strategy()` object is a subclass of `Strategy` that is used to
|
|
69
69
|
calculate signals for the MetaTrader 5 trading platform. The signals
|
|
70
|
-
are generated by the `MT5Strategy` object and sent to the the `
|
|
70
|
+
are generated by the `MT5Strategy` object and sent to the the `Mt5ExecutionEngine`
|
|
71
71
|
for live trading and `MT5BacktestEngine` objects for backtesting.
|
|
72
72
|
"""
|
|
73
73
|
|
|
@@ -146,9 +146,9 @@ class MT5Strategy(Strategy):
|
|
|
146
146
|
def _initialize_portfolio(self):
|
|
147
147
|
positions = ["LONG", "SHORT"]
|
|
148
148
|
orders = ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]
|
|
149
|
+
self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
149
150
|
self._positions: Dict[str, Dict[str, int | float]] = {}
|
|
150
151
|
self._trades: Dict[str, Dict[str, int]] = {}
|
|
151
|
-
self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
152
152
|
for symbol in self.symbols:
|
|
153
153
|
self._positions[symbol] = {}
|
|
154
154
|
self._orders[symbol] = {}
|
|
@@ -188,7 +188,7 @@ class MT5Strategy(Strategy):
|
|
|
188
188
|
This method updates the trades for the strategy based on the fill event.
|
|
189
189
|
It is used to keep track of the number of trades executed for each order.
|
|
190
190
|
"""
|
|
191
|
-
if event.type ==
|
|
191
|
+
if event.type == Events.FILL:
|
|
192
192
|
if event.order != "EXIT":
|
|
193
193
|
self._trades[event.symbol][event.order] += 1
|
|
194
194
|
elif event.order == "EXIT" and event.direction == "BUY":
|
|
@@ -206,6 +206,7 @@ class MT5Strategy(Strategy):
|
|
|
206
206
|
- ``price``: The price at which to execute the action, used for pending orders.
|
|
207
207
|
- ``stoplimit``: The stop-limit price for STOP-LIMIT orders, used for pending stop limit orders.
|
|
208
208
|
- ``id``: The unique identifier for the strategy or order.
|
|
209
|
+
- ``comment``: An optional comment or description related to the trade signal.
|
|
209
210
|
"""
|
|
210
211
|
pass
|
|
211
212
|
|
|
@@ -677,6 +678,12 @@ class MT5Strategy(Strategy):
|
|
|
677
678
|
if period_count == 0 or period_count is None:
|
|
678
679
|
return True
|
|
679
680
|
return period_count % signal_inverval == 0
|
|
681
|
+
|
|
682
|
+
@staticmethod
|
|
683
|
+
def stop_time(time_zone: str, stop_time: str) -> bool:
|
|
684
|
+
now = datetime.now(pytz.timezone(time_zone)).time()
|
|
685
|
+
stop_time = datetime.strptime(stop_time, "%H:%M").time()
|
|
686
|
+
return now >= stop_time
|
|
680
687
|
|
|
681
688
|
def ispositions(
|
|
682
689
|
self, symbol, strategy_id, position, max_trades, one_true=False, account=None
|
bbstrader/metatrader/account.py
CHANGED
|
@@ -9,6 +9,7 @@ from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
|
|
|
9
9
|
|
|
10
10
|
from bbstrader.metatrader.utils import (
|
|
11
11
|
AccountInfo,
|
|
12
|
+
BookInfo,
|
|
12
13
|
InvalidBroker,
|
|
13
14
|
OrderCheckResult,
|
|
14
15
|
OrderSentResult,
|
|
@@ -188,10 +189,12 @@ def check_mt5_connection(**kwargs):
|
|
|
188
189
|
except Exception:
|
|
189
190
|
raise_mt5_error(INIT_MSG)
|
|
190
191
|
|
|
192
|
+
|
|
191
193
|
def shutdown_mt5():
|
|
192
194
|
"""Close the connection to the MetaTrader 5 terminal."""
|
|
193
195
|
mt5.shutdown()
|
|
194
|
-
|
|
196
|
+
|
|
197
|
+
|
|
195
198
|
class Broker(object):
|
|
196
199
|
def __init__(self, name: str = None, **kwargs):
|
|
197
200
|
if name is None:
|
|
@@ -323,7 +326,7 @@ class Account(object):
|
|
|
323
326
|
f"For {supported['FTMO'].name}, See [{ftmo_url}]\n"
|
|
324
327
|
)
|
|
325
328
|
raise InvalidBroker(message=msg)
|
|
326
|
-
|
|
329
|
+
|
|
327
330
|
def shutdown(self):
|
|
328
331
|
"""Close the connection to the MetaTrader 5 terminal."""
|
|
329
332
|
shutdown_mt5()
|
|
@@ -1037,6 +1040,29 @@ class Account(object):
|
|
|
1037
1040
|
"""
|
|
1038
1041
|
self._show_info(self.get_tick_info, "tick", symbol=symbol)
|
|
1039
1042
|
|
|
1043
|
+
def get_market_book(self, symbol: str) -> Union[Tuple[BookInfo], None]:
|
|
1044
|
+
"""
|
|
1045
|
+
Get the Market Depth content for a specific symbol.
|
|
1046
|
+
Args:
|
|
1047
|
+
symbol (str): Financial instrument name. Required unnamed parameter.
|
|
1048
|
+
The symbol name should be specified in the same format as in the Market Watch window.
|
|
1049
|
+
|
|
1050
|
+
Returns:
|
|
1051
|
+
The Market Depth content as a tuple from BookInfo entries featuring order type, price and volume in lots.
|
|
1052
|
+
Return None in case of an error.
|
|
1053
|
+
|
|
1054
|
+
Raises:
|
|
1055
|
+
MT5TerminalError: A specific exception based on the error code.
|
|
1056
|
+
"""
|
|
1057
|
+
try:
|
|
1058
|
+
book = mt5.market_book_get(symbol)
|
|
1059
|
+
if book is None:
|
|
1060
|
+
return None
|
|
1061
|
+
else:
|
|
1062
|
+
return tuple([BookInfo(**entry._asdict()) for entry in book])
|
|
1063
|
+
except Exception as e:
|
|
1064
|
+
raise_mt5_error(e)
|
|
1065
|
+
|
|
1040
1066
|
def calculate_margin(
|
|
1041
1067
|
self, action: Literal["buy", "sell"], symbol: str, lot: float, price: float
|
|
1042
1068
|
) -> float:
|
|
@@ -1299,8 +1325,7 @@ class Account(object):
|
|
|
1299
1325
|
df.drop(["time_msc", "external_id"], axis=1, inplace=True)
|
|
1300
1326
|
df.set_index("time", inplace=True)
|
|
1301
1327
|
if save:
|
|
1302
|
-
|
|
1303
|
-
df.to_csv(file)
|
|
1328
|
+
df.to_csv("trades_history.csv")
|
|
1304
1329
|
if to_df:
|
|
1305
1330
|
return df
|
|
1306
1331
|
else:
|
|
@@ -1507,8 +1532,7 @@ class Account(object):
|
|
|
1507
1532
|
df["time_done"] = pd.to_datetime(df["time_done"], unit="s")
|
|
1508
1533
|
|
|
1509
1534
|
if save:
|
|
1510
|
-
|
|
1511
|
-
df.to_csv(file)
|
|
1535
|
+
df.to_csv("orders_history.csv")
|
|
1512
1536
|
if to_df:
|
|
1513
1537
|
return df
|
|
1514
1538
|
else:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import MetaTrader5 as mt5
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import seaborn as sns
|
|
6
|
+
|
|
7
|
+
from bbstrader.metatrader.account import check_mt5_connection
|
|
8
|
+
from bbstrader.metatrader.utils import TIMEFRAMES
|
|
9
|
+
|
|
10
|
+
sns.set_theme()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_data(path, symbol, timeframe, bars):
|
|
14
|
+
check_mt5_connection(path=path)
|
|
15
|
+
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, bars)
|
|
16
|
+
df = pd.DataFrame(rates)
|
|
17
|
+
df["time"] = pd.to_datetime(df["time"], unit="s")
|
|
18
|
+
return df
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def volume_profile(df, bins):
|
|
22
|
+
prices = (df["high"] + df["low"]) / 2
|
|
23
|
+
volumes = df["tick_volume"]
|
|
24
|
+
hist, bin_edges = np.histogram(prices, bins=bins, weights=volumes)
|
|
25
|
+
bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
|
|
26
|
+
return hist, bin_edges, bin_centers
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def value_area(hist, bin_centers, percentage):
|
|
30
|
+
total_volume = np.sum(hist)
|
|
31
|
+
poc_index = np.argmax(hist)
|
|
32
|
+
poc = bin_centers[poc_index]
|
|
33
|
+
|
|
34
|
+
sorted_indices = np.argsort(hist)[::-1]
|
|
35
|
+
volume_accum = 0
|
|
36
|
+
value_area_indices = []
|
|
37
|
+
|
|
38
|
+
for idx in sorted_indices:
|
|
39
|
+
volume_accum += hist[idx]
|
|
40
|
+
value_area_indices.append(idx)
|
|
41
|
+
if volume_accum >= percentage * total_volume:
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
vah = max(bin_centers[i] for i in value_area_indices)
|
|
45
|
+
val = min(bin_centers[i] for i in value_area_indices)
|
|
46
|
+
return poc, vah, val
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def display_volume_profile(
|
|
50
|
+
symbol,
|
|
51
|
+
path,
|
|
52
|
+
timeframe: str = "1m",
|
|
53
|
+
bars: int = 1440,
|
|
54
|
+
bins: int = 100,
|
|
55
|
+
va_percentage: float = 0.7,
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Display a volume profile chart for a given market symbol using historical data.
|
|
59
|
+
|
|
60
|
+
This function retrieves historical price and volume data for a given symbol and
|
|
61
|
+
plots a vertical volume profile chart showing the volume distribution across
|
|
62
|
+
price levels. It highlights key levels such as:
|
|
63
|
+
- Point of Control (POC): Price level with the highest traded volume.
|
|
64
|
+
- Value Area High (VAH): Upper bound of the value area.
|
|
65
|
+
- Value Area Low (VAL): Lower bound of the value area.
|
|
66
|
+
- Current Price: Latest bid price from MetaTrader 5.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
symbol (str): Market symbol (e.g., "AAPL", "EURUSD").
|
|
70
|
+
path (str): Path to the historical data see ``bbstrader.metatrader.account.check_mt5_connection()``.
|
|
71
|
+
timeframe (str, optional): Timeframe for each candle (default is "1m").
|
|
72
|
+
bars (int, optional): Number of historical bars to fetch (default is 1440).
|
|
73
|
+
bins (int, optional): Number of price bins for volume profile calculation (default is 100).
|
|
74
|
+
va_percentage (float, optional): Percentage of total volume to define the value area (default is 0.7).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
None: Displays a matplotlib chart of the volume profile.
|
|
78
|
+
"""
|
|
79
|
+
df = _get_data(path, symbol, TIMEFRAMES[timeframe], bars)
|
|
80
|
+
hist, bin_edges, bin_centers = volume_profile(df, bins)
|
|
81
|
+
poc, vah, val = value_area(hist, bin_centers, va_percentage)
|
|
82
|
+
current_price = mt5.symbol_info_tick(symbol).bid
|
|
83
|
+
|
|
84
|
+
plt.figure(figsize=(6, 10))
|
|
85
|
+
plt.barh(bin_centers, hist, height=bin_centers[1] - bin_centers[0], color="skyblue")
|
|
86
|
+
plt.axhline(poc, color="red", linestyle="--", label=f"POC: {poc:.5f}")
|
|
87
|
+
plt.axhline(vah, color="green", linestyle="--", label=f"VAH: {vah:.5f}")
|
|
88
|
+
plt.axhline(val, color="orange", linestyle="--", label=f"VAL: {val:.5f}")
|
|
89
|
+
plt.axhline(
|
|
90
|
+
current_price, color="black", linestyle=":", label=f"Price: {current_price:.5f}"
|
|
91
|
+
)
|
|
92
|
+
plt.legend()
|
|
93
|
+
plt.title("Volume Profile")
|
|
94
|
+
plt.xlabel("Volume")
|
|
95
|
+
plt.ylabel("Price")
|
|
96
|
+
plt.grid(True)
|
|
97
|
+
plt.tight_layout()
|
|
98
|
+
plt.show()
|
bbstrader/metatrader/copier.py
CHANGED
|
@@ -302,16 +302,11 @@ class TradeCopier(object):
|
|
|
302
302
|
if self.start_time is None or self.end_time is None:
|
|
303
303
|
return True
|
|
304
304
|
else:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
now = datetime.now()
|
|
306
|
+
start_time = datetime.strptime(self.start_time, "%H:%M").time()
|
|
307
|
+
end_time = datetime.strptime(self.end_time, "%H:%M").time()
|
|
308
|
+
if start_time <= now.time() <= end_time:
|
|
308
309
|
return True
|
|
309
|
-
elif datetime.now().hour == int(start_hour):
|
|
310
|
-
if datetime.now().minute >= int(start_minutes):
|
|
311
|
-
return True
|
|
312
|
-
elif datetime.now().hour == int(end_hour):
|
|
313
|
-
if datetime.now().minute < int(end_minutes):
|
|
314
|
-
return True
|
|
315
310
|
return False
|
|
316
311
|
|
|
317
312
|
def copy_new_trade(
|
|
@@ -491,12 +486,7 @@ class TradeCopier(object):
|
|
|
491
486
|
)
|
|
492
487
|
return source_orders, dest_orders
|
|
493
488
|
|
|
494
|
-
def
|
|
495
|
-
assert destination.get("copy", False), "Destination account not set to copy"
|
|
496
|
-
what = destination.get("copy_what", "all")
|
|
497
|
-
if what not in ["all", "orders"]:
|
|
498
|
-
return
|
|
499
|
-
check_mt5_connection(**destination)
|
|
489
|
+
def _copy_new_orders(self, destination):
|
|
500
490
|
source_orders, destination_orders = self.get_orders(destination)
|
|
501
491
|
# Check for new orders
|
|
502
492
|
dest_ids = [order.magic for order in destination_orders]
|
|
@@ -505,6 +495,7 @@ class TradeCopier(object):
|
|
|
505
495
|
if not self.slippage(source_order, destination):
|
|
506
496
|
self.copy_new_order(source_order, destination)
|
|
507
497
|
|
|
498
|
+
def _copy_modified_orders(self, destination):
|
|
508
499
|
# Check for modified orders
|
|
509
500
|
source_orders, destination_orders = self.get_orders(destination)
|
|
510
501
|
for source_order in source_orders:
|
|
@@ -514,6 +505,8 @@ class TradeCopier(object):
|
|
|
514
505
|
ticket = destination_order.ticket
|
|
515
506
|
symbol = destination_order.symbol
|
|
516
507
|
self.modify_order(ticket, symbol, source_order, destination)
|
|
508
|
+
|
|
509
|
+
def _copy_closed_orders(self, destination):
|
|
517
510
|
# Check for closed orders
|
|
518
511
|
source_orders, destination_orders = self.get_orders(destination)
|
|
519
512
|
source_ids = [order.ticket for order in source_orders]
|
|
@@ -524,8 +517,8 @@ class TradeCopier(object):
|
|
|
524
517
|
)
|
|
525
518
|
self.remove_order(src_symbol, destination_order, destination)
|
|
526
519
|
|
|
527
|
-
|
|
528
|
-
#
|
|
520
|
+
def _sync_positions(self, what, destination):
|
|
521
|
+
# Update postions
|
|
529
522
|
source_positions, _ = self.get_positions(destination)
|
|
530
523
|
_, destination_orders = self.get_orders(destination)
|
|
531
524
|
for source_position in source_positions:
|
|
@@ -538,6 +531,8 @@ class TradeCopier(object):
|
|
|
538
531
|
if not self.slippage(source_position, destination):
|
|
539
532
|
self.copy_new_position(source_position, destination)
|
|
540
533
|
|
|
534
|
+
def _sync_orders(self, destination):
|
|
535
|
+
# Update orders
|
|
541
536
|
_, destination_positions = self.get_positions(destination)
|
|
542
537
|
source_orders, _ = self.get_orders(destination)
|
|
543
538
|
for destination_position in destination_positions:
|
|
@@ -548,16 +543,25 @@ class TradeCopier(object):
|
|
|
548
543
|
)
|
|
549
544
|
if not self.slippage(source_order, destination):
|
|
550
545
|
self.copy_new_order(source_order, destination)
|
|
551
|
-
Mt5.shutdown()
|
|
552
546
|
|
|
553
|
-
def
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
547
|
+
def _copy_what(self, destination):
|
|
548
|
+
if not destination.get("copy", False):
|
|
549
|
+
raise ValueError("Destination account not set to copy mode")
|
|
550
|
+
return destination.get("copy_what", "all")
|
|
551
|
+
|
|
552
|
+
def copy_orders(self, destination: dict):
|
|
553
|
+
what = self._copy_what(destination)
|
|
554
|
+
if what not in ["all", "orders"]:
|
|
557
555
|
return
|
|
558
556
|
check_mt5_connection(**destination)
|
|
559
|
-
|
|
557
|
+
self._copy_new_orders(destination)
|
|
558
|
+
self._copy_modified_orders(destination)
|
|
559
|
+
self._copy_closed_orders(destination)
|
|
560
|
+
self._sync_positions(what, destination)
|
|
561
|
+
self._sync_orders(destination)
|
|
560
562
|
|
|
563
|
+
def _copy_new_positions(self, destination):
|
|
564
|
+
source_positions, destination_positions = self.get_positions(destination)
|
|
561
565
|
# Check for new positions
|
|
562
566
|
dest_ids = [pos.magic for pos in destination_positions]
|
|
563
567
|
for source_position in source_positions:
|
|
@@ -565,6 +569,7 @@ class TradeCopier(object):
|
|
|
565
569
|
if not self.slippage(source_position, destination):
|
|
566
570
|
self.copy_new_position(source_position, destination)
|
|
567
571
|
|
|
572
|
+
def _copy_modified_positions(self, destination):
|
|
568
573
|
# Check for modified positions
|
|
569
574
|
source_positions, destination_positions = self.get_positions(destination)
|
|
570
575
|
for source_position in source_positions:
|
|
@@ -576,6 +581,8 @@ class TradeCopier(object):
|
|
|
576
581
|
self.modify_position(
|
|
577
582
|
ticket, symbol, source_position, destination
|
|
578
583
|
)
|
|
584
|
+
|
|
585
|
+
def _copy_closed_position(self, destination):
|
|
579
586
|
# Check for closed positions
|
|
580
587
|
source_positions, destination_positions = self.get_positions(destination)
|
|
581
588
|
source_ids = [pos.ticket for pos in source_positions]
|
|
@@ -585,7 +592,15 @@ class TradeCopier(object):
|
|
|
585
592
|
destination_position.symbol, destination, type="source"
|
|
586
593
|
)
|
|
587
594
|
self.remove_position(src_symbol, destination_position, destination)
|
|
588
|
-
|
|
595
|
+
|
|
596
|
+
def copy_positions(self, destination: dict):
|
|
597
|
+
what = self._copy_what(destination)
|
|
598
|
+
if what not in ["all", "positions"]:
|
|
599
|
+
return
|
|
600
|
+
check_mt5_connection(**destination)
|
|
601
|
+
self._copy_new_positions(destination)
|
|
602
|
+
self._copy_modified_positions(destination)
|
|
603
|
+
self._copy_closed_position(destination)
|
|
589
604
|
|
|
590
605
|
def log_error(self, e, symbol=None):
|
|
591
606
|
error_msg = repr(e)
|
|
@@ -607,11 +622,13 @@ class TradeCopier(object):
|
|
|
607
622
|
continue
|
|
608
623
|
self.copy_orders(destination)
|
|
609
624
|
self.copy_positions(destination)
|
|
625
|
+
Mt5.shutdown()
|
|
610
626
|
time.sleep(0.1)
|
|
611
|
-
except Exception as e:
|
|
612
|
-
self.log_error(e)
|
|
613
627
|
except KeyboardInterrupt:
|
|
628
|
+
logger.info("Stopping the Trade Copier ...")
|
|
614
629
|
break
|
|
630
|
+
except Exception as e:
|
|
631
|
+
self.log_error(e)
|
|
615
632
|
time.sleep(self.sleeptime)
|
|
616
633
|
|
|
617
634
|
|
|
@@ -661,6 +678,39 @@ def RunMultipleCopier(
|
|
|
661
678
|
process.join()
|
|
662
679
|
|
|
663
680
|
|
|
681
|
+
def _strtodict(string: str) -> dict:
|
|
682
|
+
string = string.strip().replace("\n", "").replace(" ", "").replace('"""', "")
|
|
683
|
+
return dict(item.split(":") for item in string.split(","))
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _parse_symbols(section):
|
|
687
|
+
symbols: str = section.get("symbols")
|
|
688
|
+
symbols = symbols.strip().replace("\n", " ").replace('"""', "")
|
|
689
|
+
if symbols in ["all", "*"]:
|
|
690
|
+
section["symbols"] = symbols
|
|
691
|
+
elif ":" in symbols:
|
|
692
|
+
symbols = _strtodict(symbols)
|
|
693
|
+
section["symbols"] = symbols
|
|
694
|
+
elif " " in symbols and "," not in symbols:
|
|
695
|
+
symbols = symbols.split()
|
|
696
|
+
section["symbols"] = symbols
|
|
697
|
+
elif "," in symbols:
|
|
698
|
+
symbols = symbols.replace(" ", "").split(",")
|
|
699
|
+
section["symbols"] = symbols
|
|
700
|
+
else:
|
|
701
|
+
raise ValueError("""
|
|
702
|
+
Invalid symbols format.
|
|
703
|
+
You can use space or comma separated symbols in one line or multiple lines using triple quotes.
|
|
704
|
+
You can also use a dictionary to map source symbols to destination symbols as shown below.
|
|
705
|
+
Or if you want to copy all symbols, use "all" or "*".
|
|
706
|
+
|
|
707
|
+
symbols = EURUSD, GBPUSD, USDJPY (space separated)
|
|
708
|
+
symbols = EURUSD,GBPUSD,USDJPY (comma separated)
|
|
709
|
+
symbols = EURUSD.s:EURUSD_i, GBPUSD.s:GBPUSD_i, USDJPY.s:USDJPY_i (dictionary)
|
|
710
|
+
symbols = all (copy all symbols)
|
|
711
|
+
symbols = * (copy all symbols) """)
|
|
712
|
+
|
|
713
|
+
|
|
664
714
|
def config_copier(
|
|
665
715
|
source_section: str = None,
|
|
666
716
|
dest_sections: str | List[str] = None,
|
|
@@ -686,10 +736,6 @@ def config_copier(
|
|
|
686
736
|
"""
|
|
687
737
|
from bbstrader.core.utils import dict_from_ini
|
|
688
738
|
|
|
689
|
-
def strtodict(string: str) -> dict:
|
|
690
|
-
string = string.strip().replace("\n", "").replace(" ", "").replace('"""', "")
|
|
691
|
-
return dict(item.split(":") for item in string.split(","))
|
|
692
|
-
|
|
693
739
|
if not inifile:
|
|
694
740
|
inifile = Path().home() / ".bbstrader" / "copier" / "copier.ini"
|
|
695
741
|
if not inifile.exists() or not inifile.is_file():
|
|
@@ -719,33 +765,7 @@ def config_copier(
|
|
|
719
765
|
raise ValueError(
|
|
720
766
|
f"Destination section {dest_section} not found in {inifile}"
|
|
721
767
|
)
|
|
722
|
-
|
|
723
|
-
symbols = symbols.strip().replace("\n", " ").replace('"""', "")
|
|
724
|
-
if symbols in ["all", "*"]:
|
|
725
|
-
section["symbols"] = symbols
|
|
726
|
-
elif ":" in symbols:
|
|
727
|
-
symbols = strtodict(symbols)
|
|
728
|
-
section["symbols"] = symbols
|
|
729
|
-
elif " " in symbols and "," not in symbols:
|
|
730
|
-
symbols = symbols.split()
|
|
731
|
-
section["symbols"] = symbols
|
|
732
|
-
elif "," in symbols:
|
|
733
|
-
symbols = symbols.replace(" ", "").split(",")
|
|
734
|
-
section["symbols"] = symbols
|
|
735
|
-
else:
|
|
736
|
-
err_msg = """
|
|
737
|
-
Invalid symbols format.
|
|
738
|
-
You can use space or comma separated symbols in one line or multiple lines using triple quotes.
|
|
739
|
-
You can also use a dictionary to map source symbols to destination symbols as shown below.
|
|
740
|
-
Or if you want to copy all symbols, use "all" or "*".
|
|
741
|
-
|
|
742
|
-
symbols = EURUSD, GBPUSD, USDJPY (space separated)
|
|
743
|
-
symbols = EURUSD,GBPUSD,USDJPY (comma separated)
|
|
744
|
-
symbols = EURUSD.s:EURUSD_i, GBPUSD.s:GBPUSD_i, USDJPY.s:USDJPY_i (dictionary)
|
|
745
|
-
symbols = all (copy all symbols)
|
|
746
|
-
symbols = * (copy all symbols) """
|
|
747
|
-
raise ValueError(err_msg)
|
|
748
|
-
|
|
768
|
+
_parse_symbols(section)
|
|
749
769
|
destinations.append(section)
|
|
750
770
|
|
|
751
771
|
return source, destinations
|