bbstrader 0.2.98__tar.gz → 0.2.99__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 bbstrader might be problematic. Click here for more details.
- {bbstrader-0.2.98/bbstrader.egg-info → bbstrader-0.2.99}/PKG-INFO +1 -1
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/backtest.py +7 -7
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/event.py +12 -4
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/execution.py +3 -3
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/portfolio.py +3 -3
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/strategy.py +8 -2
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/account.py +1 -1
- bbstrader-0.2.99/bbstrader/metatrader/analysis.py +98 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/copier.py +71 -46
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/trade.py +46 -54
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/factors.py +97 -97
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/execution.py +144 -157
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/strategies.py +13 -5
- {bbstrader-0.2.98 → bbstrader-0.2.99/bbstrader.egg-info}/PKG-INFO +1 -1
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/SOURCES.txt +1 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/setup.py +1 -1
- {bbstrader-0.2.98 → bbstrader-0.2.99}/LICENSE +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/MANIFEST.in +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/README.md +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/__ini__.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/__main__.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/__init__.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/data.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/performance.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/scripts.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/compat.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/config.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/core/__init__.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/core/data.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/core/utils.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/ibkr/__init__.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/ibkr/utils.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/__init__.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/rates.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/risk.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/scripts.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/utils.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/__init__.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/ml.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/nlp.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/optimization.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/portfolio.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/risk.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/__init__.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/scripts.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/utils.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/tseries.py +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/entry_points.txt +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/requires.txt +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/requirements.txt +0 -0
- {bbstrader-0.2.98 → bbstrader-0.2.99}/setup.cfg +0 -0
|
@@ -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")
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -10,7 +10,7 @@ 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
15
|
from bbstrader.metatrader.account import (
|
|
16
16
|
Account,
|
|
@@ -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":
|
|
@@ -678,6 +678,12 @@ class MT5Strategy(Strategy):
|
|
|
678
678
|
if period_count == 0 or period_count is None:
|
|
679
679
|
return True
|
|
680
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
|
|
681
687
|
|
|
682
688
|
def ispositions(
|
|
683
689
|
self, symbol, strategy_id, position, max_trades, one_true=False, account=None
|
|
@@ -1059,7 +1059,7 @@ class Account(object):
|
|
|
1059
1059
|
if book is None:
|
|
1060
1060
|
return None
|
|
1061
1061
|
else:
|
|
1062
|
-
return
|
|
1062
|
+
return tuple([BookInfo(**entry._asdict()) for entry in book])
|
|
1063
1063
|
except Exception as e:
|
|
1064
1064
|
raise_mt5_error(e)
|
|
1065
1065
|
|
|
@@ -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()
|
|
@@ -486,12 +486,7 @@ class TradeCopier(object):
|
|
|
486
486
|
)
|
|
487
487
|
return source_orders, dest_orders
|
|
488
488
|
|
|
489
|
-
def
|
|
490
|
-
assert destination.get("copy", False), "Destination account not set to copy"
|
|
491
|
-
what = destination.get("copy_what", "all")
|
|
492
|
-
if what not in ["all", "orders"]:
|
|
493
|
-
return
|
|
494
|
-
check_mt5_connection(**destination)
|
|
489
|
+
def _copy_new_orders(self, destination):
|
|
495
490
|
source_orders, destination_orders = self.get_orders(destination)
|
|
496
491
|
# Check for new orders
|
|
497
492
|
dest_ids = [order.magic for order in destination_orders]
|
|
@@ -500,6 +495,7 @@ class TradeCopier(object):
|
|
|
500
495
|
if not self.slippage(source_order, destination):
|
|
501
496
|
self.copy_new_order(source_order, destination)
|
|
502
497
|
|
|
498
|
+
def _copy_modified_orders(self, destination):
|
|
503
499
|
# Check for modified orders
|
|
504
500
|
source_orders, destination_orders = self.get_orders(destination)
|
|
505
501
|
for source_order in source_orders:
|
|
@@ -509,6 +505,8 @@ class TradeCopier(object):
|
|
|
509
505
|
ticket = destination_order.ticket
|
|
510
506
|
symbol = destination_order.symbol
|
|
511
507
|
self.modify_order(ticket, symbol, source_order, destination)
|
|
508
|
+
|
|
509
|
+
def _copy_closed_orders(self, destination):
|
|
512
510
|
# Check for closed orders
|
|
513
511
|
source_orders, destination_orders = self.get_orders(destination)
|
|
514
512
|
source_ids = [order.ticket for order in source_orders]
|
|
@@ -519,8 +517,8 @@ class TradeCopier(object):
|
|
|
519
517
|
)
|
|
520
518
|
self.remove_order(src_symbol, destination_order, destination)
|
|
521
519
|
|
|
522
|
-
|
|
523
|
-
#
|
|
520
|
+
def _sync_positions(self, what, destination):
|
|
521
|
+
# Update postions
|
|
524
522
|
source_positions, _ = self.get_positions(destination)
|
|
525
523
|
_, destination_orders = self.get_orders(destination)
|
|
526
524
|
for source_position in source_positions:
|
|
@@ -533,6 +531,8 @@ class TradeCopier(object):
|
|
|
533
531
|
if not self.slippage(source_position, destination):
|
|
534
532
|
self.copy_new_position(source_position, destination)
|
|
535
533
|
|
|
534
|
+
def _sync_orders(self, destination):
|
|
535
|
+
# Update orders
|
|
536
536
|
_, destination_positions = self.get_positions(destination)
|
|
537
537
|
source_orders, _ = self.get_orders(destination)
|
|
538
538
|
for destination_position in destination_positions:
|
|
@@ -543,16 +543,25 @@ class TradeCopier(object):
|
|
|
543
543
|
)
|
|
544
544
|
if not self.slippage(source_order, destination):
|
|
545
545
|
self.copy_new_order(source_order, destination)
|
|
546
|
-
Mt5.shutdown()
|
|
547
546
|
|
|
548
|
-
def
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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"]:
|
|
552
555
|
return
|
|
553
556
|
check_mt5_connection(**destination)
|
|
554
|
-
|
|
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)
|
|
555
562
|
|
|
563
|
+
def _copy_new_positions(self, destination):
|
|
564
|
+
source_positions, destination_positions = self.get_positions(destination)
|
|
556
565
|
# Check for new positions
|
|
557
566
|
dest_ids = [pos.magic for pos in destination_positions]
|
|
558
567
|
for source_position in source_positions:
|
|
@@ -560,6 +569,7 @@ class TradeCopier(object):
|
|
|
560
569
|
if not self.slippage(source_position, destination):
|
|
561
570
|
self.copy_new_position(source_position, destination)
|
|
562
571
|
|
|
572
|
+
def _copy_modified_positions(self, destination):
|
|
563
573
|
# Check for modified positions
|
|
564
574
|
source_positions, destination_positions = self.get_positions(destination)
|
|
565
575
|
for source_position in source_positions:
|
|
@@ -571,6 +581,8 @@ class TradeCopier(object):
|
|
|
571
581
|
self.modify_position(
|
|
572
582
|
ticket, symbol, source_position, destination
|
|
573
583
|
)
|
|
584
|
+
|
|
585
|
+
def _copy_closed_position(self, destination):
|
|
574
586
|
# Check for closed positions
|
|
575
587
|
source_positions, destination_positions = self.get_positions(destination)
|
|
576
588
|
source_ids = [pos.ticket for pos in source_positions]
|
|
@@ -580,7 +592,15 @@ class TradeCopier(object):
|
|
|
580
592
|
destination_position.symbol, destination, type="source"
|
|
581
593
|
)
|
|
582
594
|
self.remove_position(src_symbol, destination_position, destination)
|
|
583
|
-
|
|
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)
|
|
584
604
|
|
|
585
605
|
def log_error(self, e, symbol=None):
|
|
586
606
|
error_msg = repr(e)
|
|
@@ -602,8 +622,10 @@ class TradeCopier(object):
|
|
|
602
622
|
continue
|
|
603
623
|
self.copy_orders(destination)
|
|
604
624
|
self.copy_positions(destination)
|
|
625
|
+
Mt5.shutdown()
|
|
605
626
|
time.sleep(0.1)
|
|
606
627
|
except KeyboardInterrupt:
|
|
628
|
+
logger.info("Stopping the Trade Copier ...")
|
|
607
629
|
break
|
|
608
630
|
except Exception as e:
|
|
609
631
|
self.log_error(e)
|
|
@@ -656,6 +678,39 @@ def RunMultipleCopier(
|
|
|
656
678
|
process.join()
|
|
657
679
|
|
|
658
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
|
+
|
|
659
714
|
def config_copier(
|
|
660
715
|
source_section: str = None,
|
|
661
716
|
dest_sections: str | List[str] = None,
|
|
@@ -681,10 +736,6 @@ def config_copier(
|
|
|
681
736
|
"""
|
|
682
737
|
from bbstrader.core.utils import dict_from_ini
|
|
683
738
|
|
|
684
|
-
def strtodict(string: str) -> dict:
|
|
685
|
-
string = string.strip().replace("\n", "").replace(" ", "").replace('"""', "")
|
|
686
|
-
return dict(item.split(":") for item in string.split(","))
|
|
687
|
-
|
|
688
739
|
if not inifile:
|
|
689
740
|
inifile = Path().home() / ".bbstrader" / "copier" / "copier.ini"
|
|
690
741
|
if not inifile.exists() or not inifile.is_file():
|
|
@@ -714,33 +765,7 @@ def config_copier(
|
|
|
714
765
|
raise ValueError(
|
|
715
766
|
f"Destination section {dest_section} not found in {inifile}"
|
|
716
767
|
)
|
|
717
|
-
|
|
718
|
-
symbols = symbols.strip().replace("\n", " ").replace('"""', "")
|
|
719
|
-
if symbols in ["all", "*"]:
|
|
720
|
-
section["symbols"] = symbols
|
|
721
|
-
elif ":" in symbols:
|
|
722
|
-
symbols = strtodict(symbols)
|
|
723
|
-
section["symbols"] = symbols
|
|
724
|
-
elif " " in symbols and "," not in symbols:
|
|
725
|
-
symbols = symbols.split()
|
|
726
|
-
section["symbols"] = symbols
|
|
727
|
-
elif "," in symbols:
|
|
728
|
-
symbols = symbols.replace(" ", "").split(",")
|
|
729
|
-
section["symbols"] = symbols
|
|
730
|
-
else:
|
|
731
|
-
err_msg = """
|
|
732
|
-
Invalid symbols format.
|
|
733
|
-
You can use space or comma separated symbols in one line or multiple lines using triple quotes.
|
|
734
|
-
You can also use a dictionary to map source symbols to destination symbols as shown below.
|
|
735
|
-
Or if you want to copy all symbols, use "all" or "*".
|
|
736
|
-
|
|
737
|
-
symbols = EURUSD, GBPUSD, USDJPY (space separated)
|
|
738
|
-
symbols = EURUSD,GBPUSD,USDJPY (comma separated)
|
|
739
|
-
symbols = EURUSD.s:EURUSD_i, GBPUSD.s:GBPUSD_i, USDJPY.s:USDJPY_i (dictionary)
|
|
740
|
-
symbols = all (copy all symbols)
|
|
741
|
-
symbols = * (copy all symbols) """
|
|
742
|
-
raise ValueError(err_msg)
|
|
743
|
-
|
|
768
|
+
_parse_symbols(section)
|
|
744
769
|
destinations.append(section)
|
|
745
770
|
|
|
746
771
|
return source, destinations
|