bbstrader 0.3.0__tar.gz → 0.3.1__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.3.0/bbstrader.egg-info → bbstrader-0.3.1}/PKG-INFO +2 -5
- {bbstrader-0.3.0 → bbstrader-0.3.1}/README.md +1 -4
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/__init__.py +1 -1
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/__main__.py +17 -13
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/core/data.py +92 -29
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/account.py +4 -10
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/copier.py +112 -36
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/scripts.py +26 -12
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/trade.py +27 -32
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/utils.py +1 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/models/nlp.py +117 -74
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/trading/execution.py +58 -37
- {bbstrader-0.3.0 → bbstrader-0.3.1/bbstrader.egg-info}/PKG-INFO +2 -5
- {bbstrader-0.3.0 → bbstrader-0.3.1}/setup.py +1 -2
- {bbstrader-0.3.0 → bbstrader-0.3.1}/LICENSE +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/MANIFEST.in +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/__init__.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/backtest.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/data.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/event.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/execution.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/performance.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/portfolio.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/scripts.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/btengine/strategy.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/compat.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/config.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/core/__init__.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/core/scripts.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/core/utils.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/ibkr/__init__.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/ibkr/utils.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/__init__.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/analysis.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/rates.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/metatrader/risk.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/models/__init__.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/models/factors.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/models/ml.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/models/optimization.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/models/portfolio.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/models/risk.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/trading/__init__.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/trading/scripts.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/trading/strategies.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/trading/utils.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader/tseries.py +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader.egg-info/SOURCES.txt +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader.egg-info/entry_points.txt +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader.egg-info/requires.txt +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/requirements.txt +0 -0
- {bbstrader-0.3.0 → bbstrader-0.3.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Simplified Investment & Trading Toolkit
|
|
5
5
|
Home-page: https://github.com/bbalouki/bbstrader
|
|
6
6
|
Download-URL: https://pypi.org/project/bbstrader/
|
|
@@ -172,10 +172,7 @@ To begin using `bbstrader`, please ensure your system meets the following prereq
|
|
|
172
172
|
* [Admirals Group AS](https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets) (for Stocks, ETFs, Indices, Commodities, Futures, Forex)
|
|
173
173
|
* [Just Global Markets Ltd.](https://one.justmarkets.link/a/tufvj0xugm/registration/trader) (for Stocks, Crypto, Indices, Commodities, Forex)
|
|
174
174
|
* [FTMO](https://trader.ftmo.com/?affiliates=JGmeuQqepAZLMcdOEQRp) (Proprietary Firm)
|
|
175
|
-
|
|
176
|
-
* Interactive Brokers Trader Workstation (TWS) or IB Gateway must be installed and running.
|
|
177
|
-
* An active account with Interactive Brokers.
|
|
178
|
-
* The Python client library for the IBKR API: `ibapi`.
|
|
175
|
+
|
|
179
176
|
|
|
180
177
|
### Installation
|
|
181
178
|
|
|
@@ -88,10 +88,7 @@ To begin using `bbstrader`, please ensure your system meets the following prereq
|
|
|
88
88
|
* [Admirals Group AS](https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets) (for Stocks, ETFs, Indices, Commodities, Futures, Forex)
|
|
89
89
|
* [Just Global Markets Ltd.](https://one.justmarkets.link/a/tufvj0xugm/registration/trader) (for Stocks, Crypto, Indices, Commodities, Forex)
|
|
90
90
|
* [FTMO](https://trader.ftmo.com/?affiliates=JGmeuQqepAZLMcdOEQRp) (Proprietary Firm)
|
|
91
|
-
|
|
92
|
-
* Interactive Brokers Trader Workstation (TWS) or IB Gateway must be installed and running.
|
|
93
|
-
* An active account with Interactive Brokers.
|
|
94
|
-
* The Python client library for the IBKR API: `ibapi`.
|
|
91
|
+
|
|
95
92
|
|
|
96
93
|
### Installation
|
|
97
94
|
|
|
@@ -7,7 +7,7 @@ __author__ = "Bertin Balouki SIMYELI"
|
|
|
7
7
|
__copyright__ = "2023-2025 Bertin Balouki SIMYELI"
|
|
8
8
|
__email__ = "bertin@bbstrader.com"
|
|
9
9
|
__license__ = "MIT"
|
|
10
|
-
__version__ = "0.
|
|
10
|
+
__version__ = "0.3.1"
|
|
11
11
|
|
|
12
12
|
from bbstrader import compat # noqa: F401
|
|
13
13
|
from bbstrader import core # noqa: F401
|
|
@@ -49,19 +49,23 @@ def main():
|
|
|
49
49
|
if ("-h" in sys.argv or "--help" in sys.argv) and args.run is None:
|
|
50
50
|
print(Fore.WHITE + USAGE_TEXT)
|
|
51
51
|
sys.exit(0)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
try:
|
|
53
|
+
match args.run:
|
|
54
|
+
case Module.COPIER.value:
|
|
55
|
+
copy_trades(unknown)
|
|
56
|
+
case Module.BACKTEST.value:
|
|
57
|
+
backtest(unknown)
|
|
58
|
+
case Module.EXECUTION.value:
|
|
59
|
+
execute_strategy(unknown)
|
|
60
|
+
case Module.NEWS_FEED.value:
|
|
61
|
+
send_news_feed(unknown)
|
|
62
|
+
case _:
|
|
63
|
+
print(Fore.RED + f"Unknown module: {args.run}")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
except Exception:
|
|
68
|
+
sys.exit(1)
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
if __name__ == "__main__":
|
|
@@ -149,7 +149,7 @@ class FmpNews(object):
|
|
|
149
149
|
try:
|
|
150
150
|
articles = pd.read_csv("latest_fmp_articles.csv")
|
|
151
151
|
articles = articles.to_dict(orient="records")
|
|
152
|
-
if self._last_date(articles[0]["date"]) < end_date:
|
|
152
|
+
if self._last_date(articles[0]["date"]).hour < end_date.hour:
|
|
153
153
|
articles = self.get_articles(**kwargs)
|
|
154
154
|
else:
|
|
155
155
|
return articles
|
|
@@ -161,7 +161,9 @@ class FmpNews(object):
|
|
|
161
161
|
df.to_csv("latest_fmp_articles.csv", index=False)
|
|
162
162
|
return articles
|
|
163
163
|
|
|
164
|
-
def get_news(
|
|
164
|
+
def get_news(
|
|
165
|
+
self, query, source="articles", articles=None, symbol: str = None, **kwargs
|
|
166
|
+
):
|
|
165
167
|
"""
|
|
166
168
|
Retrieves relevant financial news based on the specified source.
|
|
167
169
|
|
|
@@ -183,6 +185,10 @@ class FmpNews(object):
|
|
|
183
185
|
Returns an empty list if no relevant news is found.
|
|
184
186
|
"""
|
|
185
187
|
query = _get_search_query(query)
|
|
188
|
+
if symbol is not None:
|
|
189
|
+
symbol = symbol.replace("-", "").split("=")[
|
|
190
|
+
0
|
|
191
|
+
] # if symbol is a yahoo finance ticker
|
|
186
192
|
source_methods = {
|
|
187
193
|
"articles": lambda: self.get_latest_articles(articles=articles, save=True),
|
|
188
194
|
"releases": lambda: self.get_releases(symbol=symbol, **kwargs),
|
|
@@ -191,6 +197,8 @@ class FmpNews(object):
|
|
|
191
197
|
"forex": lambda: self.get_forex_news(symbol=symbol, **kwargs),
|
|
192
198
|
}
|
|
193
199
|
news_source = source_methods.get(source, lambda: [])()
|
|
200
|
+
if source == "articles":
|
|
201
|
+
symbol = None # Articles do not require a symbol filter
|
|
194
202
|
news = self.parse_news(news_source, symbol=symbol)
|
|
195
203
|
return _filter_news(news, query)
|
|
196
204
|
|
|
@@ -203,7 +211,7 @@ class FinancialNews(object):
|
|
|
203
211
|
|
|
204
212
|
"""
|
|
205
213
|
|
|
206
|
-
def _fetch_news(self, url, query,
|
|
214
|
+
def _fetch_news(self, url, query, n_news, headline_tag) -> List[str]:
|
|
207
215
|
headers = {"User-Agent": "Mozilla/5.0"}
|
|
208
216
|
try:
|
|
209
217
|
response = requests.get(url, headers=headers)
|
|
@@ -224,32 +232,60 @@ class FinancialNews(object):
|
|
|
224
232
|
]
|
|
225
233
|
return headlines[:n_news]
|
|
226
234
|
|
|
227
|
-
def get_yahoo_finance_news(self, query, asset_type="stock", n_news=10):
|
|
235
|
+
def get_yahoo_finance_news(self, query: str, asset_type="stock", n_news=10):
|
|
228
236
|
"""
|
|
229
237
|
Fetches recent Yahoo Finance news headlines for a given financial asset.
|
|
230
238
|
|
|
231
239
|
Args:
|
|
232
240
|
query (str): The asset symbol or name (e.g., "AAPL").
|
|
233
|
-
asset_type (str, optional): The type of asset (e.g., "stock", "etf"). Defaults to "stock"
|
|
241
|
+
asset_type (str, optional): The type of asset (e.g., "stock", "etf"). Defaults to "stock",
|
|
242
|
+
supported types include:
|
|
243
|
+
- "stock": Stock symbols (e.g., AAPL, MSFT)
|
|
244
|
+
- "etf": Exchange-traded funds (e.g., SPY, QQQ)
|
|
245
|
+
- "future": Futures contracts (e.g., CL=F for crude oil)
|
|
246
|
+
- "forex": Forex pairs (e.g., EURUSD=X, USDJPY=X)
|
|
247
|
+
- "crypto": Cryptocurrency pairs (e.g., BTC-USD, ETH-USD)
|
|
248
|
+
- "index": Stock market indices (e.g., ^GSPC for S&P 500)
|
|
234
249
|
n_news (int, optional): The number of news headlines to return. Defaults to 10.
|
|
235
250
|
|
|
251
|
+
Note:
|
|
252
|
+
For commotities and bonds, use the "Future" asset type.
|
|
253
|
+
|
|
236
254
|
Returns:
|
|
237
255
|
list[str]: A list of Yahoo Finance news headlines relevant to the query.
|
|
238
256
|
"""
|
|
257
|
+
if asset_type == "forex" or asset_type == "future":
|
|
258
|
+
assert (
|
|
259
|
+
"=" in query
|
|
260
|
+
), "Forex query must contain '=' for currency pairs (e.g., EURUSD=X, CL=F)"
|
|
261
|
+
if asset_type == "crypto":
|
|
262
|
+
assert (
|
|
263
|
+
"-" in query
|
|
264
|
+
), "Crypto query must contain '-' for crypto pairs (e.g., BTC-USD, ETH-USD)"
|
|
265
|
+
if asset_type == "index":
|
|
266
|
+
assert query.startswith(
|
|
267
|
+
"^"
|
|
268
|
+
), "Index query must start with '^' (e.g., ^GSPC for S&P 500)"
|
|
239
269
|
url = (
|
|
240
270
|
f"https://finance.yahoo.com/quote/{query}/news"
|
|
241
|
-
if asset_type in ["stock", "etf"]
|
|
271
|
+
if asset_type in ["stock", "etf", "index", "future", "forex"]
|
|
242
272
|
else "https://finance.yahoo.com/news"
|
|
243
273
|
)
|
|
244
|
-
return self._fetch_news(url, query,
|
|
274
|
+
return self._fetch_news(url, query, n_news, "h3")
|
|
245
275
|
|
|
246
|
-
def get_google_finance_news(self, query, asset_type="stock", n_news=10):
|
|
276
|
+
def get_google_finance_news(self, query: str, asset_type="stock", n_news=10):
|
|
247
277
|
"""
|
|
248
278
|
Fetches recent Google Finance news headlines for a given financial asset.
|
|
249
279
|
|
|
250
280
|
Args:
|
|
251
281
|
query (str): The asset symbol or name (e.g., "AAPL").
|
|
252
282
|
asset_type (str, optional): The type of asset (e.g., "stock", "crypto"). Defaults to "stock".
|
|
283
|
+
Supported types include:
|
|
284
|
+
- "stock": Stock symbols (e.g., AAPL, MSFT)
|
|
285
|
+
- "etf": Exchange-traded funds (e.g., SPY, QQQ)
|
|
286
|
+
- "future": Futures contracts (e.g., CL=F or crude oil)
|
|
287
|
+
- "forex": Forex pairs (e.g., EURUSD, USDJPY)
|
|
288
|
+
- "crypto": Cryptocurrency pairs (e.g., BTCUSD, ETHUSD)
|
|
253
289
|
n_news (int, optional): The number of news headlines to return. Defaults to 10.
|
|
254
290
|
|
|
255
291
|
Returns:
|
|
@@ -258,20 +294,18 @@ class FinancialNews(object):
|
|
|
258
294
|
search_terms = {
|
|
259
295
|
"stock": f"{query} stock OR {query} shares OR {query} market",
|
|
260
296
|
"etf": f"{query} ETF OR {query} fund OR {query} exchange-traded fund",
|
|
261
|
-
"
|
|
262
|
-
"commodity": f"{query} price OR {query} futures OR {query} market",
|
|
297
|
+
"future": f"{query} futures OR {query} price OR {query} market",
|
|
263
298
|
"forex": f"{query} forex OR {query} exchange rate OR {query} market",
|
|
264
299
|
"crypto": f"{query} cryptocurrency OR {query} price OR {query} market",
|
|
265
|
-
"bond": f"{query} bond OR {query} yield OR {query} interest rate",
|
|
266
300
|
"index": f"{query} index OR {query} stock market OR {query} performance",
|
|
267
301
|
}
|
|
268
302
|
search_query = search_terms.get(asset_type, query)
|
|
269
303
|
url = f"https://news.google.com/search?q={search_query.replace(' ', '+')}"
|
|
270
|
-
return self._fetch_news(url, query,
|
|
304
|
+
return self._fetch_news(url, query, n_news, "a")
|
|
271
305
|
|
|
272
306
|
def get_reddit_posts(
|
|
273
307
|
self,
|
|
274
|
-
symbol,
|
|
308
|
+
symbol: str,
|
|
275
309
|
client_id=None,
|
|
276
310
|
client_secret=None,
|
|
277
311
|
user_agent=None,
|
|
@@ -294,8 +328,8 @@ class FinancialNews(object):
|
|
|
294
328
|
- "stock": Searches in stock-related subreddits (e.g., wallstreetbets, stocks).
|
|
295
329
|
- "forex": Searches in forex-related subreddits.
|
|
296
330
|
- "commodities": Searches in commodity-related subreddits (e.g., gold, oil).
|
|
297
|
-
- "
|
|
298
|
-
- "
|
|
331
|
+
- "etf": Searches in ETF-related subreddits.
|
|
332
|
+
- "future": Searches in futures and options trading subreddits.
|
|
299
333
|
- "crypto": Searches in cryptocurrency-related subreddits.
|
|
300
334
|
- If an unrecognized asset class is provided, defaults to stock-related subreddits.
|
|
301
335
|
n_posts (int, optional): The number of posts to return per subreddit. Defaults to 10.
|
|
@@ -317,15 +351,36 @@ class FinancialNews(object):
|
|
|
317
351
|
"""
|
|
318
352
|
|
|
319
353
|
reddit = praw.Reddit(
|
|
320
|
-
client_id=client_id,
|
|
354
|
+
client_id=client_id,
|
|
355
|
+
client_secret=client_secret,
|
|
356
|
+
user_agent=user_agent,
|
|
357
|
+
check_for_updates=False,
|
|
358
|
+
comment_kind="t1",
|
|
359
|
+
message_kind="t4",
|
|
360
|
+
redditor_kind="t2",
|
|
361
|
+
submission_kind="t3",
|
|
362
|
+
subreddit_kind="t5",
|
|
363
|
+
trophy_kind="t6",
|
|
364
|
+
oauth_url="https://oauth.reddit.com",
|
|
365
|
+
reddit_url="https://www.reddit.com",
|
|
366
|
+
short_url="https://redd.it",
|
|
367
|
+
timeout=16,
|
|
368
|
+
ratelimit_seconds=5,
|
|
321
369
|
)
|
|
322
|
-
|
|
370
|
+
assert reddit.read_only
|
|
323
371
|
subreddit_mapping = {
|
|
324
372
|
"stock": ["wallstreetbets", "stocks", "investing", "StockMarket"],
|
|
325
373
|
"forex": ["Forex", "ForexTrading", "DayTrading"],
|
|
326
|
-
"commodities": ["Commodities", "Gold", "Silverbugs", "oil"],
|
|
327
374
|
"etfs": ["ETFs", "investing"],
|
|
328
|
-
"futures": [
|
|
375
|
+
"futures": [
|
|
376
|
+
"FuturesTrading",
|
|
377
|
+
"OptionsTrading",
|
|
378
|
+
"DayTrading",
|
|
379
|
+
"Commodities",
|
|
380
|
+
"Gold",
|
|
381
|
+
"Silverbugs",
|
|
382
|
+
"oil",
|
|
383
|
+
],
|
|
329
384
|
"crypto": ["CryptoCurrency", "Bitcoin", "ethereum", "altcoin"],
|
|
330
385
|
}
|
|
331
386
|
try:
|
|
@@ -473,16 +528,23 @@ class FinancialNews(object):
|
|
|
473
528
|
maximum = 100
|
|
474
529
|
if limit > maximum:
|
|
475
530
|
raise ValueError(f"Number of total news articles allowed is {maximum}")
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
531
|
+
try:
|
|
532
|
+
response = requests.get(
|
|
533
|
+
"https://data-api.coindesk.com/news/v1/article/list",
|
|
534
|
+
params={"lang": lang, "limit": limit},
|
|
535
|
+
headers={"Content-type": "application/json; charset=UTF-8"},
|
|
536
|
+
)
|
|
537
|
+
response.raise_for_status()
|
|
538
|
+
json_response = response.json()
|
|
539
|
+
except requests.exceptions.RequestException:
|
|
485
540
|
return []
|
|
541
|
+
if (
|
|
542
|
+
response.status_code != 200
|
|
543
|
+
or "Data" not in json_response
|
|
544
|
+
or len(json_response["Data"]) == 0
|
|
545
|
+
):
|
|
546
|
+
return []
|
|
547
|
+
articles = json_response["Data"]
|
|
486
548
|
to_keep = [
|
|
487
549
|
"PUBLISHED_ON",
|
|
488
550
|
"TITLE",
|
|
@@ -495,10 +557,11 @@ class FinancialNews(object):
|
|
|
495
557
|
]
|
|
496
558
|
filtered_articles = []
|
|
497
559
|
for article in articles:
|
|
560
|
+
keys = article.keys()
|
|
498
561
|
filtered_articles.append(
|
|
499
562
|
{
|
|
500
563
|
k.lower(): article[k]
|
|
501
|
-
if k in
|
|
564
|
+
if k in keys and k != "PUBLISHED_ON"
|
|
502
565
|
else datetime.fromtimestamp(article[k])
|
|
503
566
|
for k in to_keep
|
|
504
567
|
if article[k] is not None and "sponsored" not in str(article[k])
|
|
@@ -760,10 +760,7 @@ class Account(object):
|
|
|
760
760
|
This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
|
|
761
761
|
For other brokers use `get_symbols()` or this method will use it by default.
|
|
762
762
|
"""
|
|
763
|
-
if (
|
|
764
|
-
self.broker != AdmiralMarktsGroup()
|
|
765
|
-
or self.broker != PepperstoneGroupLimited()
|
|
766
|
-
):
|
|
763
|
+
if self.broker not in [AdmiralMarktsGroup(), PepperstoneGroupLimited()]:
|
|
767
764
|
return self.get_symbols(symbol_type=SymbolType.FOREX)
|
|
768
765
|
else:
|
|
769
766
|
fx_categories = {
|
|
@@ -816,11 +813,8 @@ class Account(object):
|
|
|
816
813
|
This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
|
|
817
814
|
For other brokers use `get_symbols()` or this method will use it by default.
|
|
818
815
|
"""
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
self.broker != AdmiralMarktsGroup()
|
|
822
|
-
or self.broker != PepperstoneGroupLimited()
|
|
823
|
-
):
|
|
816
|
+
|
|
817
|
+
if self.broker not in [AdmiralMarktsGroup(), PepperstoneGroupLimited()]:
|
|
824
818
|
return self.get_symbols(symbol_type=SymbolType.STOCKS)
|
|
825
819
|
else:
|
|
826
820
|
stocks, etfs = [], []
|
|
@@ -883,7 +877,7 @@ class Account(object):
|
|
|
883
877
|
SymbolType.STOCKS, exchange_code, exchange_map
|
|
884
878
|
)
|
|
885
879
|
etfs = self._get_symbols_by_category(
|
|
886
|
-
|
|
880
|
+
SymbolType.ETFs, exchange_code, exchange_map
|
|
887
881
|
)
|
|
888
882
|
return stocks + etfs if etf else stocks
|
|
889
883
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import multiprocessing
|
|
2
|
+
import threading
|
|
2
3
|
import time
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Dict, List, Literal, Tuple
|
|
6
7
|
|
|
7
|
-
from loguru import logger
|
|
8
|
+
from loguru import logger as log
|
|
8
9
|
|
|
9
10
|
from bbstrader.config import BBSTRADER_DIR
|
|
10
11
|
from bbstrader.metatrader.account import Account, check_mt5_connection
|
|
@@ -20,12 +21,14 @@ except ImportError:
|
|
|
20
21
|
__all__ = ["TradeCopier", "RunCopier", "RunMultipleCopier", "config_copier"]
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
log.add(
|
|
24
25
|
f"{BBSTRADER_DIR}/logs/copier.log",
|
|
25
26
|
enqueue=True,
|
|
26
27
|
level="INFO",
|
|
27
28
|
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
28
29
|
)
|
|
30
|
+
global logger
|
|
31
|
+
logger = log
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
def fix_lot(fixed):
|
|
@@ -113,11 +116,21 @@ def calculate_copy_lot(
|
|
|
113
116
|
raise ValueError("Invalid mode selected")
|
|
114
117
|
|
|
115
118
|
|
|
116
|
-
def get_copy_symbols(destination: dict
|
|
119
|
+
def get_copy_symbols(destination: dict, source: dict):
|
|
117
120
|
symbols = destination.get("symbols", "all")
|
|
118
|
-
|
|
121
|
+
src_account = Account(**source)
|
|
122
|
+
dest_account = Account(**destination)
|
|
119
123
|
if symbols == "all" or symbols == "*":
|
|
120
|
-
|
|
124
|
+
src_symbols = src_account.get_symbols()
|
|
125
|
+
dest_symbols = dest_account.get_symbols()
|
|
126
|
+
for s in src_symbols:
|
|
127
|
+
if s not in dest_symbols:
|
|
128
|
+
err_msg = (
|
|
129
|
+
f"To use 'all' or '*', Source account@{src_account.number} "
|
|
130
|
+
f"and destination account@{dest_account.number} "
|
|
131
|
+
f"must be the same type and have the same symbols"
|
|
132
|
+
)
|
|
133
|
+
raise ValueError(err_msg)
|
|
121
134
|
elif isinstance(symbols, (list, dict)):
|
|
122
135
|
return symbols
|
|
123
136
|
elif isinstance(symbols, str):
|
|
@@ -127,22 +140,6 @@ def get_copy_symbols(destination: dict = None):
|
|
|
127
140
|
return symbols.split()
|
|
128
141
|
|
|
129
142
|
|
|
130
|
-
def get_copy_symbol(symbol, destination: dict = None, type="destination"):
|
|
131
|
-
symbols = get_copy_symbols(destination)
|
|
132
|
-
if isinstance(symbols, list):
|
|
133
|
-
if symbol in symbols:
|
|
134
|
-
return symbol
|
|
135
|
-
if isinstance(symbols, dict):
|
|
136
|
-
if type == "destination":
|
|
137
|
-
if symbol in symbols.keys():
|
|
138
|
-
return symbols[symbol]
|
|
139
|
-
if type == "source":
|
|
140
|
-
for k, v in symbols.items():
|
|
141
|
-
if v == symbol:
|
|
142
|
-
return k
|
|
143
|
-
raise ValueError(f"Symbol {symbol} not found in {type} account")
|
|
144
|
-
|
|
145
|
-
|
|
146
143
|
class TradeCopier(object):
|
|
147
144
|
"""
|
|
148
145
|
``TradeCopier`` responsible for copying trading orders and positions from a source account to multiple destination accounts.
|
|
@@ -159,7 +156,10 @@ class TradeCopier(object):
|
|
|
159
156
|
"sleeptime",
|
|
160
157
|
"start_time",
|
|
161
158
|
"end_time",
|
|
159
|
+
"shutdown_event",
|
|
160
|
+
"custom_logger",
|
|
162
161
|
)
|
|
162
|
+
shutdown_event: threading.Event
|
|
163
163
|
|
|
164
164
|
def __init__(
|
|
165
165
|
self,
|
|
@@ -168,6 +168,8 @@ class TradeCopier(object):
|
|
|
168
168
|
sleeptime: float = 0.1,
|
|
169
169
|
start_time: str = None,
|
|
170
170
|
end_time: str = None,
|
|
171
|
+
shutdown_event=None,
|
|
172
|
+
custom_logger=None,
|
|
171
173
|
):
|
|
172
174
|
"""
|
|
173
175
|
Initializes the ``TradeCopier`` instance, setting up the source and destination trading accounts for trade copying.
|
|
@@ -245,8 +247,15 @@ class TradeCopier(object):
|
|
|
245
247
|
self.sleeptime = sleeptime
|
|
246
248
|
self.start_time = start_time
|
|
247
249
|
self.end_time = end_time
|
|
248
|
-
self.
|
|
250
|
+
self.shutdown_event = shutdown_event
|
|
251
|
+
self._add_logger(custom_logger)
|
|
249
252
|
self._add_copy()
|
|
253
|
+
self.errors = set()
|
|
254
|
+
|
|
255
|
+
def _add_logger(self, custom_logger):
|
|
256
|
+
if custom_logger:
|
|
257
|
+
global logger
|
|
258
|
+
logger = custom_logger
|
|
250
259
|
|
|
251
260
|
def _add_copy(self):
|
|
252
261
|
self.source["copy"] = True
|
|
@@ -269,6 +278,21 @@ class TradeCopier(object):
|
|
|
269
278
|
check_mt5_connection(**destination)
|
|
270
279
|
return Account(**destination).get_positions(symbol=symbol)
|
|
271
280
|
|
|
281
|
+
def get_copy_symbol(self, symbol, destination: dict = None, type="destination"):
|
|
282
|
+
symbols = get_copy_symbols(destination, self.source)
|
|
283
|
+
if isinstance(symbols, list):
|
|
284
|
+
if symbol in symbols:
|
|
285
|
+
return symbol
|
|
286
|
+
if isinstance(symbols, dict):
|
|
287
|
+
if type == "destination":
|
|
288
|
+
if symbol in symbols.keys():
|
|
289
|
+
return symbols[symbol]
|
|
290
|
+
if type == "source":
|
|
291
|
+
for k, v in symbols.items():
|
|
292
|
+
if v == symbol:
|
|
293
|
+
return k
|
|
294
|
+
raise ValueError(f"Symbol {symbol} not found in {type} account")
|
|
295
|
+
|
|
272
296
|
def isorder_modified(self, source: TradeOrder, dest: TradeOrder):
|
|
273
297
|
if source.type == dest.type and source.ticket == dest.magic:
|
|
274
298
|
return (
|
|
@@ -315,7 +339,7 @@ class TradeCopier(object):
|
|
|
315
339
|
return
|
|
316
340
|
check_mt5_connection(**destination)
|
|
317
341
|
volume = trade.volume if hasattr(trade, "volume") else trade.volume_initial
|
|
318
|
-
symbol = get_copy_symbol(trade.symbol, destination)
|
|
342
|
+
symbol = self.get_copy_symbol(trade.symbol, destination)
|
|
319
343
|
lot = calculate_copy_lot(
|
|
320
344
|
volume,
|
|
321
345
|
symbol,
|
|
@@ -326,7 +350,9 @@ class TradeCopier(object):
|
|
|
326
350
|
dest_eqty=Account(**destination).get_account_info().margin_free,
|
|
327
351
|
)
|
|
328
352
|
|
|
329
|
-
trade_instance = Trade(
|
|
353
|
+
trade_instance = Trade(
|
|
354
|
+
symbol=symbol, **destination, max_risk=100.0, logger=None
|
|
355
|
+
)
|
|
330
356
|
try:
|
|
331
357
|
action = action_type[trade.type]
|
|
332
358
|
except KeyError:
|
|
@@ -463,7 +489,7 @@ class TradeCopier(object):
|
|
|
463
489
|
|
|
464
490
|
def get_positions(self, destination: dict):
|
|
465
491
|
source_positions = self.source_positions() or []
|
|
466
|
-
dest_symbols = get_copy_symbols(destination)
|
|
492
|
+
dest_symbols = get_copy_symbols(destination, self.source)
|
|
467
493
|
dest_positions = self.destination_positions(destination) or []
|
|
468
494
|
source_positions = self.filter_positions_and_orders(
|
|
469
495
|
source_positions, symbols=dest_symbols
|
|
@@ -475,7 +501,7 @@ class TradeCopier(object):
|
|
|
475
501
|
|
|
476
502
|
def get_orders(self, destination: dict):
|
|
477
503
|
source_orders = self.source_orders() or []
|
|
478
|
-
dest_symbols = get_copy_symbols(destination)
|
|
504
|
+
dest_symbols = get_copy_symbols(destination, self.source)
|
|
479
505
|
dest_orders = self.destination_orders(destination) or []
|
|
480
506
|
source_orders = self.filter_positions_and_orders(
|
|
481
507
|
source_orders, symbols=dest_symbols
|
|
@@ -511,7 +537,7 @@ class TradeCopier(object):
|
|
|
511
537
|
source_ids = [order.ticket for order in source_orders]
|
|
512
538
|
for destination_order in destination_orders:
|
|
513
539
|
if destination_order.magic not in source_ids:
|
|
514
|
-
src_symbol = get_copy_symbol(
|
|
540
|
+
src_symbol = self.get_copy_symbol(
|
|
515
541
|
destination_order.symbol, destination, type="source"
|
|
516
542
|
)
|
|
517
543
|
self.remove_order(src_symbol, destination_order, destination)
|
|
@@ -547,7 +573,7 @@ class TradeCopier(object):
|
|
|
547
573
|
if not destination.get("copy", False):
|
|
548
574
|
raise ValueError("Destination account not set to copy mode")
|
|
549
575
|
return destination.get("copy_what", "all")
|
|
550
|
-
|
|
576
|
+
|
|
551
577
|
def copy_orders(self, destination: dict):
|
|
552
578
|
what = self._copy_what(destination)
|
|
553
579
|
if what not in ["all", "orders"]:
|
|
@@ -587,7 +613,7 @@ class TradeCopier(object):
|
|
|
587
613
|
source_ids = [pos.ticket for pos in source_positions]
|
|
588
614
|
for destination_position in destination_positions:
|
|
589
615
|
if destination_position.magic not in source_ids:
|
|
590
|
-
src_symbol = get_copy_symbol(
|
|
616
|
+
src_symbol = self.get_copy_symbol(
|
|
591
617
|
destination_position.symbol, destination, type="source"
|
|
592
618
|
)
|
|
593
619
|
self.remove_position(src_symbol, destination_position, destination)
|
|
@@ -612,8 +638,15 @@ class TradeCopier(object):
|
|
|
612
638
|
logger.info("Trade Copier Running ...")
|
|
613
639
|
logger.info(f"Source Account: {self.source.get('login')}")
|
|
614
640
|
while True:
|
|
641
|
+
if self.shutdown_event and self.shutdown_event.is_set():
|
|
642
|
+
logger.info(
|
|
643
|
+
"Shutdown event received, stopping Trade Copier gracefully."
|
|
644
|
+
)
|
|
645
|
+
break
|
|
615
646
|
try:
|
|
616
647
|
for destination in self.destinations:
|
|
648
|
+
if self.shutdown_event and self.shutdown_event.is_set():
|
|
649
|
+
break
|
|
617
650
|
if destination.get("path") == self.source.get("path"):
|
|
618
651
|
err_msg = "Source and destination accounts are on the same \
|
|
619
652
|
MetaTrader 5 installation which is not allowed."
|
|
@@ -623,18 +656,52 @@ class TradeCopier(object):
|
|
|
623
656
|
self.copy_positions(destination)
|
|
624
657
|
Mt5.shutdown()
|
|
625
658
|
time.sleep(0.1)
|
|
659
|
+
|
|
660
|
+
if self.shutdown_event and self.shutdown_event.is_set():
|
|
661
|
+
logger.info(
|
|
662
|
+
"Shutdown event received during destination processing, exiting."
|
|
663
|
+
)
|
|
664
|
+
break
|
|
665
|
+
|
|
626
666
|
except KeyboardInterrupt:
|
|
627
|
-
logger.info("
|
|
628
|
-
|
|
667
|
+
logger.info("KeyboardInterrupt received, stopping the Trade Copier ...")
|
|
668
|
+
if self.shutdown_event:
|
|
669
|
+
self.shutdown_event.set()
|
|
670
|
+
break
|
|
629
671
|
except Exception as e:
|
|
630
672
|
self.log_error(e)
|
|
673
|
+
if self.shutdown_event and self.shutdown_event.is_set():
|
|
674
|
+
logger.error(
|
|
675
|
+
"Error occurred after shutdown signaled, exiting loop."
|
|
676
|
+
)
|
|
677
|
+
break
|
|
678
|
+
|
|
679
|
+
# Check shutdown event before sleeping
|
|
680
|
+
if self.shutdown_event and self.shutdown_event.is_set():
|
|
681
|
+
logger.info("Shutdown event checked before sleep, exiting.")
|
|
682
|
+
break
|
|
631
683
|
time.sleep(self.sleeptime)
|
|
684
|
+
logger.info("Trade Copier has shut down.")
|
|
632
685
|
|
|
633
686
|
|
|
634
687
|
def RunCopier(
|
|
635
|
-
source: dict,
|
|
688
|
+
source: dict,
|
|
689
|
+
destinations: list,
|
|
690
|
+
sleeptime: float,
|
|
691
|
+
start_time: str,
|
|
692
|
+
end_time: str,
|
|
693
|
+
shutdown_event=None,
|
|
694
|
+
custom_logger=None,
|
|
636
695
|
):
|
|
637
|
-
copier = TradeCopier(
|
|
696
|
+
copier = TradeCopier(
|
|
697
|
+
source,
|
|
698
|
+
destinations,
|
|
699
|
+
sleeptime,
|
|
700
|
+
start_time,
|
|
701
|
+
end_time,
|
|
702
|
+
shutdown_event,
|
|
703
|
+
custom_logger,
|
|
704
|
+
)
|
|
638
705
|
copier.run()
|
|
639
706
|
|
|
640
707
|
|
|
@@ -644,6 +711,8 @@ def RunMultipleCopier(
|
|
|
644
711
|
start_delay: float = 1.0,
|
|
645
712
|
start_time: str = None,
|
|
646
713
|
end_time: str = None,
|
|
714
|
+
shutdown_event=None,
|
|
715
|
+
custom_logger=None,
|
|
647
716
|
):
|
|
648
717
|
processes = []
|
|
649
718
|
|
|
@@ -661,10 +730,16 @@ def RunMultipleCopier(
|
|
|
661
730
|
)
|
|
662
731
|
continue
|
|
663
732
|
logger.info(f"Starting process for source account @{source.get('login')}")
|
|
664
|
-
|
|
665
733
|
process = multiprocessing.Process(
|
|
666
734
|
target=RunCopier,
|
|
667
|
-
args=(
|
|
735
|
+
args=(
|
|
736
|
+
source,
|
|
737
|
+
destinations,
|
|
738
|
+
sleeptime,
|
|
739
|
+
start_time,
|
|
740
|
+
end_time,
|
|
741
|
+
),
|
|
742
|
+
kwargs=dict(shutdown_event=shutdown_event, custom_logger=custom_logger),
|
|
668
743
|
)
|
|
669
744
|
processes.append(process)
|
|
670
745
|
process.start()
|
|
@@ -672,13 +747,14 @@ def RunMultipleCopier(
|
|
|
672
747
|
if start_delay:
|
|
673
748
|
time.sleep(start_delay)
|
|
674
749
|
|
|
675
|
-
# Wait for all processes to complete
|
|
676
750
|
for process in processes:
|
|
677
751
|
process.join()
|
|
678
752
|
|
|
679
753
|
|
|
680
754
|
def _strtodict(string: str) -> dict:
|
|
681
755
|
string = string.strip().replace("\n", "").replace(" ", "").replace('"""', "")
|
|
756
|
+
if string.endswith(","):
|
|
757
|
+
string = string[:-1]
|
|
682
758
|
return dict(item.split(":") for item in string.split(","))
|
|
683
759
|
|
|
684
760
|
|