Qubx 0.5.3__tar.gz → 0.5.5__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 Qubx might be problematic. Click here for more details.

Files changed (97) hide show
  1. {qubx-0.5.3 → qubx-0.5.5}/PKG-INFO +1 -1
  2. {qubx-0.5.3 → qubx-0.5.5}/pyproject.toml +1 -1
  3. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/management.py +23 -1
  4. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/simulator.py +4 -1
  5. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/interfaces.py +34 -2
  6. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/metrics.py +85 -7
  7. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/trackers/sizers.py +54 -0
  8. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/runner/configs.py +2 -0
  9. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/runner/runner.py +47 -9
  10. {qubx-0.5.3 → qubx-0.5.5}/README.md +0 -0
  11. {qubx-0.5.3 → qubx-0.5.5}/build.py +0 -0
  12. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/__init__.py +0 -0
  13. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/_nb_magic.py +0 -0
  14. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/__init__.py +0 -0
  15. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/account.py +0 -0
  16. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/broker.py +0 -0
  17. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/data.py +0 -0
  18. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/ome.py +0 -0
  19. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/optimization.py +0 -0
  20. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/simulated_data.py +0 -0
  21. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/backtester/utils.py +0 -0
  22. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/cli/__init__.py +0 -0
  23. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/cli/commands.py +0 -0
  24. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/connectors/ccxt/__init__.py +0 -0
  25. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/connectors/ccxt/account.py +0 -0
  26. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/connectors/ccxt/broker.py +0 -0
  27. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/connectors/ccxt/customizations.py +0 -0
  28. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/connectors/ccxt/data.py +0 -0
  29. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  30. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/connectors/ccxt/factory.py +0 -0
  31. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/connectors/ccxt/utils.py +0 -0
  32. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/__init__.py +0 -0
  33. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/account.py +0 -0
  34. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/basics.py +0 -0
  35. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/context.py +0 -0
  36. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/exceptions.py +0 -0
  37. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/helpers.py +0 -0
  38. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/loggers.py +0 -0
  39. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/lookups.py +0 -0
  40. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/mixins/__init__.py +0 -0
  41. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/mixins/market.py +0 -0
  42. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/mixins/processing.py +0 -0
  43. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/mixins/subscription.py +0 -0
  44. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/mixins/trading.py +0 -0
  45. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/mixins/universe.py +0 -0
  46. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/series.pxd +0 -0
  47. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/series.pyi +0 -0
  48. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/series.pyx +0 -0
  49. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/utils.pyi +0 -0
  50. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/core/utils.pyx +0 -0
  51. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/data/__init__.py +0 -0
  52. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/data/helpers.py +0 -0
  53. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/data/readers.py +0 -0
  54. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/data/tardis.py +0 -0
  55. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/gathering/simplest.py +0 -0
  56. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/math/__init__.py +0 -0
  57. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/math/stats.py +0 -0
  58. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/pandaz/__init__.py +0 -0
  59. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/pandaz/ta.py +0 -0
  60. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/pandaz/utils.py +0 -0
  61. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
  62. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/resources/instruments/symbols-binance.json +0 -0
  63. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
  64. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
  65. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
  66. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
  67. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
  68. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/ta/__init__.py +0 -0
  69. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/ta/indicators.pxd +0 -0
  70. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/ta/indicators.pyi +0 -0
  71. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/ta/indicators.pyx +0 -0
  72. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/trackers/__init__.py +0 -0
  73. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/trackers/abvanced.py +0 -0
  74. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/trackers/composite.py +0 -0
  75. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/trackers/rebalancers.py +0 -0
  76. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/trackers/riskctrl.py +0 -0
  77. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/__init__.py +0 -0
  78. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/_pyxreloader.py +0 -0
  79. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/charting/lookinglass.py +0 -0
  80. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  81. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/marketdata/binance.py +0 -0
  82. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/marketdata/ccxt.py +0 -0
  83. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/marketdata/dukas.py +0 -0
  84. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/misc.py +0 -0
  85. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/ntp.py +0 -0
  86. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/numbers_utils.py +0 -0
  87. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/orderbook.py +0 -0
  88. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/plotting/__init__.py +0 -0
  89. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/plotting/dashboard.py +0 -0
  90. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/plotting/data.py +0 -0
  91. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/plotting/interfaces.py +0 -0
  92. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  93. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  94. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/runner/__init__.py +0 -0
  95. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  96. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/runner/accounts.py +0 -0
  97. {qubx-0.5.3 → qubx-0.5.5}/src/qubx/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Qubx
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: Qubx - quantitative trading framework
5
5
  Home-page: https://github.com/dmarienko/Qubx
6
6
  Author: Dmitry Marienko
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Qubx"
3
- version = "0.5.3"
3
+ version = "0.5.5"
4
4
  description = "Qubx - quantitative trading framework"
5
5
  authors = [
6
6
  "Dmitry Marienko <dmitry@gmail.com>",
@@ -90,8 +90,12 @@ class BacktestsResultsManager:
90
90
  stop = pd.Timestamp(info.get("stop", "")).round("1s")
91
91
  dscr = info.get("description", "")
92
92
  _s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(pd.Timestamp(info.get('creation_time', '')).round('1s'))} by {cyan(info.get('author', ''))}"
93
+
93
94
  if dscr:
94
- _s += f"\n\t{magenta(dscr)}"
95
+ dscr = dscr.split("\n")
96
+ for _d in dscr:
97
+ _s += f"\n\t{magenta('# ' + _d)}"
98
+
95
99
  _s += f"\n\tstrategy: {green(s_cls)}"
96
100
  _s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
97
101
  _s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
@@ -117,3 +121,21 @@ class BacktestsResultsManager:
117
121
  for i in _m_repr:
118
122
  print("\t " + cyan(i))
119
123
  print()
124
+
125
+ def delete(self, name: str | int):
126
+ print(red(f" -> Danger zone - you are about to delete {name} ..."))
127
+ for info in self.results.values():
128
+ match name:
129
+ case int():
130
+ if info.get("idx", -1) == name:
131
+ Path(info["path"]).unlink()
132
+ print(f" -> Deleted {red(name)} ...")
133
+ self.reload()
134
+ return
135
+ case str():
136
+ if info.get("name", "") == name:
137
+ Path(info["path"]).unlink()
138
+ print(f" -> Deleted {red(name)} ...")
139
+ self.reload()
140
+ return
141
+ print(f" -> No results found for {red(name)} !")
@@ -57,6 +57,7 @@ def simulate(
57
57
  open_close_time_indent_secs=1,
58
58
  debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
59
59
  show_latency_report: bool = False,
60
+ parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
60
61
  ) -> list[TradingSessionResult]:
61
62
  """
62
63
  Backtest utility for trading strategies or signals using historical data.
@@ -149,6 +150,7 @@ def simulate(
149
150
  n_jobs=n_jobs,
150
151
  silent=silent,
151
152
  show_latency_report=show_latency_report,
153
+ parallel_backend=parallel_backend,
152
154
  )
153
155
 
154
156
 
@@ -160,6 +162,7 @@ def _run_setups(
160
162
  n_jobs: int = -1,
161
163
  silent: bool = False,
162
164
  show_latency_report: bool = False,
165
+ parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
163
166
  ) -> list[TradingSessionResult]:
164
167
  # loggers don't work well with joblib and multiprocessing in general because they contain
165
168
  # open file handlers that cannot be pickled. I found a solution which requires the usage of enqueue=True
@@ -170,7 +173,7 @@ def _run_setups(
170
173
  n_jobs = 1 if _main_loop_silent else n_jobs
171
174
 
172
175
  reports = ProgressParallel(
173
- n_jobs=n_jobs, total=len(strategies_setups), silent=_main_loop_silent, backend="multiprocessing"
176
+ n_jobs=n_jobs, total=len(strategies_setups), silent=_main_loop_silent, backend=parallel_backend
174
177
  )(
175
178
  delayed(_run_setup)(id, f"Simulated-{id}", setup, data_setup, start, stop, silent, show_latency_report)
176
179
  for id, setup in enumerate(strategies_setups)
@@ -1003,6 +1003,18 @@ class PositionsTracker:
1003
1003
  ...
1004
1004
 
1005
1005
 
1006
+ def _unpickle_instance(chain: tuple[type], state: dict):
1007
+ """
1008
+ chain is a tuple of the *original* classes, e.g. (A, B, C).
1009
+ Reconstruct a new ephemeral class that inherits from them.
1010
+ """
1011
+ name = "_".join(cls.__name__ for cls in chain)
1012
+ # Reverse the chain to respect the typical left-to-right MRO
1013
+ inst = type(name, chain[::-1], {"__module__": "__main__"})()
1014
+ inst.__dict__.update(state)
1015
+ return inst
1016
+
1017
+
1006
1018
  class Mixable(type):
1007
1019
  """
1008
1020
  It's possible to create composite strategies dynamically by adding mixins with functionality.
@@ -1011,8 +1023,28 @@ class Mixable(type):
1011
1023
  NewStrategy(....) can be used in simulation or live trading.
1012
1024
  """
1013
1025
 
1014
- def __add__(cls: type, other_cls: type):
1015
- return type(cls)(f"{cls.__name__}_{other_cls.__name__}", (other_cls, cls), {"__module__": cls.__module__})
1026
+ def __add__(cls, other_cls):
1027
+ # If we already have a _composition, combine them;
1028
+ # else treat cls itself as the start of the chain
1029
+ cls_chain = getattr(cls, "__composition__", (cls,))
1030
+ other_chain = getattr(other_cls, "__composition__", (other_cls,))
1031
+
1032
+ # Combine them into one chain. You can define your own order rules:
1033
+ new_chain = cls_chain + other_chain
1034
+
1035
+ # Create ephemeral class
1036
+ name = "_".join(c.__name__ for c in new_chain)
1037
+
1038
+ def __reduce__(self):
1039
+ # Just return the chain of *original real classes*
1040
+ return _unpickle_instance, (new_chain, self.__dict__)
1041
+
1042
+ new_cls = type(
1043
+ name,
1044
+ new_chain[::-1],
1045
+ {"__module__": cls.__module__, "__composition__": new_chain, "__reduce__": __reduce__},
1046
+ )
1047
+ return new_cls
1016
1048
 
1017
1049
 
1018
1050
  class IStrategy(metaclass=Mixable):
@@ -609,6 +609,7 @@ class TradingSessionResult:
609
609
  creation_time: pd.Timestamp | None = None # when result was created
610
610
  author: str | None = None # who created the result
611
611
  qubx_version: str | None = None # Qubx version used to create the result
612
+ _metrics: dict[str, float] | None = None # performance metrics
612
613
  # fmt: on
613
614
 
614
615
  def __init__(
@@ -649,6 +650,39 @@ class TradingSessionResult:
649
650
  self.creation_time = pd.Timestamp(creation_time) if creation_time else pd.Timestamp.now()
650
651
  self.author = author
651
652
  self.qubx_version = version()
653
+ self._metrics = None
654
+
655
+ def performance(self) -> dict[str, float]:
656
+ """
657
+ Calculate performance metrics for the trading session
658
+ """
659
+ if not self._metrics:
660
+ # - caluclate short statistics
661
+ self._metrics = portfolio_metrics(
662
+ self.portfolio_log,
663
+ self.executions_log,
664
+ self.capital,
665
+ performance_statistics_period=DAILY_365,
666
+ account_transactions=True,
667
+ commission_factor=1,
668
+ )
669
+ # - convert timestamps to isoformat
670
+ for k, v in self._metrics.items():
671
+ match v:
672
+ case pd.Timestamp():
673
+ self._metrics[k] = v.isoformat()
674
+ case np.float64():
675
+ self._metrics[k] = float(v)
676
+ # fmt: off
677
+ for k in [
678
+ "equity", "drawdown_usd", "drawdown_pct",
679
+ "compound_returns", "returns_daily", "returns", "monthly_returns",
680
+ "rolling_sharpe", "long_value", "short_value",
681
+ ]:
682
+ self._metrics.pop(k, None)
683
+ # fmt: on
684
+
685
+ return self._metrics
652
686
 
653
687
  @property
654
688
  def symbols(self) -> list[str]:
@@ -690,6 +724,7 @@ class TradingSessionResult:
690
724
  "author": self.author,
691
725
  "qubx_version": self.qubx_version,
692
726
  "symbols": self.symbols,
727
+ "performance": dict(self.performance()),
693
728
  }
694
729
 
695
730
  def to_html(self, compound=True) -> HTML:
@@ -743,8 +778,42 @@ class TradingSessionResult:
743
778
  """
744
779
  return HTML(_tmpl)
745
780
 
746
- def to_file(self, name: str, description: str | None = None, compound=True, archive=True):
747
- name = (name + self.creation_time.strftime("%Y%m%d%H%M%S")) if self.creation_time else name
781
+ def to_file(
782
+ self,
783
+ name: str,
784
+ description: str | None = None,
785
+ compound=True,
786
+ archive=True,
787
+ suffix: str | None = None,
788
+ attachments: list[str] | None = None,
789
+ ):
790
+ """
791
+ Save the trading session results to files.
792
+
793
+ Args:
794
+ name (str): Base name/path for saving the files
795
+ description (str | None, optional): Description to include in info file. Defaults to None.
796
+ compound (bool, optional): Whether to use compound returns in report. Defaults to True.
797
+ archive (bool, optional): Whether to zip the output files. Defaults to True.
798
+ suffix (str | None, optional): Optional suffix to append to filename. Defaults to None.
799
+ attachments (list[str] | None, optional): Additional files to include. Defaults to None.
800
+
801
+ The following files are saved:
802
+ - info.yml: Contains strategy configuration and metadata
803
+ - portfolio.csv: Portfolio state log
804
+ - executions.csv: Trade execution log
805
+ - signals.csv: Strategy signals log
806
+ - report.html: HTML performance report
807
+ - Any provided attachment files
808
+
809
+ If archive=True, all files are zipped into a single archive and the directory is removed.
810
+ """
811
+ import shutil
812
+
813
+ if suffix is not None:
814
+ name = f"{name}{suffix}"
815
+ else:
816
+ name = (name + self.creation_time.strftime("%Y%m%d%H%M%S")) if self.creation_time else name
748
817
  p = Path(makedirs(name))
749
818
  with open(p / "info.yml", "w") as f:
750
819
  info = self.info()
@@ -761,9 +830,13 @@ class TradingSessionResult:
761
830
  with open(p / "report.html", "w") as f:
762
831
  f.write(self.to_html(compound=compound).data)
763
832
 
764
- if archive:
765
- import shutil
833
+ # - save attachments
834
+ if attachments:
835
+ for a in attachments:
836
+ if (af := Path(a)).is_file():
837
+ shutil.copy(af, p / af.name)
766
838
 
839
+ if archive:
767
840
  shutil.make_archive(name, "zip", p) # type: ignore
768
841
  shutil.rmtree(p) # type: ignore
769
842
 
@@ -784,9 +857,11 @@ class TradingSessionResult:
784
857
  # load result
785
858
  _qbx_version = info.pop("qubx_version")
786
859
  _decr = info.pop("description", None)
860
+ _perf = info.pop("performance", None)
787
861
  info["instruments"] = info.pop("symbols")
788
862
  tsr = TradingSessionResult(**info, portfolio_log=portfolio, executions_log=executions, signals_log=signals)
789
863
  tsr.qubx_version = _qbx_version
864
+ tsr._metrics = _perf
790
865
  return tsr
791
866
 
792
867
  def __repr__(self) -> str:
@@ -796,10 +871,13 @@ class TradingSessionResult:
796
871
  : QUBX: {self.qubx_version}
797
872
  : Capital: {self.capital} {self.base_currency} ({self.commissions} @ {self.exchange})
798
873
  : Instruments: [{",".join(self.symbols)}]
799
- : Generated: {len(self.signals_log)} signals, {len(self.executions_log)} executions
800
- : Strategy: {self.config(False)}
801
874
  : Created: {self.creation_time} by {self.author}
802
- """
875
+ : Strategy: {self.config(False)}
876
+ : Generated: {len(self.signals_log)} signals, {len(self.executions_log)} executions
877
+ """
878
+ _perf = pd.DataFrame.from_dict(self.performance(), orient="index").T.to_string(index=None)
879
+ for _i, s in enumerate(_perf.split("\n")):
880
+ r += f" : {s}\n" if _i > 0 else f" `----: {s}\n"
803
881
  return r
804
882
 
805
883
 
@@ -173,3 +173,57 @@ class LongShortRatioPortfolioSizer(IPositionSizer):
173
173
  t_pos.append(TargetPosition.create(ctx, signal, _p * _p_q))
174
174
 
175
175
  return t_pos
176
+
177
+
178
+ class FixedRiskSizerWithConstantCapital(IPositionSizer):
179
+ def __init__(
180
+ self,
181
+ capital: float,
182
+ max_cap_in_risk: float,
183
+ max_allowed_position=np.inf,
184
+ divide_by_symbols: bool = True,
185
+ ):
186
+ """
187
+ Create fixed risk sizer calculator instance.
188
+ :param max_cap_in_risk: maximal risked capital (in percentage)
189
+ :param max_allowed_position: limitation for max position size in quoted currency (i.e. max 5000 in USDT)
190
+ :param reinvest_profit: if true use profit to reinvest
191
+ """
192
+ self.capital = capital
193
+ assert self.capital > 0, f" >> {self.__class__.__name__}: Capital must be positive, got {self.capital}"
194
+ self.max_cap_in_risk = max_cap_in_risk / 100
195
+ self.max_allowed_position_quoted = max_allowed_position
196
+ self.divide_by_symbols = divide_by_symbols
197
+
198
+ def calculate_target_positions(self, ctx: IStrategyContext, signals: List[Signal]) -> List[TargetPosition]:
199
+ t_pos = []
200
+ for signal in signals:
201
+ target_position_size = 0
202
+ if signal.signal != 0:
203
+ if signal.stop and signal.stop > 0:
204
+ # - get signal entry price
205
+ if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
206
+ continue
207
+
208
+ # - just use same fixed capital
209
+ _cap = self.capital / (len(ctx.instruments) if self.divide_by_symbols else 1)
210
+
211
+ # fmt: off
212
+ _direction = np.sign(signal.signal)
213
+ target_position_size = (
214
+ _direction * min(
215
+ (_cap * self.max_cap_in_risk) / abs(signal.stop / _entry - 1),
216
+ self.max_allowed_position_quoted
217
+ ) / _entry
218
+ )
219
+ # fmt: on
220
+
221
+ else:
222
+ logger.warning(
223
+ f" >>> {self.__class__.__name__}: stop is not specified for {str(signal)} - can't calculate position !"
224
+ )
225
+ continue
226
+
227
+ t_pos.append(TargetPosition.create(ctx, signal, target_position_size))
228
+
229
+ return t_pos
@@ -55,6 +55,8 @@ class StrategySimulationConfig(BaseModel):
55
55
  parameters: dict = Field(default_factory=dict)
56
56
  data: dict = Field(default_factory=dict)
57
57
  simulation: dict = Field(default_factory=dict)
58
+ description: str | list[str] | None = None
59
+ variate: dict = Field(default_factory=dict)
58
60
 
59
61
 
60
62
  def load_simulation_config_from_yaml(path: Path | str) -> StrategySimulationConfig:
@@ -4,8 +4,11 @@ import time
4
4
  from functools import reduce
5
5
  from pathlib import Path
6
6
 
7
+ import pandas as pd
8
+
7
9
  from qubx import formatter, logger, lookup
8
10
  from qubx.backtester.account import SimulatedAccountProcessor
11
+ from qubx.backtester.optimization import variate
9
12
  from qubx.backtester.simulator import SimulatedBroker, simulate
10
13
  from qubx.connectors.ccxt.account import CcxtAccountProcessor
11
14
  from qubx.connectors.ccxt.broker import CcxtBroker
@@ -18,7 +21,7 @@ from qubx.core.helpers import BasicScheduler
18
21
  from qubx.core.interfaces import IAccountProcessor, IBroker, IDataProvider, IStrategyContext
19
22
  from qubx.core.loggers import StrategyLogging
20
23
  from qubx.data import DataReader
21
- from qubx.utils.misc import class_import, makedirs
24
+ from qubx.utils.misc import blue, class_import, cyan, green, magenta, makedirs, red, yellow
22
25
  from qubx.utils.runner.configs import ExchangeConfig, load_simulation_config_from_yaml, load_strategy_config_from_yaml
23
26
 
24
27
  from .accounts import AccountConfigurationManager
@@ -379,6 +382,8 @@ def simulate_strategy(
379
382
 
380
383
  cfg = load_simulation_config_from_yaml(config_file)
381
384
  stg = cfg.strategy
385
+ simulation_name = config_file.stem
386
+ _v_id = pd.Timestamp("now").strftime("%Y%m%d%H%M%S")
382
387
 
383
388
  match stg:
384
389
  case list():
@@ -388,8 +393,24 @@ def simulate_strategy(
388
393
  case _:
389
394
  raise SimulationConfigError(f"Invalid strategy type: {stg}")
390
395
 
391
- strategy = stg_cls(**cfg.parameters)
392
- exp_name = config_file.stem
396
+ # - create simulation setup
397
+ if cfg.variate:
398
+ # - get conditions for variations if exists
399
+ cond = cfg.variate.pop("with", None)
400
+ conditions = []
401
+ dict2lambda = lambda a, d: eval(f"lambda {a}: {d}") # noqa: E731
402
+ if cond:
403
+ for a, c in cond.items():
404
+ conditions.append(dict2lambda(a, c))
405
+
406
+ experiments = variate(stg_cls, **(cfg.parameters | cfg.variate), conditions=conditions)
407
+ experiments = {f"{simulation_name}.{_v_id}.[{k}]": v for k, v in experiments.items()}
408
+ print(f"Variation is enabled. There are {len(experiments)} simualtions to run.")
409
+ _n_jobs = -1
410
+ else:
411
+ strategy = stg_cls(**cfg.parameters)
412
+ experiments = {simulation_name: strategy}
413
+ _n_jobs = 1
393
414
 
394
415
  data_i = {}
395
416
 
@@ -409,13 +430,30 @@ def simulate_strategy(
409
430
  sim_params["stop"] = stop
410
431
  logger.info(f"Stop date set to {stop}")
411
432
 
412
- test_res = simulate({exp_name: strategy}, data=data_i, **sim_params)
413
- logger.info(f"<g>Simulation Results:</g>\n{str(test_res[0])}")
433
+ # - run simulation
434
+ print(f" > Run simulation for [{red(simulation_name)}] ::: {sim_params['start']} - {sim_params['stop']}")
435
+ sim_params["n_jobs"] = sim_params.get("n_jobs", _n_jobs)
436
+ test_res = simulate(experiments, data=data_i, **sim_params)
414
437
 
415
438
  _where_to_save = save_path if save_path is not None else Path("results/")
416
- s_path = Path(makedirs(str(_where_to_save))) / exp_name
417
-
418
- logger.info(f"Saving results to <g>{s_path}</g> ...")
419
- test_res[0].to_file(str(s_path))
439
+ s_path = Path(makedirs(str(_where_to_save))) / simulation_name
440
+
441
+ # logger.info(f"Saving simulation results to <g>{s_path}</g> ...")
442
+ if cfg.description is not None:
443
+ _descr = cfg.description
444
+ if isinstance(cfg.description, list):
445
+ _descr = "\n".join(cfg.description)
446
+ else:
447
+ _descr = str(cfg.description)
448
+
449
+ if len(test_res) > 1:
450
+ # - TODO: think how to deal with variations !
451
+ s_path = s_path / f"variations.{_v_id}"
452
+ print(f" > Saving variations results to <g>{s_path}</g> ...")
453
+ for k, t in enumerate(test_res):
454
+ t.to_file(str(s_path), description=_descr, suffix=f".{k}", attachments=[str(config_file)])
455
+ else:
456
+ print(f" > Saving simulation results to <g>{s_path}</g> ...")
457
+ test_res[0].to_file(str(s_path), description=_descr, attachments=[str(config_file)])
420
458
 
421
459
  return test_res
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes