bbstrader 0.1.4__tar.gz → 0.1.6__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.

Files changed (33) hide show
  1. {bbstrader-0.1.4 → bbstrader-0.1.6}/PKG-INFO +4 -1
  2. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/btengine/backtest.py +10 -7
  3. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/btengine/data.py +22 -11
  4. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/btengine/event.py +4 -3
  5. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/btengine/execution.py +1 -1
  6. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/btengine/strategy.py +3 -1
  7. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/metatrader/account.py +46 -2
  8. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/metatrader/rates.py +41 -12
  9. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/metatrader/risk.py +52 -30
  10. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/metatrader/trade.py +16 -13
  11. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/metatrader/utils.py +3 -2
  12. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/trading/execution.py +24 -12
  13. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader.egg-info/PKG-INFO +4 -1
  14. {bbstrader-0.1.4 → bbstrader-0.1.6}/setup.py +6 -1
  15. {bbstrader-0.1.4 → bbstrader-0.1.6}/LICENSE +0 -0
  16. {bbstrader-0.1.4 → bbstrader-0.1.6}/README.md +0 -0
  17. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/__ini__.py +0 -0
  18. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/btengine/__init__.py +0 -0
  19. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/btengine/performance.py +0 -0
  20. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/btengine/portfolio.py +0 -0
  21. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/metatrader/__init__.py +0 -0
  22. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/models/__init__.py +0 -0
  23. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/models/risk.py +0 -0
  24. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/strategies.py +0 -0
  25. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/trading/__init__.py +0 -0
  26. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/trading/run.py +0 -0
  27. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/trading/utils.py +0 -0
  28. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader/tseries.py +0 -0
  29. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader.egg-info/SOURCES.txt +0 -0
  30. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader.egg-info/dependency_links.txt +0 -0
  31. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader.egg-info/requires.txt +0 -0
  32. {bbstrader-0.1.4 → bbstrader-0.1.6}/bbstrader.egg-info/top_level.txt +0 -0
  33. {bbstrader-0.1.4 → bbstrader-0.1.6}/setup.cfg +0 -0
@@ -1,12 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bbstrader
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Simplified Investment & Trading Toolkit
5
5
  Home-page: https://github.com/bbalouki/bbstrader
6
+ Download-URL: https://pypi.org/project/bbstrader/
6
7
  Author: Bertin Balouki SIMYELI
7
8
  Author-email: <bertin@bbstrader.com>
8
9
  Maintainer: Bertin Balouki SIMYELI
9
10
  License: The MIT License (MIT)
11
+ Project-URL: Documentation, https://bbstrader.readthedocs.io/en/latest/
12
+ Project-URL: Source Code, https://github.com/bbalouki/bbstrader
10
13
  Keywords: Finance,Toolkit,Financial,Analysis,Fundamental,Quantitative,Database,Equities,Currencies,Economics,ETFs,Funds,Indices,Moneymarkets,Commodities,Futures,CFDs,Derivatives,Trading,Investing,Portfolio,Optimization,Performance
11
14
  Classifier: Development Status :: 5 - Production/Stable
12
15
  Classifier: Intended Audience :: Developers
@@ -6,7 +6,6 @@ import pandas as pd
6
6
  import yfinance as yf
7
7
  from queue import Queue
8
8
  from datetime import datetime
9
- from seaborn import saturate
10
9
  from bbstrader.btengine.data import *
11
10
  from bbstrader.btengine.execution import *
12
11
  from bbstrader.btengine.portfolio import Portfolio
@@ -132,7 +131,8 @@ class Backtest(object):
132
131
  self.start_date,
133
132
  self.initial_capital, **self.kwargs
134
133
  )
135
- self.execution_handler: ExecutionHandler = self.eh_cls(self.events)
134
+ self.execution_handler: ExecutionHandler = self.eh_cls(
135
+ self.events, **self.kwargs)
136
136
 
137
137
  def _run_backtest(self):
138
138
  """
@@ -172,7 +172,7 @@ class Backtest(object):
172
172
  self.fills += 1
173
173
  self.portfolio.update_fill(event)
174
174
 
175
- time.sleep(self.heartbeat)
175
+ time.sleep(self.heartbeat)
176
176
 
177
177
  def _output_performance(self):
178
178
  """
@@ -788,10 +788,10 @@ def _run_sma_backtest(
788
788
  "quantity": quantity,
789
789
  "hmm_end": "2009-12-31",
790
790
  "hmm_tiker": "^GSPC",
791
- "yf_start": "2010-01-01",
791
+ "yf_start": "2010-01-04",
792
792
  "hmm_start": "1990-01-01",
793
- "start_pos": "2023-01-01",
794
- "session_duration": 23.0,
793
+ "mt5_start": datetime(2010, 1, 4),
794
+ "mt5_end": datetime(2023, 1, 1),
795
795
  "backtester_class": SMAStrategyBacktester,
796
796
  "data_handler": MT5HistoricDataHandler
797
797
  }
@@ -892,7 +892,10 @@ def run_backtest(
892
892
  quantity=test_quantity
893
893
  )
894
894
  else:
895
- execution_handler = kwargs.get("exc_handler", SimulatedExecutionHandler)
895
+ if exc_handler is None:
896
+ execution_handler = SimulatedExecutionHandler
897
+ else:
898
+ execution_handler = exc_handler
896
899
  engine = Backtest(
897
900
  symbol_list, initial_capital, heartbeat, start_date,
898
901
  data_handler, execution_handler, strategy, **kwargs
@@ -8,6 +8,7 @@ from queue import Queue
8
8
  from abc import ABCMeta, abstractmethod
9
9
  from bbstrader.metatrader.rates import Rates
10
10
  from bbstrader.btengine.event import MarketEvent
11
+ from datetime import datetime
11
12
 
12
13
 
13
14
  __all__ = [
@@ -260,7 +261,7 @@ class HistoricCSVDataHandler(BaseCSVDataHandler):
260
261
  csv_dir = kwargs.get("csv_dir")
261
262
  super().__init__(events, symbol_list, csv_dir)
262
263
 
263
- MAX_BARS = 10_000_000
264
+
264
265
  class MT5HistoricDataHandler(BaseCSVDataHandler):
265
266
  """
266
267
  Downloads historical data from MetaTrader 5 (MT5) and provides
@@ -281,19 +282,16 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
281
282
  symbol_list (List[str]): A list of symbol strings to download data for.
282
283
  **kwargs: Keyword arguments for data retrieval:
283
284
  time_frame (str): MT5 time frame (e.g., 'D1' for daily).
284
- max_bars (int): Maximum number of bars to download per symbol.
285
- start_pos (int | str, optional): Starting bar position (default: 0).
286
- If it set to `str`, it must be in 'YYYY-MM-DD' format and
287
- session_duration (int | float): Number of trading hours per day.
285
+ mt5_start (datetime): Start date for historical data.
286
+ mt5_end (datetime): End date for historical data.
288
287
  mt5_data (str): Directory for storing data (default: 'mt5_data').
289
288
 
290
289
  Note:
291
290
  Requires a working connection to an MT5 terminal.
292
291
  """
293
292
  self.tf = kwargs.get('time_frame', 'D1')
294
- self.start_pos = kwargs.get('start_pos', 0)
295
- self.max_bars = kwargs.get('max_bars', MAX_BARS)
296
- self.sd = kwargs.get('session_duration', 6.5)
293
+ self.start = kwargs.get('mt5_start')
294
+ self.end = kwargs.get('mt5_end', datetime.now())
297
295
  self.data_dir = kwargs.get('mt5_data', 'mt5_data')
298
296
  self.symbol_list = symbol_list
299
297
  csv_dir = self._download_data(self.data_dir)
@@ -304,8 +302,10 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
304
302
  data_dir.mkdir(parents=True, exist_ok=True)
305
303
  for symbol in self.symbol_list:
306
304
  try:
307
- rate = Rates(symbol, self.tf, self.start_pos, self.max_bars, self.sd)
308
- data = rate.get_rates_from_pos()
305
+ rate = Rates(symbol=symbol, time_frame=self.tf)
306
+ data = rate.get_historical_data(
307
+ date_from=self.start, date_to=self.end
308
+ )
309
309
  if data is None:
310
310
  raise ValueError(f"No data found for {symbol}")
311
311
  data.to_csv(data_dir / f'{symbol}.csv')
@@ -359,16 +359,27 @@ class YFHistoricDataHandler(BaseCSVDataHandler):
359
359
  return cache_dir
360
360
 
361
361
 
362
+ # TODO # Get data from EODHD
363
+ class EODHDHistoricDataHandler(BaseCSVDataHandler):
364
+ ...
365
+
362
366
  # TODO # Get data from FinancialModelingPrep ()
363
367
  class FMPHistoricDataHandler(BaseCSVDataHandler):
364
368
  ...
365
369
 
366
370
 
367
371
  class BaseFMPDataHanler(object):
372
+ """
373
+ This will serve as the base class for all other FMP data
374
+ that is not historical data and does not have an OHLC structure.
375
+ """
368
376
  ...
369
377
 
370
378
 
371
379
  class FMPFundamentalDataHandler(BaseFMPDataHanler):
372
380
  ...
373
381
 
374
- # TODO Add other Handlers
382
+ # TODO Add other Handlers for FMP
383
+
384
+
385
+ # TODO Add data Handlers for Interactive Brokers
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime
2
+ from typing import Literal
2
3
 
3
4
  __all__ = [
4
5
  "Event",
@@ -150,8 +151,8 @@ class FillEvent(Event):
150
151
  symbol: str,
151
152
  exchange: str,
152
153
  quantity: int | float,
153
- direction: str,
154
- fill_cost: int | float,
154
+ direction: Literal['LONG', 'SHORT', 'EXIT'],
155
+ fill_cost: int | float | None,
155
156
  commission: float | None = None
156
157
  ):
157
158
  """
@@ -168,7 +169,7 @@ class FillEvent(Event):
168
169
  symbol (str): The instrument which was filled.
169
170
  exchange (str): The exchange where the order was filled.
170
171
  quantity (int | float): The filled quantity.
171
- direction (str): The direction of fill ('BUY' or 'SELL')
172
+ direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
172
173
  fill_cost (int | float): The holdings value in dollars.
173
174
  commission (float | None): An optional commission sent from IB.
174
175
  """
@@ -51,7 +51,7 @@ class SimulatedExecutionHandler(ExecutionHandler):
51
51
  handler.
52
52
  """
53
53
 
54
- def __init__(self, events: Queue):
54
+ def __init__(self, events: Queue, **kwargs):
55
55
  """
56
56
  Initialises the handler, setting the event queues
57
57
  up internally.
@@ -1,4 +1,5 @@
1
1
  from abc import ABCMeta, abstractmethod
2
+ from typing import Dict, Union
2
3
 
3
4
 
4
5
  class Strategy(metaclass=ABCMeta):
@@ -22,9 +23,10 @@ class Strategy(metaclass=ABCMeta):
22
23
  """
23
24
 
24
25
  @abstractmethod
25
- def calculate_signals(self):
26
+ def calculate_signals(self, *args, **kwargs) -> Dict[str, Union[str, None]]:
26
27
  """
27
28
  Provides the mechanisms to calculate the list of signals.
29
+ This methods should return a dictionary of symbols and their respective signals.
28
30
  """
29
31
  raise NotImplementedError(
30
32
  "Should implement calculate_signals()"
@@ -555,13 +555,19 @@ class Account(object):
555
555
 
556
556
  Raises:
557
557
  MT5TerminalError: A specific exception based on the error code.
558
+
559
+ Notes:
560
+ The `time` property is converted to a `datetime` object using Broker server time.
558
561
  """
559
562
  try:
560
563
  symbol_info = mt5.symbol_info(symbol)
561
564
  if symbol_info is None:
562
565
  return None
563
566
  else:
564
- return SymbolInfo(**symbol_info._asdict())
567
+ symbol_info_dict = symbol_info._asdict()
568
+ time = datetime.fromtimestamp(symbol_info.time)
569
+ symbol_info_dict['time'] = time
570
+ return SymbolInfo(**symbol_info_dict)
565
571
  except Exception as e:
566
572
  msg = self._symbol_info_msg(symbol)
567
573
  raise_mt5_error(message=f"{e+msg}")
@@ -595,13 +601,19 @@ class Account(object):
595
601
 
596
602
  Raises:
597
603
  MT5TerminalError: A specific exception based on the error code.
604
+
605
+ Notes:
606
+ The `time` property is converted to a `datetime` object using Broker server time.
598
607
  """
599
608
  try:
600
609
  tick_info = mt5.symbol_info_tick(symbol)
601
610
  if tick_info is None:
602
611
  return None
603
612
  else:
604
- return TickInfo(**tick_info._asdict())
613
+ info_dict = tick_info._asdict()
614
+ time = datetime.fromtimestamp(tick_info.time)
615
+ info_dict['time'] = time
616
+ return TickInfo(**info_dict)
605
617
  except Exception as e:
606
618
  msg = self._symbol_info_msg(symbol)
607
619
  raise_mt5_error(message=f"{e+msg}")
@@ -615,6 +627,38 @@ class Account(object):
615
627
  """
616
628
  self._show_info(self.get_tick_info, "tick", symbol=symbol)
617
629
 
630
+ def calculate_margin(self,
631
+ action: Literal['buy', 'sell'],
632
+ symbol: str,
633
+ lot: float,
634
+ price: float) -> float:
635
+ """
636
+ Calculate margin required for an order.
637
+
638
+ Args:
639
+ action (str): The trading action, either 'buy' or 'sell'.
640
+ symbol (str): The symbol of the financial instrument.
641
+ lot (float): The lot size of the order.
642
+ price (float): The price of the order.
643
+
644
+ Returns:
645
+ float: The margin required for the order.
646
+
647
+ Raises:
648
+ MT5TerminalError: A specific exception based on the error code.
649
+ """
650
+ _action = {
651
+ 'buy': mt5.ORDER_TYPE_BUY,
652
+ 'sell': mt5.ORDER_TYPE_SELL
653
+ }
654
+ try:
655
+ margin = mt5.order_calc_margin(_action[action], symbol, lot, price)
656
+ if margin is None:
657
+ return None
658
+ return margin
659
+ except Exception as e:
660
+ raise_mt5_error(e)
661
+
618
662
  def check_order(self,
619
663
  request: Dict[str, Any]) -> OrderCheckResult:
620
664
  """
@@ -21,6 +21,10 @@ class Rates(object):
21
21
  flexibility in retrieving data either by specifying a starting position
22
22
  and count of bars or by providing a specific date range.
23
23
 
24
+ Notes:
25
+ The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
26
+ get_volume` properties return data in Broker's timezone.
27
+
24
28
  Example:
25
29
  >>> rates = Rates("EURUSD", "1h")
26
30
  >>> df = rates.get_historical_data(
@@ -62,8 +66,14 @@ class Rates(object):
62
66
  self.start_pos = self._get_start_pos(start_pos, time_frame)
63
67
  self.count = count
64
68
  self._mt5_initialized()
65
- self.data = self.get_rates_from_pos()
69
+ self.__data = self.get_rates_from_pos()
70
+
66
71
 
72
+ def _mt5_initialized(self):
73
+ """Ensures the MetaTrader 5 Terminal is initialized."""
74
+ if not Mt5.initialize():
75
+ raise_mt5_error(message=INIT_MSG)
76
+
67
77
  def _get_start_pos(self, index, time_frame):
68
78
  if isinstance(index, int):
69
79
  start_pos = index
@@ -112,11 +122,6 @@ class Rates(object):
112
122
  )
113
123
  return TIMEFRAMES[time_frame]
114
124
 
115
- def _mt5_initialized(self):
116
- """Ensures the MetaTrader 5 Terminal is initialized."""
117
- if not Mt5.initialize():
118
- raise_mt5_error(message=INIT_MSG)
119
-
120
125
  def _fetch_data(
121
126
  self, start: Union[int, datetime],
122
127
  count: Union[int, datetime]
@@ -160,6 +165,13 @@ class Rates(object):
160
165
  Returns:
161
166
  Union[pd.DataFrame, None]: A DataFrame containing historical
162
167
  data if successful, otherwise None.
168
+
169
+ Raises:
170
+ ValueError: If `start_pos` or `count` is not provided during
171
+ initialization.
172
+
173
+ Notes:
174
+ The Datetime for this method is in Broker's timezone.
163
175
  """
164
176
  if self.start_pos is None or self.count is None:
165
177
  raise ValueError(
@@ -171,27 +183,38 @@ class Rates(object):
171
183
 
172
184
  @property
173
185
  def get_open(self):
174
- return self.data['Open']
186
+ return self.__data['Open']
175
187
 
176
188
  @property
177
189
  def get_high(self):
178
- return self.data['High']
190
+ return self.__data['High']
179
191
 
180
192
  @property
181
193
  def get_low(self):
182
- return self.data['Low']
194
+ return self.__data['Low']
183
195
 
184
196
  @property
185
197
  def get_close(self):
186
- return self.data['Close']
198
+ return self.__data['Close']
187
199
 
188
200
  @property
189
201
  def get_adj_close(self):
190
- return self.data['Adj Close']
202
+ return self.__data['Adj Close']
191
203
 
192
204
  @property
193
205
  def get_returns(self):
194
- data = self.data.copy()
206
+ """
207
+ Fractional change between the current and a prior element.
208
+
209
+ Computes the fractional change from the immediately previous row by default.
210
+ This is useful in comparing the fraction of change in a time series of elements.
211
+
212
+ Note
213
+ ----
214
+ It calculates fractional change (also known as `per unit change or relative change`)
215
+ and `not percentage change`. If you need the percentage change, multiply these values by 100.
216
+ """
217
+ data = self.__data.copy()
195
218
  data['Returns'] = data['Adj Close'].pct_change()
196
219
  data = data.dropna()
197
220
  return data['Returns']
@@ -219,6 +242,12 @@ class Rates(object):
219
242
  Returns:
220
243
  Union[pd.DataFrame, None]: A DataFrame containing historical data
221
244
  if successful, otherwise None.
245
+
246
+ Raises:
247
+ ValueError: If the starting date is greater than the ending date.
248
+
249
+ Notes:
250
+ The Datetime for this method is in UTC timezone.
222
251
  """
223
252
  df = self._fetch_data(date_from, date_to)
224
253
  if save_csv and df is not None:
@@ -1,4 +1,5 @@
1
1
  import random
2
+ import re
2
3
  import numpy as np
3
4
  from scipy.stats import norm
4
5
  from datetime import datetime
@@ -67,6 +68,8 @@ class RiskManagement(Account):
67
68
  max_trades: Optional[int] = None,
68
69
  std_stop: bool = False,
69
70
  pchange_sl: Optional[float] = None,
71
+ var_level: float = 0.95,
72
+ var_time_frame: TimeFrame = 'D1',
70
73
  account_leverage: bool = True,
71
74
  time_frame: TimeFrame = 'D1',
72
75
  start_time: str = "1:00",
@@ -91,6 +94,9 @@ class RiskManagement(Account):
91
94
  On `historical volatility` of the trading instrument. Defaults to False.
92
95
  pchange_sl (float, optional): If set, the Stop loss is calculated based
93
96
  On `percentage change` of the trading instrument.
97
+ var_level (float, optional): Confidence level for Value-at-Risk,e.g., 0.99 for 99% confidence interval.
98
+ The default is 0.95.
99
+ var_time_frame (str, optional): Time frame to use to calculate the VaR.
94
100
  account_leverage (bool, optional): If set to True the account leverage will be used
95
101
  In risk management setting. Defaults to False.
96
102
  time_frame (str, optional): The time frame on which the program is working
@@ -118,7 +124,9 @@ class RiskManagement(Account):
118
124
  if be is not None and (not isinstance(be, int) or be <= 0):
119
125
  raise ValueError("be must be a positive integer number")
120
126
  if time_frame not in TIMEFRAMES:
121
- raise ValueError("Unsupported time frame")
127
+ raise ValueError("Unsupported time frame {}".format(time_frame))
128
+ if var_time_frame not in TIMEFRAMES:
129
+ raise ValueError("Unsupported time frame {}".format(var_time_frame))
122
130
 
123
131
  self.symbol = symbol
124
132
  self.start_time = start_time
@@ -126,6 +134,8 @@ class RiskManagement(Account):
126
134
  self.max_trades = max_trades
127
135
  self.std = std_stop
128
136
  self.pchange = pchange_sl
137
+ self.var_level = var_level
138
+ self.var_tf = var_time_frame
129
139
  self.daily_dd = daily_risk
130
140
  self.max_risk = max_risk
131
141
  self.rr = rr
@@ -136,8 +146,21 @@ class RiskManagement(Account):
136
146
  self.account_leverage = account_leverage
137
147
  self.symbol_info = super().get_symbol_info(self.symbol)
138
148
 
139
- self.TF = self.get_minutes(
140
- ) if time_frame == 'D1' else TIMEFRAMES[time_frame]
149
+ self._tf = time_frame
150
+
151
+ def _convert_time_frame(self, tf: str) -> int:
152
+ """Convert time frame to minutes"""
153
+ if tf == 'D1':
154
+ tf_int = self.get_minutes()
155
+ elif 'm' in tf:
156
+ tf_int = TIMEFRAMES[tf]
157
+ elif 'h' in tf:
158
+ tf_int = int(tf[0])*60
159
+ elif tf == 'W1':
160
+ tf_int = self.get_minutes() * 5
161
+ elif tf == 'MN1':
162
+ tf_int = self.get_minutes() * 22
163
+ return tf_int
141
164
 
142
165
  def risk_level(self) -> float:
143
166
  """
@@ -194,10 +217,11 @@ class RiskManagement(Account):
194
217
  def max_trade(self) -> int:
195
218
  """calculates the maximum number of trades allowed"""
196
219
  minutes = self.get_minutes()
220
+ tf_int = self._convert_time_frame(self._tf)
197
221
  if self.max_trades is not None:
198
222
  max_trades = self.max_trades
199
223
  else:
200
- max_trades = round(minutes / self.TF)
224
+ max_trades = round(minutes / tf_int)
201
225
  return max(max_trades, 1)
202
226
 
203
227
  def get_minutes(self) -> int:
@@ -217,24 +241,22 @@ class RiskManagement(Account):
217
241
 
218
242
  return hours
219
243
 
220
- def get_std_stop(self, tf: TimeFrame = 'D1', interval: int = 252):
244
+ def get_std_stop(self):
221
245
  """
222
246
  Calculate the standard deviation-based stop loss level
223
247
  for a given financial instrument.
224
248
 
225
- Args:
226
- tf (str): Timeframe for data, default is 'D1' (Daily).
227
- interval (int): Number of historical data points to consider
228
- for calculating standard deviation, default is 252.
229
-
230
249
  Returns:
231
250
  - Standard deviation-based stop loss level, rounded to the nearest point.
232
251
  - 0 if the calculated stop loss is less than or equal to 0.
233
252
  """
234
- rate = Rates(self.symbol, tf, 0, interval)
235
- data = rate.get_rates_from_pos()
236
- returns = np.diff(data['Close'])
237
- std = np.std(returns)
253
+ minutes = self.get_minutes()
254
+ tf_int = self._convert_time_frame(self._tf)
255
+ interval = round((minutes / tf_int) * 252)
256
+
257
+ rate = Rates(self.symbol, self._tf, 0, interval)
258
+ returns = rate.get_returns*100
259
+ std = returns.std()
238
260
  point = self.get_symbol_info(self.symbol).point
239
261
  av_price = (self.symbol_info.bid + self.symbol_info.ask)/2
240
262
  price_interval = av_price * ((100-std))/100
@@ -271,26 +293,27 @@ class RiskManagement(Account):
271
293
  # Use std as default pchange
272
294
  return self.get_std_stop()
273
295
 
274
- def calculate_var(self, tf: TimeFrame = 'D1', interval=252, c=0.95):
296
+ def calculate_var(self, tf: TimeFrame = 'D1', c=0.95):
275
297
  """
276
298
  Calculate Value at Risk (VaR) for a given portfolio.
277
299
 
278
300
  Args:
279
301
  tf (str): Time frame to use to calculate volatility.
280
- interval (int): How many periods to use based on time frame.
281
302
  c (float): Confidence level for VaR calculation (default is 95%).
282
303
 
283
304
  Returns:
284
305
  - VaR value
285
306
  """
307
+ minutes = self.get_minutes()
308
+ tf_int = self._convert_time_frame(tf)
309
+ interval = round((minutes / tf_int) * 252)
310
+
286
311
  rate = Rates(self.symbol, tf, 0, interval)
287
- prices = rate.get_rates_from_pos()
288
- prices['return'] = prices['Close'].pct_change()
289
- prices.dropna(inplace=True)
290
- P = self.get_account_info().margin_free
291
- mu = np.mean(prices['return'])
292
- sigma = np.std(prices['return'])
293
- var = self.var_cov_var(P, c, mu, sigma)
312
+ returns = rate.get_returns*100
313
+ p = self.get_account_info().margin_free
314
+ mu = returns.mean()
315
+ sigma = returns.std()
316
+ var = self.var_cov_var(p, c, mu, sigma)
294
317
  return var
295
318
 
296
319
  def var_cov_var(self, P: float, c: float, mu: float, sigma: float):
@@ -312,11 +335,15 @@ class RiskManagement(Account):
312
335
  def var_loss_value(self):
313
336
  """
314
337
  Calculate the stop-loss level based on VaR.
338
+
339
+ Notes:
340
+ The Var is Estimated using the Variance-Covariance method on the daily returns.
341
+ If you want to use the VaR for a different time frame .
315
342
  """
316
343
  P = self.get_account_info().margin_free
317
344
  trade_risk = self.get_trade_risk()
318
345
  loss_allowed = P * trade_risk
319
- var = self.calculate_var()
346
+ var = self.calculate_var(c=self.var_level, tf=self.var_tf)
320
347
  return min(var, loss_allowed)
321
348
 
322
349
  def get_take_profit(self) -> int:
@@ -480,13 +507,8 @@ class RiskManagement(Account):
480
507
  if self.get_symbol_type(self.symbol) == 'IDX':
481
508
  rates = self.get_currency_rates(self.symbol)
482
509
  if rates['mc'] == rates['pc'] == 'JPY':
483
- if self.std:
484
- raise ValueError(
485
- f"""Please Set std=False or use pchange_sl=True
486
- or set sl=value or use the default method calculation for {self.symbol}
487
- Currency risk"""
488
- )
489
510
  lot = lot * contract_size
511
+ lot = self._check_lot(lot)
490
512
  volume = round(lot * av_price * contract_size)
491
513
  if contract_size == 1:
492
514
  volume = round(lot * av_price)
@@ -14,7 +14,7 @@ from bbstrader.metatrader.utils import (
14
14
  )
15
15
 
16
16
  # Configure the logger
17
- logger = config_logger('trade.log', console_log=True)
17
+ logger = config_logger('trade.log', console_log=False)
18
18
 
19
19
  class Trade(RiskManagement):
20
20
  """
@@ -160,7 +160,7 @@ class Trade(RiskManagement):
160
160
  print()
161
161
  self.risk_managment()
162
162
  print(
163
- f">>> Everything is OK, @{self.expert_name} is Running ....>>>\n")
163
+ f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
164
164
 
165
165
  def initialize(self):
166
166
  """
@@ -416,7 +416,7 @@ class Trade(RiskManagement):
416
416
  request["action"] = Mt5.TRADE_ACTION_PENDING
417
417
  request["type"] = self._order_type()[action][0]
418
418
 
419
- self.break_even(comment)
419
+ self.break_even(mm=mm)
420
420
  if self.check(comment):
421
421
  self.request_result(_price, request, action),
422
422
 
@@ -555,7 +555,7 @@ class Trade(RiskManagement):
555
555
  check_result = self.check_order(request)
556
556
  result = self.send_order(request)
557
557
  except Exception as e:
558
- print(f"{self.get_current_time()} -", end=' ')
558
+ print(f"{self.current_datetime()} -", end=' ')
559
559
  trade_retcode_message(
560
560
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
561
561
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
@@ -571,7 +571,7 @@ class Trade(RiskManagement):
571
571
  check_result = self.check_order(request)
572
572
  result = self.send_order(request)
573
573
  except Exception as e:
574
- print(f"{self.get_current_time()} -", end=' ')
574
+ print(f"{self.current_datetime()} -", end=' ')
575
575
  trade_retcode_message(
576
576
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
577
577
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
@@ -767,7 +767,7 @@ class Trade(RiskManagement):
767
767
  return True
768
768
  return False
769
769
 
770
- def break_even(self, id: Optional[int] = None):
770
+ def break_even(self, mm=True, id: Optional[int] = None):
771
771
  """
772
772
  Checks if it's time to put the break even,
773
773
  if so , it will sets the break even ,and if the break even was already set,
@@ -776,8 +776,11 @@ class Trade(RiskManagement):
776
776
 
777
777
  Args:
778
778
  id (int): The strategy Id or Expert Id
779
+ mm (bool): Weither to manage the position or not
779
780
  """
780
781
  time.sleep(0.1)
782
+ if not mm:
783
+ return
781
784
  Id = id if id is not None else self.expert_id
782
785
  positions = self.get_positions(symbol=self.symbol)
783
786
  be = self.get_break_even()
@@ -900,7 +903,7 @@ class Trade(RiskManagement):
900
903
  check_result = self.check_order(request)
901
904
  result = self.send_order(request)
902
905
  except Exception as e:
903
- print(f"{self.get_current_time()} -", end=' ')
906
+ print(f"{self.current_datetime()} -", end=' ')
904
907
  trade_retcode_message(
905
908
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
906
909
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
@@ -917,7 +920,7 @@ class Trade(RiskManagement):
917
920
  check_result = self.check_order(request)
918
921
  result = self.send_order(request)
919
922
  except Exception as e:
920
- print(f"{self.get_current_time()} -", end=' ')
923
+ print(f"{self.current_datetime()} -", end=' ')
921
924
  trade_retcode_message(
922
925
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
923
926
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
@@ -1041,7 +1044,7 @@ class Trade(RiskManagement):
1041
1044
  check_result = self.check_order(request)
1042
1045
  result = self.send_order(request)
1043
1046
  except Exception as e:
1044
- print(f"{self.get_current_time()} -", end=' ')
1047
+ print(f"{self.current_datetime()} -", end=' ')
1045
1048
  trade_retcode_message(
1046
1049
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1047
1050
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
@@ -1055,7 +1058,7 @@ class Trade(RiskManagement):
1055
1058
  check_result = self.check_order(request)
1056
1059
  result = self.send_order(request)
1057
1060
  except Exception as e:
1058
- print(f"{self.get_current_time()} -", end=' ')
1061
+ print(f"{self.current_datetime()} -", end=' ')
1059
1062
  trade_retcode_message(
1060
1063
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1061
1064
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
@@ -1111,13 +1114,13 @@ class Trade(RiskManagement):
1111
1114
 
1112
1115
  if len(tickets) == 0:
1113
1116
  logger.info(
1114
- f"ALL {position_type.upper()} Positions closed, SYMBOL={self.symbol}.")
1117
+ f"ALL {pos_type.upper()} Positions closed, SYMBOL={self.symbol}.")
1115
1118
  else:
1116
1119
  logger.info(
1117
- f"{len(tickets)} {position_type.upper()} Positions not closed, SYMBOL={self.symbol}")
1120
+ f"{len(tickets)} {pos_type.upper()} Positions not closed, SYMBOL={self.symbol}")
1118
1121
  else:
1119
1122
  logger.info(
1120
- f"No {position_type.upper()} Positions to close, SYMBOL={self.symbol}.")
1123
+ f"No {pos_type.upper()} Positions to close, SYMBOL={self.symbol}.")
1121
1124
 
1122
1125
  def get_stats(self) -> Tuple[Dict[str, Any]]:
1123
1126
  """
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  import MetaTrader5 as MT5
2
3
  import logging
3
4
  from typing import List, NamedTuple, Optional
@@ -216,7 +217,7 @@ class SymbolInfo(NamedTuple):
216
217
  volume: int
217
218
  volumehigh: int
218
219
  volumelow: int
219
- time: int
220
+ time: datetime
220
221
  digits: int
221
222
  spread: int
222
223
  spread_float: bool
@@ -316,7 +317,7 @@ class TickInfo(NamedTuple):
316
317
  * flags: Tick flags
317
318
  * volume_real: Volume for the current Last price with greater accuracy
318
319
  """
319
- time: int
320
+ time: datetime
320
321
  bid: float
321
322
  ask: float
322
323
  last: float
@@ -165,10 +165,13 @@ def sma_trading(
165
165
  time.sleep((60 * iter_time) - 1.5)
166
166
  if iter_time == 1:
167
167
  time_intervals += 1
168
- elif iter_time == trade_time:
169
- time_intervals += trade_time
168
+ elif trade_time % iter_time == 0:
169
+ time_intervals += iter_time
170
170
  else:
171
- time_intervals += (trade_time/iter_time)
171
+ raise ValueError(
172
+ f"iter_time must be a multiple of the {tf} !!!"
173
+ f"(e.g; if time_frame is 15m, iter_time must be 1.5, 3, 3, 15 etc)"
174
+ )
172
175
  if period.lower() == 'month':
173
176
  if trade.days_end() and today != 'Friday':
174
177
  sleep_time = trade.sleep_time()
@@ -484,10 +487,13 @@ def pair_trading(
484
487
 
485
488
  if iter_time == 1:
486
489
  time_intervals += 1
487
- elif iter_time == trade_time:
488
- time_intervals += trade_time
490
+ elif trade_time % iter_time == 0:
491
+ time_intervals += iter_time
489
492
  else:
490
- time_intervals += (trade_time/iter_time)
493
+ raise ValueError(
494
+ f"iter_time must be a multiple of the {tf} !!!"
495
+ f"(e.g; if time_frame is 15m, iter_time must be 1.5, 3, 3, 15 etc)"
496
+ )
491
497
 
492
498
  if period.lower() == 'month':
493
499
  if p0.days_end() and today != 'Friday':
@@ -706,10 +712,13 @@ def ou_trading(
706
712
  time.sleep((60 * iter_time) - 1.5)
707
713
  if iter_time == 1:
708
714
  time_intervals += 1
709
- elif iter_time == trade_time:
710
- time_intervals += trade_time
715
+ elif trade_time % iter_time == 0:
716
+ time_intervals += iter_time
711
717
  else:
712
- time_intervals += (trade_time/iter_time)
718
+ raise ValueError(
719
+ f"iter_time must be a multiple of the {tf} !!!"
720
+ f"(e.g; if time_frame is 15m, iter_time must be 1.5, 3, 3, 15 etc)"
721
+ )
713
722
 
714
723
  if period.lower() == 'month':
715
724
  if trade.days_end() and today != 'Friday':
@@ -918,10 +927,13 @@ def arch_trading(
918
927
  time.sleep((60 * iter_time) - 1.5)
919
928
  if iter_time == 1:
920
929
  time_intervals += 1
921
- elif iter_time == trade_time:
922
- time_intervals += trade_time
930
+ elif trade_time % iter_time == 0:
931
+ time_intervals += iter_time
923
932
  else:
924
- time_intervals += (trade_time/iter_time)
933
+ raise ValueError(
934
+ f"iter_time must be a multiple of the {tf} !!!"
935
+ f"(e.g; if time_frame is 15m, iter_time must be 1.5, 3, 3, 15 etc)"
936
+ )
925
937
 
926
938
  if period.lower() == 'month':
927
939
  if trade.days_end() and today != 'Friday':
@@ -1,12 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bbstrader
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Simplified Investment & Trading Toolkit
5
5
  Home-page: https://github.com/bbalouki/bbstrader
6
+ Download-URL: https://pypi.org/project/bbstrader/
6
7
  Author: Bertin Balouki SIMYELI
7
8
  Author-email: <bertin@bbstrader.com>
8
9
  Maintainer: Bertin Balouki SIMYELI
9
10
  License: The MIT License (MIT)
11
+ Project-URL: Documentation, https://bbstrader.readthedocs.io/en/latest/
12
+ Project-URL: Source Code, https://github.com/bbalouki/bbstrader
10
13
  Keywords: Finance,Toolkit,Financial,Analysis,Fundamental,Quantitative,Database,Equities,Currencies,Economics,ETFs,Funds,Indices,Moneymarkets,Commodities,Futures,CFDs,Derivatives,Trading,Investing,Portfolio,Optimization,Performance
11
14
  Classifier: Development Status :: 5 - Production/Stable
12
15
  Classifier: Intended Audience :: Developers
@@ -7,7 +7,7 @@ if sys.version_info < (3, 10):
7
7
  with open("README.md", encoding="utf-8") as fh:
8
8
  long_description = fh.read()
9
9
 
10
- VERSION = '0.1.04'
10
+ VERSION = '0.1.06'
11
11
  DESCRIPTION = 'Simplified Investment & Trading Toolkit'
12
12
 
13
13
  KEYWORDS = [
@@ -44,6 +44,11 @@ setup(
44
44
  version=VERSION,
45
45
  author='Bertin Balouki SIMYELI',
46
46
  url='https://github.com/bbalouki/bbstrader',
47
+ download_url='https://pypi.org/project/bbstrader/',
48
+ project_urls={
49
+ "Documentation": "https://bbstrader.readthedocs.io/en/latest/",
50
+ "Source Code": "https://github.com/bbalouki/bbstrader",
51
+ },
47
52
  license='The MIT License (MIT)',
48
53
  author_email='<bertin@bbstrader.com>',
49
54
  maintainer='Bertin Balouki SIMYELI',
File without changes
File without changes
File without changes