vortex-api 2.1.0__tar.gz → 2.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex_api
3
- Version: 2.1.0
3
+ Version: 2.1.1
4
4
  Summary: Vortex APIs to place orders in Rupeezy application
5
5
  Author-email: "Astha Credit & Securities Pvt Ltd." <tech@rupeezy.in>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortex_api"
7
- version = "2.1.0"
7
+ version = "2.1.1"
8
8
  description = "Vortex APIs to place orders in Rupeezy application"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -38,4 +38,4 @@ Homepage = "https://vortex.rupeezy.in"
38
38
  Repository = "https://github.com/RupeezyTech/pyvortex"
39
39
 
40
40
  [tool.setuptools.packages.find]
41
- include = ["vortex_api"]
41
+ include = ["vortex_api", "vortex_api.*"]
@@ -0,0 +1 @@
1
+ __version__ = "2.1.1"
@@ -2,7 +2,6 @@ import requests
2
2
  import csv
3
3
  import datetime
4
4
  import logging
5
- import math
6
5
  from enum import Enum
7
6
  import inspect
8
7
  import wrapt
@@ -658,9 +657,13 @@ class VortexAPI:
658
657
  """
659
658
  Save backtest results to Rupeezy for viewing on the developer portal.
660
659
 
660
+ Supports multiple backtesting libraries (auto-detected from the result type):
661
+ - **backtesting.py**: pass the stats object from Backtest.run()
662
+ - **vectorbt**: pass a vbt.Portfolio object
663
+ - **backtrader**: pass the strategy from cerebro.run() (i.e. results[0])
664
+
661
665
  Args:
662
- stats: The Stats object returned by Backtest.run() or Backtest.optimize()
663
- from the backtesting.py library.
666
+ stats: The result object from any supported backtesting library.
664
667
  name (str): A label for this backtest run (e.g. "SMA Crossover v2").
665
668
  symbol (str, optional): Primary instrument symbol.
666
669
  description (str, optional): Notes about this run.
@@ -669,7 +672,8 @@ class VortexAPI:
669
672
  Returns:
670
673
  dict: { "status": "success", "backtest_id": "bt_abc123", "url": "https://..." }
671
674
  """
672
- payload = _serialize_stats(stats, name, symbol, description, tags or [])
675
+ from .backtest import serialize_stats
676
+ payload = serialize_stats(stats, name, symbol, description, tags or [])
673
677
  endpoint = "/strategies/backtests"
674
678
 
675
679
  return self._make_api_request("POST", endpoint, data=payload)
@@ -687,15 +691,15 @@ class VortexAPI:
687
691
  """
688
692
  Save optimization results to Rupeezy for viewing on the developer portal.
689
693
 
690
- Takes the output of backtesting.py's Backtest.optimize() and uploads
691
- both the parameter heatmap and the best result's full backtest data.
694
+ Supports multiple backtesting libraries (auto-detected from the result type):
695
+ - **backtesting.py**: pass stats + heatmap from bt.optimize(return_heatmap=True)
696
+ - **vectorbt**: pass Portfolio + metric Series from multi-param run
697
+ - **backtrader**: pass results list from cerebro.run() after optstrategy()
692
698
 
693
699
  Args:
694
- stats: The Stats object returned by Backtest.optimize() for the best
695
- parameter combination.
696
- heatmap: The pandas Series with MultiIndex returned by
697
- Backtest.optimize(return_heatmap=True). Contains the objective
698
- metric value for every parameter combination tested.
700
+ stats: The result object from any supported backtesting library.
701
+ heatmap: The heatmap/metric Series (backtesting.py/vectorbt) or
702
+ metric extraction callable (backtrader).
699
703
  name (str): A label for this optimization run (e.g. "SMA Grid Search").
700
704
  symbol (str, optional): Primary instrument symbol (e.g. "NIFTY").
701
705
  description (str, optional): Notes about this optimization run.
@@ -735,8 +739,9 @@ class VortexAPI:
735
739
  is_maximize = maximize
736
740
  objective_metric = "Sharpe Ratio"
737
741
 
738
- payload = _serialize_optimization(
739
- stats=stats,
742
+ from .backtest import serialize_optimization
743
+ payload = serialize_optimization(
744
+ result=stats,
740
745
  heatmap=heatmap,
741
746
  name=name,
742
747
  symbol=symbol,
@@ -745,351 +750,5 @@ class VortexAPI:
745
750
  maximize=is_maximize,
746
751
  param_ranges=param_ranges,
747
752
  )
748
-
749
753
  endpoint = "/strategies/optimizations"
750
754
  return self._make_api_request("POST", endpoint, data=payload)
751
-
752
- # ─── Backtest serialization helpers (module-level) ───────────────────────────
753
-
754
- def _safe_float(val):
755
- """Convert a value to float, returning None for NaN/None/invalid."""
756
- if val is None:
757
- return None
758
- try:
759
- f = float(val)
760
- if math.isnan(f) or math.isinf(f):
761
- return None
762
- return round(f, 4)
763
- except (ValueError, TypeError):
764
- return None
765
-
766
-
767
- def _safe_isoformat(val):
768
- """Convert a datetime-like value to ISO format string."""
769
- if val is None:
770
- return None
771
- try:
772
- return val.isoformat()
773
- except AttributeError:
774
- return str(val)
775
-
776
-
777
- def _duration_to_days(val):
778
- """Convert a timedelta-like value to integer days."""
779
- if val is None:
780
- return None
781
- try:
782
- return val.days
783
- except AttributeError:
784
- return None
785
-
786
-
787
- def _serialize_stats(stats, name, symbol, description, tags):
788
- def _sf(val):
789
- if val is None:
790
- return 0.0
791
- try:
792
- f = float(val)
793
- return 0.0 if (math.isnan(f) or math.isinf(f)) else round(f, 4)
794
- except (ValueError, TypeError):
795
- return 0.0
796
-
797
- def _dd(val):
798
- if val is None:
799
- return 0
800
- try:
801
- return val.days
802
- except AttributeError:
803
- return 0
804
-
805
- summary = {
806
- "return_pct": _sf(stats.get("Return [%]")),
807
- "return_ann_pct": _sf(stats.get("Return (Ann.) [%]")),
808
- "volatility_ann_pct": _sf(stats.get("Volatility (Ann.) [%]")),
809
- "cagr_pct": _sf(stats.get("CAGR [%]")),
810
- "buy_hold_return_pct": _sf(stats.get("Buy & Hold Return [%]")),
811
- "alpha_pct": _sf(stats.get("Alpha [%]")),
812
- "beta": _sf(stats.get("Beta")),
813
- "sharpe_ratio": _sf(stats.get("Sharpe Ratio")),
814
- "sortino_ratio": _sf(stats.get("Sortino Ratio")),
815
- "calmar_ratio": _sf(stats.get("Calmar Ratio")),
816
- "max_drawdown_pct": _sf(stats.get("Max. Drawdown [%]")),
817
- "avg_drawdown_pct": _sf(stats.get("Avg. Drawdown [%]")),
818
- "max_drawdown_duration_days": _dd(stats.get("Max. Drawdown Duration")),
819
- "avg_drawdown_duration_days": _dd(stats.get("Avg. Drawdown Duration")),
820
- "equity_final": _sf(stats.get("Equity Final [$]")),
821
- "equity_peak": _sf(stats.get("Equity Peak [$]")),
822
- "commissions_total": _sf(stats.get("Commissions [$]")),
823
- "exposure_time_pct": _sf(stats.get("Exposure Time [%]")),
824
- "total_trades": int(stats.get("# Trades", 0)),
825
- "win_rate_pct": _sf(stats.get("Win Rate [%]")),
826
- "best_trade_pct": _sf(stats.get("Best Trade [%]")),
827
- "worst_trade_pct": _sf(stats.get("Worst Trade [%]")),
828
- "avg_trade_pct": _sf(stats.get("Avg. Trade [%]")),
829
- "max_trade_duration_days": _dd(stats.get("Max. Trade Duration")),
830
- "avg_trade_duration_days": _dd(stats.get("Avg. Trade Duration")),
831
- "profit_factor": _sf(stats.get("Profit Factor")),
832
- "expectancy_pct": _sf(stats.get("Expectancy [%]")),
833
- "sqn": _sf(stats.get("SQN")),
834
- "kelly_criterion": _sf(stats.get("Kelly Criterion")),
835
- }
836
-
837
- # --- Strategy name and parameters ---
838
- strategy_name = "Unknown"
839
- parameters = {}
840
- strategy = stats.get("_strategy")
841
- if strategy is not None:
842
- strategy_class = strategy if isinstance(strategy, type) else strategy.__class__
843
- strategy_name = strategy_class.__name__
844
- for attr in vars(strategy_class):
845
- if attr.startswith("_"):
846
- continue
847
- val = getattr(strategy_class, attr, None)
848
- if callable(val):
849
- continue
850
- if isinstance(val, (int, float, str, bool)):
851
- parameters[attr] = val
852
-
853
- # --- Equity curve ---
854
- equity_curve = []
855
- ec = stats.get("_equity_curve")
856
- if ec is not None and hasattr(ec, "index"):
857
- equity_series = ec["Equity"]
858
- step = max(1, len(equity_series) // 500)
859
- for i in range(0, len(equity_series), step):
860
- equity_curve.append({
861
- "date": equity_series.index[i].strftime("%Y-%m-%d"),
862
- "equity": round(float(equity_series.iloc[i]), 2),
863
- })
864
- if equity_curve and equity_curve[-1]["date"] != equity_series.index[-1].strftime("%Y-%m-%d"):
865
- equity_curve.append({
866
- "date": equity_series.index[-1].strftime("%Y-%m-%d"),
867
- "equity": round(float(equity_series.iloc[-1]), 2),
868
- })
869
-
870
- # --- Drawdown curve (computed from equity peak) ---
871
- drawdown_curve = []
872
- if ec is not None and hasattr(ec, "index"):
873
- equity_series = ec["Equity"]
874
- running_max = equity_series.cummax()
875
- drawdown = ((equity_series - running_max) / running_max) * 100
876
- step = max(1, len(drawdown) // 500)
877
- for i in range(0, len(drawdown), step):
878
- dd_val = round(float(drawdown.iloc[i]), 4)
879
- if dd_val < -0.01:
880
- drawdown_curve.append({
881
- "date": drawdown.index[i].strftime("%Y-%m-%d"),
882
- "equity": round(float(equity_series.iloc[i]), 2),
883
- "drawdown_pct": dd_val,
884
- })
885
-
886
- # --- Trade log ---
887
- trades_list = []
888
- trades = stats.get("_trades")
889
- if trades is not None and hasattr(trades, "iterrows"):
890
- for i, trade in trades.iterrows():
891
- size = trade.get("Size", 0)
892
- entry_time = trade.get("EntryTime")
893
- exit_time = trade.get("ExitTime")
894
- duration = 0
895
- if hasattr(entry_time, "strftime") and hasattr(exit_time, "strftime"):
896
- duration = (exit_time - entry_time).days
897
- trades_list.append({
898
- "trade_number": i + 1,
899
- "side": "LONG" if size > 0 else "SHORT",
900
- "size": abs(int(size)) if size else 0,
901
- "entry_bar": int(trade.get("EntryBar", 0)),
902
- "exit_bar": int(trade.get("ExitBar", 0)),
903
- "entry_date": entry_time.strftime("%Y-%m-%d") if hasattr(entry_time, "strftime") else str(entry_time),
904
- "exit_date": exit_time.strftime("%Y-%m-%d") if hasattr(exit_time, "strftime") else str(exit_time),
905
- "entry_price": _sf(trade.get("EntryPrice")),
906
- "exit_price": _sf(trade.get("ExitPrice")),
907
- "pnl_abs": _sf(trade.get("PnL")),
908
- "pnl_pct": _sf(trade.get("ReturnPct")), # already in % form
909
- "duration_days": duration,
910
- })
911
-
912
- # --- Monthly returns ---
913
- monthly_returns = []
914
- if ec is not None and hasattr(ec, "index"):
915
- try:
916
- equity_series = ec["Equity"]
917
- monthly = equity_series.resample("ME").last()
918
- pct = monthly.pct_change().dropna() * 100
919
- for date, ret in pct.items():
920
- monthly_returns.append({
921
- "year": date.year,
922
- "month": date.month,
923
- "return_pct": _sf(ret),
924
- })
925
- except Exception:
926
- pass
927
-
928
- # --- Dates as YYYY-MM-DD ---
929
- start_val = stats.get("Start")
930
- end_val = stats.get("End")
931
- start_date = start_val.strftime("%Y-%m-%d") if hasattr(start_val, "strftime") else str(start_val)
932
- end_date = end_val.strftime("%Y-%m-%d") if hasattr(end_val, "strftime") else str(end_val)
933
-
934
- return {
935
- "name": name[:200],
936
- "symbol": symbol[:50],
937
- "description": description[:2000],
938
- "tags": tags[:20],
939
- "strategy_name": strategy_name,
940
- "start_date": start_date,
941
- "end_date": end_date,
942
- "starting_capital": round(float(equity_curve[0]["equity"]), 2) if equity_curve else 0,
943
- "parameters": parameters,
944
- "summary": summary,
945
- "equity_curve": equity_curve,
946
- "drawdown_curve": drawdown_curve,
947
- "trades": trades_list,
948
- "monthly_returns": monthly_returns,
949
- }
950
-
951
-
952
- # ─── Optimization serialization helpers (module-level) ───────────────────────
953
-
954
- # Maps backtesting.py metric names → backend snake_case field names.
955
- _METRIC_NAME_MAP = {
956
- "Sharpe Ratio": "sharpe_ratio",
957
- "Sortino Ratio": "sortino_ratio",
958
- "Calmar Ratio": "calmar_ratio",
959
- "Return [%]": "return_pct",
960
- "Return (Ann.) [%]": "return_ann_pct",
961
- "Equity Final [$]": "equity_final",
962
- "SQN": "sqn",
963
- "Max. Drawdown [%]": "max_drawdown_pct",
964
- "Avg. Drawdown [%]": "avg_drawdown_pct",
965
- "Win Rate [%]": "win_rate_pct",
966
- "Profit Factor": "profit_factor",
967
- "Expectancy [%]": "expectancy_pct",
968
- "# Trades": "total_trades",
969
- "Exposure Time [%]": "exposure_time_pct",
970
- "Buy & Hold Return [%]": "buy_hold_return_pct",
971
- "CAGR [%]": "cagr_pct",
972
- "Volatility (Ann.) [%]": "volatility_ann_pct",
973
- "Kelly Criterion": "kelly_criterion",
974
- "Best Trade [%]": "best_trade_pct",
975
- "Worst Trade [%]": "worst_trade_pct",
976
- "Avg. Trade [%]": "avg_trade_pct",
977
- }
978
-
979
-
980
- def _infer_range_def(sorted_values):
981
- """
982
- Given a sorted list of unique float values, try to infer start/stop/step.
983
- If evenly spaced → {"start", "stop", "step"}.
984
- Otherwise → {"values": [...]}.
985
- """
986
- if len(sorted_values) < 2:
987
- return {"values": sorted_values}
988
-
989
- step = sorted_values[1] - sorted_values[0]
990
- is_uniform = all(
991
- abs((sorted_values[i] - sorted_values[i - 1]) - step) < 1e-9
992
- for i in range(2, len(sorted_values))
993
- )
994
-
995
- if is_uniform and step > 0:
996
- return {
997
- "start": sorted_values[0],
998
- "stop": sorted_values[-1] + step,
999
- "step": step,
1000
- }
1001
- return {"values": sorted_values}
1002
-
1003
-
1004
- def _serialize_optimization(stats, heatmap, name, symbol, description,
1005
- objective_metric, maximize, param_ranges):
1006
- """
1007
- Serialize backtesting.py optimize() output into the backend's
1008
- OptimizationSaveRequest payload.
1009
- """
1010
- # --- Map the objective metric name to backend format ---
1011
- if isinstance(objective_metric, str):
1012
- backend_metric = _METRIC_NAME_MAP.get(objective_metric, objective_metric)
1013
- else:
1014
- # Callable (custom optimization function)
1015
- backend_metric = "custom"
1016
-
1017
- # --- Build parameter_defs ---
1018
- parameter_defs = {}
1019
- if param_ranges is not None:
1020
- for param_name, rng in param_ranges.items():
1021
- if isinstance(rng, range):
1022
- parameter_defs[param_name] = {
1023
- "start": float(rng.start),
1024
- "stop": float(rng.stop),
1025
- "step": float(rng.step),
1026
- }
1027
- elif hasattr(rng, "__iter__"):
1028
- parameter_defs[param_name] = {
1029
- "values": [float(v) for v in rng],
1030
- }
1031
- else:
1032
- parameter_defs[param_name] = {"values": [float(rng)]}
1033
- elif heatmap is not None and hasattr(heatmap, "index"):
1034
- index = heatmap.index
1035
- if hasattr(index, "levels"):
1036
- # MultiIndex (2+ params)
1037
- for level_num, level_name in enumerate(index.names):
1038
- values = sorted(set(float(v) for v in index.get_level_values(level_num)))
1039
- parameter_defs[level_name] = _infer_range_def(values)
1040
- else:
1041
- # Plain Index (1 param)
1042
- level_name = index.name or "param"
1043
- values = sorted(set(float(v) for v in index))
1044
- parameter_defs[level_name] = _infer_range_def(values)
1045
-
1046
- # --- Build results array from heatmap ---
1047
- results = []
1048
- if heatmap is not None and hasattr(heatmap, "index"):
1049
- index = heatmap.index
1050
- if hasattr(index, "levels"):
1051
- for key, metric_val in heatmap.items():
1052
- params = {}
1053
- for i, level_name in enumerate(index.names):
1054
- params[level_name] = float(key[i])
1055
- results.append({
1056
- "parameters": params,
1057
- "metric_value": _safe_float(metric_val),
1058
- "return_pct": None,
1059
- "sharpe_ratio": None,
1060
- "max_drawdown_pct": None,
1061
- "total_trades": None,
1062
- })
1063
- else:
1064
- level_name = index.name or "param"
1065
- for key, metric_val in heatmap.items():
1066
- results.append({
1067
- "parameters": {level_name: float(key)},
1068
- "metric_value": _safe_float(metric_val),
1069
- "return_pct": None,
1070
- "sharpe_ratio": None,
1071
- "max_drawdown_pct": None,
1072
- "total_trades": None,
1073
- })
1074
-
1075
- # --- Serialize the best result using existing _serialize_stats ---
1076
- best_result = _serialize_stats(stats, name, symbol, description, [])
1077
-
1078
- # --- Extract strategy name from stats ---
1079
- strategy_name = "Unknown"
1080
- strategy = stats.get("_strategy")
1081
- if strategy is not None:
1082
- strategy_class = strategy if isinstance(strategy, type) else strategy.__class__
1083
- strategy_name = strategy_class.__name__
1084
-
1085
- return {
1086
- "name": name[:200],
1087
- "symbol": symbol[:50],
1088
- "strategy_name": strategy_name,
1089
- "description": description[:2000],
1090
- "objective_metric": backend_metric,
1091
- "maximize": maximize,
1092
- "parameter_defs": parameter_defs,
1093
- "results": results,
1094
- "best_result": best_result,
1095
- }
@@ -0,0 +1,117 @@
1
+ """Backtest serialization subpackage.
2
+
3
+ Auto-detects the backtesting library from the result object type and
4
+ dispatches to the appropriate adapter. Supports:
5
+
6
+ - backtesting.py (Backtest.run() / Backtest.optimize())
7
+ - vectorbt (vbt.Portfolio)
8
+ - backtrader (cerebro.run() Strategy)
9
+
10
+ Usage from api.py:
11
+ from .backtest import serialize_stats, serialize_optimization
12
+ """
13
+
14
+
15
+ def _is_backtestingpy(result):
16
+ """Check if result is a backtesting.py stats object (pd.Series with _strategy key)."""
17
+ return hasattr(result, "get") and result.get("_strategy") is not None
18
+
19
+
20
+ def _is_vectorbt(result):
21
+ """Check if result is a vectorbt Portfolio object."""
22
+ return (hasattr(result, "stats")
23
+ and hasattr(result, "value")
24
+ and hasattr(result, "trades"))
25
+
26
+
27
+ def _is_backtrader(result):
28
+ """Check if result is a backtrader Strategy object."""
29
+ # Single strategy
30
+ if hasattr(result, "analyzers"):
31
+ return True
32
+ # List of strategies from cerebro.run()
33
+ if isinstance(result, list) and len(result) > 0:
34
+ item = result[0]
35
+ if hasattr(item, "analyzers"):
36
+ return True
37
+ # Optimization result: list of lists
38
+ if isinstance(item, list) and len(item) > 0 and hasattr(item[0], "analyzers"):
39
+ return True
40
+ return False
41
+
42
+
43
+ def serialize_stats(result, name, symbol, description, tags):
44
+ """Auto-detect library and serialize backtest stats to backend payload.
45
+
46
+ Args:
47
+ result: Stats/Portfolio/Strategy object from any supported library.
48
+ name: Backtest name.
49
+ symbol: Trading symbol.
50
+ description: Description text.
51
+ tags: List of tag strings.
52
+
53
+ Returns:
54
+ dict: Payload ready for POST to /strategies/backtests.
55
+
56
+ Raises:
57
+ TypeError: If the result type is not recognized.
58
+ """
59
+ if _is_backtestingpy(result):
60
+ from ._backtestingpy import serialize_stats as _serialize
61
+ return _serialize(result, name, symbol, description, tags)
62
+
63
+ if _is_vectorbt(result):
64
+ from ._vectorbt import serialize_stats as _serialize
65
+ return _serialize(result, name, symbol, description, tags)
66
+
67
+ if _is_backtrader(result):
68
+ from ._backtrader import serialize_stats as _serialize
69
+ return _serialize(result, name, symbol, description, tags)
70
+
71
+ raise TypeError(
72
+ f"Unsupported backtest result type: {type(result).__name__}. "
73
+ f"Expected stats from backtesting.py, a vectorbt Portfolio, "
74
+ f"or a backtrader Strategy."
75
+ )
76
+
77
+
78
+ def serialize_optimization(result, heatmap, name, symbol, description,
79
+ objective_metric, maximize, param_ranges):
80
+ """Auto-detect library and serialize optimization results.
81
+
82
+ Args:
83
+ result: Stats/Portfolio/Strategy-list from any supported library.
84
+ heatmap: Heatmap Series (backtesting.py/vectorbt) or metric_fn (backtrader).
85
+ name: Optimization name.
86
+ symbol: Trading symbol.
87
+ description: Description text.
88
+ objective_metric: Metric that was optimized.
89
+ maximize: Boolean — True to maximize, False to minimize.
90
+ param_ranges: Dict of param_name -> range/list.
91
+
92
+ Returns:
93
+ dict: Payload ready for POST to /strategies/optimizations.
94
+
95
+ Raises:
96
+ TypeError: If the result type is not recognized.
97
+ """
98
+ if _is_backtestingpy(result):
99
+ from ._backtestingpy import serialize_optimization as _serialize
100
+ return _serialize(result, heatmap, name, symbol, description,
101
+ objective_metric, maximize, param_ranges)
102
+
103
+ if _is_vectorbt(result):
104
+ from ._vectorbt import serialize_optimization as _serialize
105
+ return _serialize(result, heatmap, name, symbol, description,
106
+ objective_metric, maximize, param_ranges)
107
+
108
+ if _is_backtrader(result):
109
+ from ._backtrader import serialize_optimization as _serialize
110
+ return _serialize(result, heatmap, name, symbol, description,
111
+ objective_metric, maximize, param_ranges)
112
+
113
+ raise TypeError(
114
+ f"Unsupported optimization result type: {type(result).__name__}. "
115
+ f"Expected stats from backtesting.py, a vectorbt Portfolio, "
116
+ f"or backtrader optimization results."
117
+ )