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
bbstrader/scripts.py
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import importlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
import json
|
|
6
|
+
import multiprocessing
|
|
7
|
+
import multiprocessing as mp
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import textwrap
|
|
11
|
+
import time
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
from typing import Any, Dict, List, Literal, Type
|
|
15
|
+
|
|
16
|
+
import nltk
|
|
17
|
+
from loguru import logger
|
|
18
|
+
from sumy.nlp.tokenizers import Tokenizer
|
|
19
|
+
from sumy.parsers.plaintext import PlaintextParser
|
|
20
|
+
from sumy.summarizers.text_rank import TextRankSummarizer
|
|
21
|
+
|
|
22
|
+
from bbstrader.btengine.backtest import run_backtest
|
|
23
|
+
from bbstrader.btengine.data import (
|
|
24
|
+
CSVDataHandler,
|
|
25
|
+
DataHandler,
|
|
26
|
+
EODHDataHandler,
|
|
27
|
+
FMPDataHandler,
|
|
28
|
+
MT5DataHandler,
|
|
29
|
+
YFDataHandler,
|
|
30
|
+
)
|
|
31
|
+
from bbstrader.btengine.execution import (
|
|
32
|
+
ExecutionHandler,
|
|
33
|
+
MT5ExecutionHandler,
|
|
34
|
+
SimExecutionHandler,
|
|
35
|
+
)
|
|
36
|
+
from bbstrader.btengine.strategy import BaseStrategy
|
|
37
|
+
from bbstrader.core.data import FinancialNews
|
|
38
|
+
from bbstrader.core.strategy import Strategy
|
|
39
|
+
from bbstrader.metatrader._copier import main as RunCopyApp
|
|
40
|
+
from bbstrader.metatrader.copier import RunCopier, config_copier, copier_worker_process
|
|
41
|
+
from bbstrader.metatrader.trade import create_trade_instance
|
|
42
|
+
from bbstrader.trading.execution import RunMt5Engine
|
|
43
|
+
from bbstrader.trading.strategy import LiveStrategy
|
|
44
|
+
from bbstrader.trading.utils import send_telegram_message
|
|
45
|
+
|
|
46
|
+
EXECUTION_PATH = os.path.expanduser("~/.bbstrader/execution/execution.py")
|
|
47
|
+
CONFIG_PATH = os.path.expanduser("~/.bbstrader/execution/execution.json")
|
|
48
|
+
BACKTEST_PATH = os.path.expanduser("~/.bbstrader/backtest/backtest.py")
|
|
49
|
+
CONFIG_PATH = os.path.expanduser("~/.bbstrader/backtest/backtest.json")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
DATA_HANDLER_MAP: Dict[str, Type[DataHandler]] = {
|
|
53
|
+
"csv": CSVDataHandler,
|
|
54
|
+
"mt5": MT5DataHandler,
|
|
55
|
+
"yf": YFDataHandler,
|
|
56
|
+
"eodh": EODHDataHandler,
|
|
57
|
+
"fmp": FMPDataHandler,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
EXECUTION_HANDLER_MAP: Dict[str, Type[ExecutionHandler]] = {
|
|
61
|
+
"sim": SimExecutionHandler,
|
|
62
|
+
"mt5": MT5ExecutionHandler,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["load_module", "load_class"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_module(file_path: str) -> ModuleType:
|
|
70
|
+
"""Load a module from a file path.
|
|
71
|
+
Args:
|
|
72
|
+
file_path: Path to the file to load.
|
|
73
|
+
Returns:
|
|
74
|
+
The loaded module.
|
|
75
|
+
"""
|
|
76
|
+
if not os.path.exists(file_path):
|
|
77
|
+
raise FileNotFoundError(
|
|
78
|
+
f"Strategy file {file_path} not found. Please create it."
|
|
79
|
+
)
|
|
80
|
+
spec = importlib.util.spec_from_file_location("bbstrader.cli", file_path)
|
|
81
|
+
if spec is None or spec.loader is None:
|
|
82
|
+
raise ImportError(f"Could not load spec for module at {file_path}")
|
|
83
|
+
module = importlib.util.module_from_spec(spec)
|
|
84
|
+
spec.loader.exec_module(module)
|
|
85
|
+
return module
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_class(module: ModuleType, class_name: str, base_class: Type) -> Type:
|
|
89
|
+
"""Load a class from a module.
|
|
90
|
+
Args:
|
|
91
|
+
module: The module to load the class from.
|
|
92
|
+
class_name: The name of the class to load.
|
|
93
|
+
base_class: The base class that the class must inherit from.
|
|
94
|
+
"""
|
|
95
|
+
if not hasattr(module, class_name):
|
|
96
|
+
raise AttributeError(f"{class_name} not found in {module}")
|
|
97
|
+
class_ = getattr(module, class_name)
|
|
98
|
+
if not issubclass(class_, base_class):
|
|
99
|
+
raise TypeError(f"{class_name} must inherit from {base_class}.")
|
|
100
|
+
return class_
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
##############################################################
|
|
104
|
+
###################### BACKTESTING ###########################
|
|
105
|
+
##############################################################
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def load_exc_handler(module: ModuleType, handler_name: str) -> Type[ExecutionHandler]:
|
|
109
|
+
return load_class(module, handler_name, ExecutionHandler) # type: ignore
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_data_handler(module: ModuleType, handler_name: str) -> Type[DataHandler]:
|
|
113
|
+
return load_class(module, handler_name, DataHandler) # type: ignore
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load_strategy(module: ModuleType, strategy_name: str) -> Type[Strategy]:
|
|
117
|
+
return load_class(module, strategy_name, (Strategy, BaseStrategy)) # type: ignore
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_backtest_config(config_path: str, strategy_name: str) -> Dict[str, Any]:
|
|
121
|
+
if not os.path.exists(config_path):
|
|
122
|
+
raise FileNotFoundError(
|
|
123
|
+
f"Configuration file {config_path} not found. Please create it."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
with open(config_path, "r") as f:
|
|
127
|
+
config = json.load(f)
|
|
128
|
+
try:
|
|
129
|
+
config = config[strategy_name]
|
|
130
|
+
except KeyError:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"Strategy {strategy_name} not found in the configuration file."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
required_fields = ["symbol_list", "start_date", "data_handler", "execution_handler"]
|
|
136
|
+
for field in required_fields:
|
|
137
|
+
if not config.get(field):
|
|
138
|
+
raise ValueError(f"{field} is required in the configuration file.")
|
|
139
|
+
|
|
140
|
+
config["start_date"] = datetime.strptime(config["start_date"], "%Y-%m-%d")
|
|
141
|
+
|
|
142
|
+
if config.get("execution_handler") not in EXECUTION_HANDLER_MAP:
|
|
143
|
+
try:
|
|
144
|
+
backtest_module = load_module(BACKTEST_PATH)
|
|
145
|
+
exc_handler_class = load_exc_handler(
|
|
146
|
+
backtest_module, config["execution_handler"]
|
|
147
|
+
)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
raise ValueError(f"Invalid execution handler: {e}")
|
|
150
|
+
else:
|
|
151
|
+
exc_handler_class = EXECUTION_HANDLER_MAP[config["execution_handler"]]
|
|
152
|
+
|
|
153
|
+
if config.get("data_handler") not in DATA_HANDLER_MAP:
|
|
154
|
+
try:
|
|
155
|
+
backtest_module = load_module(BACKTEST_PATH)
|
|
156
|
+
data_handler_class = load_data_handler(
|
|
157
|
+
backtest_module, config["data_handler"]
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise ValueError(f"Invalid data handler: {e}")
|
|
161
|
+
else:
|
|
162
|
+
data_handler_class = DATA_HANDLER_MAP[config["data_handler"]]
|
|
163
|
+
|
|
164
|
+
config["execution_handler"] = exc_handler_class
|
|
165
|
+
config["data_handler"] = data_handler_class
|
|
166
|
+
|
|
167
|
+
return config
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def backtest(unknown: List[str]) -> None:
|
|
171
|
+
HELP_MSG = """
|
|
172
|
+
Usage:
|
|
173
|
+
python -m bbstrader --run backtest [options]
|
|
174
|
+
|
|
175
|
+
Options:
|
|
176
|
+
-s, --strategy: Strategy class name to run
|
|
177
|
+
-c, --config: Configuration file path (default: ~/.bbstrader/backtest/backtest.json)
|
|
178
|
+
-p, --path: Path to the backtest file (default: ~/.bbstrader/backtest/backtest.py)
|
|
179
|
+
|
|
180
|
+
Note:
|
|
181
|
+
The configuration file must contain all the required parameters
|
|
182
|
+
for the data handler and execution handler and strategy.
|
|
183
|
+
See bbstrader.btengine.BacktestEngine for more details on the parameters.
|
|
184
|
+
"""
|
|
185
|
+
if "-h" in unknown or "--help" in unknown:
|
|
186
|
+
print(HELP_MSG)
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
|
|
189
|
+
parser = argparse.ArgumentParser(description="Backtesting Engine CLI")
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"-s", "--strategy", type=str, required=True, help="Strategy class name to run"
|
|
192
|
+
)
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
"-c", "--config", type=str, default=CONFIG_PATH, help="Configuration file path"
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"-p",
|
|
198
|
+
"--path",
|
|
199
|
+
type=str,
|
|
200
|
+
default=BACKTEST_PATH,
|
|
201
|
+
help="Path to the backtest file",
|
|
202
|
+
)
|
|
203
|
+
args = parser.parse_args(unknown)
|
|
204
|
+
config = load_backtest_config(args.config, args.strategy)
|
|
205
|
+
strategy_module = load_module(args.path)
|
|
206
|
+
strategy_class = load_strategy(strategy_module, args.strategy)
|
|
207
|
+
|
|
208
|
+
symbol_list = config.pop("symbol_list")
|
|
209
|
+
start_date = config.pop("start_date")
|
|
210
|
+
data_handler = config.pop("data_handler")
|
|
211
|
+
execution_handler = config.pop("execution_handler")
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
run_backtest(
|
|
215
|
+
symbol_list,
|
|
216
|
+
start_date,
|
|
217
|
+
data_handler,
|
|
218
|
+
strategy_class,
|
|
219
|
+
exc_handler=execution_handler,
|
|
220
|
+
**config,
|
|
221
|
+
)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"Error: {e}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
##############################################################
|
|
227
|
+
###################### LIVE EXECUTION ########################
|
|
228
|
+
##############################################################
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def load_live_config(config_path, strategy_name, account=None):
|
|
232
|
+
if not os.path.exists(config_path):
|
|
233
|
+
raise FileNotFoundError(f"Configuration file not found at {config_path}")
|
|
234
|
+
with open(config_path, "r") as f:
|
|
235
|
+
config = json.load(f)
|
|
236
|
+
try:
|
|
237
|
+
config = config[strategy_name]
|
|
238
|
+
except KeyError:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"Strategy {strategy_name} not found in the configuration file."
|
|
241
|
+
)
|
|
242
|
+
if account is not None:
|
|
243
|
+
try:
|
|
244
|
+
config = config[account]
|
|
245
|
+
except KeyError:
|
|
246
|
+
raise ValueError(f"Account {account} not found in the configuration file.")
|
|
247
|
+
if config.get("symbol_list") is None:
|
|
248
|
+
raise ValueError("symbol_list is required in the configuration file.")
|
|
249
|
+
if config.get("trades_kwargs") is None:
|
|
250
|
+
raise ValueError("trades_kwargs is required in the configuration file.")
|
|
251
|
+
return config
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def worker_function(account, args):
|
|
255
|
+
strategy_module = load_module(args.path)
|
|
256
|
+
strategy_class = load_class(
|
|
257
|
+
strategy_module, args.strategy, (LiveStrategy, Strategy)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
config = load_live_config(args.config, args.strategy, account)
|
|
261
|
+
symbol_list = config.pop("symbol_list")
|
|
262
|
+
trades_kwargs = config.pop("trades_kwargs")
|
|
263
|
+
trades = create_trade_instance(symbol_list, trades_kwargs)
|
|
264
|
+
|
|
265
|
+
kwargs = {
|
|
266
|
+
"symbol_list": symbol_list,
|
|
267
|
+
"trades_instances": trades,
|
|
268
|
+
"strategy_cls": strategy_class,
|
|
269
|
+
"account": account,
|
|
270
|
+
**config,
|
|
271
|
+
}
|
|
272
|
+
RunMt5Engine(account, **kwargs)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def RunMt5Terminal(args):
|
|
276
|
+
if args.parallel:
|
|
277
|
+
if len(args.account) == 0:
|
|
278
|
+
raise ValueError(
|
|
279
|
+
"account or accounts are required when running in parallel"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
processes = []
|
|
283
|
+
try:
|
|
284
|
+
for account in args.account:
|
|
285
|
+
p = mp.Process(target=worker_function, args=(account, args))
|
|
286
|
+
p.start()
|
|
287
|
+
processes.append(p)
|
|
288
|
+
|
|
289
|
+
for p in processes:
|
|
290
|
+
p.join()
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print(f"Error in parallel execution: {e}")
|
|
293
|
+
raise e
|
|
294
|
+
except KeyboardInterrupt:
|
|
295
|
+
print("\nTerminating Execution...")
|
|
296
|
+
for p in processes:
|
|
297
|
+
p.terminate()
|
|
298
|
+
for p in processes:
|
|
299
|
+
p.join()
|
|
300
|
+
print("Execution terminated")
|
|
301
|
+
else:
|
|
302
|
+
worker_function(args.account[0], args)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def RunTWSTerminal(args):
|
|
306
|
+
raise NotImplementedError("RunTWSTerminal is not implemented yet")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def execute_strategy(unknown):
|
|
310
|
+
HELP_MSG = """
|
|
311
|
+
Execute a strategy on one or multiple MT5 accounts.
|
|
312
|
+
|
|
313
|
+
Usage:
|
|
314
|
+
python -m bbstrader --run execution [options]
|
|
315
|
+
|
|
316
|
+
Options:
|
|
317
|
+
-s, --strategy: Strategy class name to run
|
|
318
|
+
-a, --account: Account(s) name(s) or ID(s) to run the strategy on (must be the same as in the configuration file)
|
|
319
|
+
-p, --path: Path to the execution file (default: ~/.bbstrader/execution/execution.py)
|
|
320
|
+
-c, --config: Path to the configuration file (default: ~/.bbstrader/execution/execution.json)
|
|
321
|
+
-l, --parallel: Run the strategy in parallel (default: False)
|
|
322
|
+
-t, --terminal: Terminal to use (default: MT5)
|
|
323
|
+
-h, --help: Show this help message and exit
|
|
324
|
+
|
|
325
|
+
Note:
|
|
326
|
+
The configuration file must contain all the required parameters
|
|
327
|
+
to create trade instances for each account and strategy.
|
|
328
|
+
The configuration file must be a dictionary with the following structure:
|
|
329
|
+
If parallel is True:
|
|
330
|
+
{
|
|
331
|
+
"strategy_name": {
|
|
332
|
+
"account_name": {
|
|
333
|
+
"symbol_list": ["symbol1", "symbol2"],
|
|
334
|
+
"trades_kwargs": {"param1": "value1", "param2": "value2"}
|
|
335
|
+
**other_parameters (for the strategy and the execution engine)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
If parallel is False:
|
|
340
|
+
{
|
|
341
|
+
"strategy_name": {
|
|
342
|
+
"symbol_list": ["symbol1", "symbol2"],
|
|
343
|
+
"trades_kwargs": {"param1": "value1", "param2": "value2"}
|
|
344
|
+
**other_parameters (for the strategy and the execution engine)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
See bbstrader.metatrader.trade.create_trade_instance for more details on the trades_kwargs.
|
|
348
|
+
See bbstrader.trading.execution.Mt5ExecutionEngine for more details on the other parameters.
|
|
349
|
+
|
|
350
|
+
All other paramaters must be python built-in types.
|
|
351
|
+
If you have custom type you must set them in your strategy class
|
|
352
|
+
or run the Mt5ExecutionEngine directly, don't run on CLI.
|
|
353
|
+
"""
|
|
354
|
+
if "-h" in unknown or "--help" in unknown:
|
|
355
|
+
print(HELP_MSG)
|
|
356
|
+
sys.exit(0)
|
|
357
|
+
|
|
358
|
+
parser = argparse.ArgumentParser()
|
|
359
|
+
parser.add_argument("-s", "--strategy", type=str, required=True)
|
|
360
|
+
parser.add_argument("-a", "--account", type=str, nargs="*", default=[])
|
|
361
|
+
parser.add_argument("-p", "--path", type=str, default=EXECUTION_PATH)
|
|
362
|
+
parser.add_argument("-c", "--config", type=str, default=CONFIG_PATH)
|
|
363
|
+
parser.add_argument("-l", "--parallel", action="store_true")
|
|
364
|
+
parser.add_argument(
|
|
365
|
+
"-t", "--terminal", type=str, default="MT5", choices=["MT5", "TWS"]
|
|
366
|
+
)
|
|
367
|
+
args = parser.parse_args(unknown)
|
|
368
|
+
|
|
369
|
+
if args.terminal == "MT5":
|
|
370
|
+
RunMt5Terminal(args)
|
|
371
|
+
elif args.terminal == "TWS":
|
|
372
|
+
RunTWSTerminal(args)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
##########################################################
|
|
376
|
+
##################### TRADE COPIER #######################
|
|
377
|
+
##########################################################
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def copier_args(parser: argparse.ArgumentParser):
|
|
381
|
+
parser.add_argument(
|
|
382
|
+
"-m",
|
|
383
|
+
"--mode",
|
|
384
|
+
type=str,
|
|
385
|
+
default="CLI",
|
|
386
|
+
choices=("CLI", "GUI"),
|
|
387
|
+
help="Run the copier in the terminal or using the GUI",
|
|
388
|
+
)
|
|
389
|
+
parser.add_argument(
|
|
390
|
+
"-s", "--source", type=str, nargs="?", default=None, help="Source section name"
|
|
391
|
+
)
|
|
392
|
+
parser.add_argument(
|
|
393
|
+
"-I", "--id", type=int, default=0, help="Source Account unique ID"
|
|
394
|
+
)
|
|
395
|
+
parser.add_argument(
|
|
396
|
+
"-U",
|
|
397
|
+
"--unique",
|
|
398
|
+
action="store_true",
|
|
399
|
+
help="Specify if the source account is only master",
|
|
400
|
+
)
|
|
401
|
+
parser.add_argument(
|
|
402
|
+
"-d",
|
|
403
|
+
"--destinations",
|
|
404
|
+
type=str,
|
|
405
|
+
nargs="*",
|
|
406
|
+
default=None,
|
|
407
|
+
help="Destination section names",
|
|
408
|
+
)
|
|
409
|
+
parser.add_argument(
|
|
410
|
+
"-i", "--interval", type=float, default=0.1, help="Update interval in seconds"
|
|
411
|
+
)
|
|
412
|
+
parser.add_argument(
|
|
413
|
+
"-c",
|
|
414
|
+
"--config",
|
|
415
|
+
nargs="?",
|
|
416
|
+
default=None,
|
|
417
|
+
type=str,
|
|
418
|
+
help="Config file name or path",
|
|
419
|
+
)
|
|
420
|
+
parser.add_argument(
|
|
421
|
+
"-t",
|
|
422
|
+
"--start",
|
|
423
|
+
type=str,
|
|
424
|
+
nargs="?",
|
|
425
|
+
default=None,
|
|
426
|
+
help="Start time in HH:MM format",
|
|
427
|
+
)
|
|
428
|
+
parser.add_argument(
|
|
429
|
+
"-e",
|
|
430
|
+
"--end",
|
|
431
|
+
type=str,
|
|
432
|
+
nargs="?",
|
|
433
|
+
default=None,
|
|
434
|
+
help="End time in HH:MM format",
|
|
435
|
+
)
|
|
436
|
+
parser.add_argument(
|
|
437
|
+
"-M",
|
|
438
|
+
"--multiprocess",
|
|
439
|
+
action="store_true",
|
|
440
|
+
help="Run each destination account in a separate process.",
|
|
441
|
+
)
|
|
442
|
+
return parser
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def copy_trades(unknown):
|
|
446
|
+
HELP_MSG = """
|
|
447
|
+
Usage:
|
|
448
|
+
python -m bbstrader --run copier [options]
|
|
449
|
+
|
|
450
|
+
Options:
|
|
451
|
+
-m, --mode: CLI for terminal app and GUI for Desktop app
|
|
452
|
+
-s, --source: Source Account section name
|
|
453
|
+
-I, --id: Source Account unique ID
|
|
454
|
+
-U, --unique: Specify if the source account is only master
|
|
455
|
+
-d, --destinations: Destination Account section names (multiple allowed)
|
|
456
|
+
-i, --interval: Update interval in seconds
|
|
457
|
+
-M, --multiprocess: When set to True, each destination account runs in a separate process.
|
|
458
|
+
-c, --config: .ini file or path (default: ~/.bbstrader/copier/copier.ini)
|
|
459
|
+
-t, --start: Start time in HH:MM format
|
|
460
|
+
-e, --end: End time in HH:MM format
|
|
461
|
+
"""
|
|
462
|
+
if "-h" in unknown or "--help" in unknown:
|
|
463
|
+
print(HELP_MSG)
|
|
464
|
+
sys.exit(0)
|
|
465
|
+
|
|
466
|
+
copy_parser = argparse.ArgumentParser("Trades Copier", add_help=False)
|
|
467
|
+
copy_parser = copier_args(copy_parser)
|
|
468
|
+
copy_args = copy_parser.parse_args(unknown)
|
|
469
|
+
|
|
470
|
+
if copy_args.mode == "GUI":
|
|
471
|
+
RunCopyApp()
|
|
472
|
+
|
|
473
|
+
elif copy_args.mode == "CLI":
|
|
474
|
+
source, destinations = config_copier(
|
|
475
|
+
source_section=copy_args.source,
|
|
476
|
+
dest_sections=copy_args.destinations,
|
|
477
|
+
inifile=copy_args.config,
|
|
478
|
+
)
|
|
479
|
+
source["id"] = copy_args.id
|
|
480
|
+
source["unique"] = copy_args.unique
|
|
481
|
+
if copy_args.multiprocess:
|
|
482
|
+
copier_processes = []
|
|
483
|
+
for dest_config in destinations:
|
|
484
|
+
process = multiprocessing.Process(
|
|
485
|
+
target=copier_worker_process,
|
|
486
|
+
args=(
|
|
487
|
+
source,
|
|
488
|
+
dest_config,
|
|
489
|
+
copy_args.interval,
|
|
490
|
+
copy_args.start,
|
|
491
|
+
copy_args.end,
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
process.start()
|
|
495
|
+
copier_processes.append(process)
|
|
496
|
+
for process in copier_processes:
|
|
497
|
+
process.join()
|
|
498
|
+
else:
|
|
499
|
+
RunCopier(
|
|
500
|
+
source,
|
|
501
|
+
destinations,
|
|
502
|
+
copy_args.interval,
|
|
503
|
+
copy_args.start,
|
|
504
|
+
copy_args.end,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
############################################################
|
|
509
|
+
##################### NEWS FEED ############################
|
|
510
|
+
############################################################
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def summarize_text(text: str, sentences_count: int = 5) -> str:
|
|
514
|
+
"""
|
|
515
|
+
Generate a summary using TextRank algorithm.
|
|
516
|
+
"""
|
|
517
|
+
parser = PlaintextParser.from_string(text, Tokenizer("english"))
|
|
518
|
+
summarizer = TextRankSummarizer()
|
|
519
|
+
summary = summarizer(parser.document, sentences_count)
|
|
520
|
+
return " ".join(str(sentence) for sentence in summary)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def format_coindesk_article(article: Dict[str, Any]) -> str:
|
|
524
|
+
if not all(
|
|
525
|
+
k in article
|
|
526
|
+
for k in (
|
|
527
|
+
"body",
|
|
528
|
+
"title",
|
|
529
|
+
"published_on",
|
|
530
|
+
"sentiment",
|
|
531
|
+
"keywords",
|
|
532
|
+
"status",
|
|
533
|
+
"url",
|
|
534
|
+
)
|
|
535
|
+
):
|
|
536
|
+
return ""
|
|
537
|
+
summary = summarize_text(article["body"], sentences_count=3)
|
|
538
|
+
text = (
|
|
539
|
+
f"📰 {article['title']}\n"
|
|
540
|
+
f"Published Date: {article['published_on']}\n"
|
|
541
|
+
f"Sentiment: {article['sentiment']}\n"
|
|
542
|
+
f"Status: {article['status']}\n"
|
|
543
|
+
f"Keywords: {article['keywords']}\n\n"
|
|
544
|
+
f"🔍 Summary\n"
|
|
545
|
+
f"{textwrap.fill(summary, width=80)}"
|
|
546
|
+
f"\n\n👉 Visit {article['url']} for full article."
|
|
547
|
+
)
|
|
548
|
+
return text
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def format_fmp_article(article: Dict[str, Any]) -> str:
|
|
552
|
+
if not all(k in article for k in ("title", "date", "content", "tickers")):
|
|
553
|
+
return ""
|
|
554
|
+
summary = summarize_text(article["content"], sentences_count=3)
|
|
555
|
+
text = (
|
|
556
|
+
f"📰 {article['title']}\n"
|
|
557
|
+
f"Published Date: {article['date']}\n"
|
|
558
|
+
f"Keywords: {article['tickers']}\n\n"
|
|
559
|
+
f"🔍 Summary\n"
|
|
560
|
+
f"{textwrap.fill(summary, width=80)}"
|
|
561
|
+
)
|
|
562
|
+
return text
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
async def send_articles(
|
|
566
|
+
articles: List[Dict[str, Any]],
|
|
567
|
+
token: str,
|
|
568
|
+
id: str,
|
|
569
|
+
source: Literal["coindesk", "fmp"],
|
|
570
|
+
interval: int = 15,
|
|
571
|
+
) -> None:
|
|
572
|
+
for article in articles:
|
|
573
|
+
message = ""
|
|
574
|
+
if source == "coindesk":
|
|
575
|
+
published_on = article.get("published_on")
|
|
576
|
+
if isinstance(
|
|
577
|
+
published_on, datetime
|
|
578
|
+
) and published_on >= datetime.now() - timedelta(minutes=interval):
|
|
579
|
+
article["published_on"] = published_on.strftime("%Y-%m-%d %H:%M:%S")
|
|
580
|
+
message = format_coindesk_article(article)
|
|
581
|
+
else:
|
|
582
|
+
message = format_fmp_article(article)
|
|
583
|
+
if message == "":
|
|
584
|
+
continue
|
|
585
|
+
await asyncio.sleep(2) # To avoid hitting Telegram rate limits
|
|
586
|
+
await send_telegram_message(token, id, text=message)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def send_news_feed(unknown: List[str]) -> None:
|
|
590
|
+
HELP_MSG = """
|
|
591
|
+
Send news feed from Coindesk to Telegram channel.
|
|
592
|
+
This script fetches the latest news articles from Coindesk, summarizes them,
|
|
593
|
+
and sends them to a specified Telegram channel at regular intervals.
|
|
594
|
+
|
|
595
|
+
Usage:
|
|
596
|
+
python -m bbstrader --run news_feed [options]
|
|
597
|
+
|
|
598
|
+
Options:
|
|
599
|
+
-q, --query: The news to look for (default: "")
|
|
600
|
+
-t, --token: Telegram bot token
|
|
601
|
+
-I, --id: Telegram Chat id
|
|
602
|
+
--fmp: Financial Modeling Prop Api Key
|
|
603
|
+
-i, --interval: Interval in minutes to fetch news (default: 15)
|
|
604
|
+
|
|
605
|
+
Note:
|
|
606
|
+
The script will run indefinitely, fetching news every 15 minutes.
|
|
607
|
+
Use Ctrl+C to stop the script.
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
if "-h" in unknown or "--help" in unknown:
|
|
611
|
+
print(HELP_MSG)
|
|
612
|
+
sys.exit(0)
|
|
613
|
+
|
|
614
|
+
parser = argparse.ArgumentParser()
|
|
615
|
+
parser.add_argument(
|
|
616
|
+
"-q", "--query", type=str, default="", help="The news to look for"
|
|
617
|
+
)
|
|
618
|
+
parser.add_argument(
|
|
619
|
+
"-t",
|
|
620
|
+
"--token",
|
|
621
|
+
type=str,
|
|
622
|
+
required=True,
|
|
623
|
+
help="Telegram bot token",
|
|
624
|
+
)
|
|
625
|
+
parser.add_argument("-I", "--id", type=str, required=True, help="Telegram Chat id")
|
|
626
|
+
parser.add_argument(
|
|
627
|
+
"--fmp", type=str, default="", help="Financial Modeling Prop Api Key"
|
|
628
|
+
)
|
|
629
|
+
parser.add_argument(
|
|
630
|
+
"-i",
|
|
631
|
+
"--interval",
|
|
632
|
+
type=int,
|
|
633
|
+
default=15,
|
|
634
|
+
help="Interval in minutes to fetch news (default: 15)",
|
|
635
|
+
)
|
|
636
|
+
args = parser.parse_args(unknown)
|
|
637
|
+
|
|
638
|
+
nltk.download("punkt", quiet=True)
|
|
639
|
+
news = FinancialNews()
|
|
640
|
+
fmp_news = news.get_fmp_news(api=args.fmp) if args.fmp else None
|
|
641
|
+
logger.info(f"Starting the News Feed on {args.interval} minutes")
|
|
642
|
+
while True:
|
|
643
|
+
try:
|
|
644
|
+
fmp_articles: List[Dict[str, Any]] = []
|
|
645
|
+
if fmp_news is not None:
|
|
646
|
+
fmp_articles = fmp_news.get_latest_articles(limit=5)
|
|
647
|
+
coindesk_articles = news.get_coindesk_news(query=args.query)
|
|
648
|
+
if coindesk_articles and isinstance(coindesk_articles, list):
|
|
649
|
+
asyncio.run(
|
|
650
|
+
send_articles(
|
|
651
|
+
coindesk_articles, # type: ignore
|
|
652
|
+
args.token,
|
|
653
|
+
args.id,
|
|
654
|
+
"coindesk",
|
|
655
|
+
interval=args.interval,
|
|
656
|
+
)
|
|
657
|
+
)
|
|
658
|
+
if len(fmp_articles) != 0:
|
|
659
|
+
asyncio.run(send_articles(fmp_articles, args.token, args.id, "fmp"))
|
|
660
|
+
time.sleep(args.interval * 60)
|
|
661
|
+
except KeyboardInterrupt:
|
|
662
|
+
logger.info("Stopping the News Feed ...")
|
|
663
|
+
sys.exit(0)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
logger.error(e)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overview
|
|
3
|
+
========
|
|
4
|
+
|
|
5
|
+
The Trading Module is responsible for the execution of trading strategies. It provides a
|
|
6
|
+
structured framework for implementing and managing trading strategies, from signal generation
|
|
7
|
+
to order execution. This module is designed to be flexible and extensible, allowing for the
|
|
8
|
+
customization of trading logic and integration with various execution handlers.
|
|
9
|
+
|
|
10
|
+
Features
|
|
11
|
+
========
|
|
12
|
+
|
|
13
|
+
- **Strategy Execution Framework**: Defines a clear structure for creating and executing trading strategies.
|
|
14
|
+
- **Signal Generation**: Supports the generation of trading signals based on market data and strategy logic.
|
|
15
|
+
- **Order Management**: Manages the creation and execution of orders based on generated signals.
|
|
16
|
+
- **Extensibility**: Allows for the implementation of custom strategies and execution handlers.
|
|
17
|
+
|
|
18
|
+
Components
|
|
19
|
+
==========
|
|
20
|
+
|
|
21
|
+
- **Execution**: Handles the execution of trades, with a base class for creating custom execution handlers.
|
|
22
|
+
- **Strategy**: Defines the core logic of the trading strategy, including signal generation and order creation.
|
|
23
|
+
- **Utils**: Provides utility functions to support the trading process.
|
|
24
|
+
|
|
25
|
+
Notes
|
|
26
|
+
=====
|
|
27
|
+
|
|
28
|
+
This module can be used in both backtesting and live trading environments by swapping out the
|
|
29
|
+
execution handler.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from bbstrader.trading.execution import * # noqa: F403
|
|
33
|
+
from bbstrader.trading.strategy import * # noqa: F403
|