vortex-api 2.0.7__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.0.7
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.0.7"
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,206 +672,83 @@ 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)
676
680
 
677
- # ─── Backtest serialization helpers (module-level) ───────────────────────────
678
-
679
- def _safe_float(val):
680
- """Convert a value to float, returning None for NaN/None/invalid."""
681
- if val is None:
682
- return None
683
- try:
684
- f = float(val)
685
- if math.isnan(f) or math.isinf(f):
686
- return None
687
- return round(f, 4)
688
- except (ValueError, TypeError):
689
- return None
690
-
691
-
692
- def _safe_isoformat(val):
693
- """Convert a datetime-like value to ISO format string."""
694
- if val is None:
695
- return None
696
- try:
697
- return val.isoformat()
698
- except AttributeError:
699
- return str(val)
700
-
701
-
702
- def _duration_to_days(val):
703
- """Convert a timedelta-like value to integer days."""
704
- if val is None:
705
- return None
706
- try:
707
- return val.days
708
- except AttributeError:
709
- return None
710
-
711
-
712
- def _serialize_stats(stats, name, symbol, description, tags):
713
- def _sf(val):
714
- if val is None:
715
- return 0.0
716
- try:
717
- f = float(val)
718
- return 0.0 if (math.isnan(f) or math.isinf(f)) else round(f, 4)
719
- except (ValueError, TypeError):
720
- return 0.0
721
-
722
- def _dd(val):
723
- if val is None:
724
- return 0
725
- try:
726
- return val.days
727
- except AttributeError:
728
- return 0
729
-
730
- summary = {
731
- "return_pct": _sf(stats.get("Return [%]")),
732
- "return_ann_pct": _sf(stats.get("Return (Ann.) [%]")),
733
- "volatility_ann_pct": _sf(stats.get("Volatility (Ann.) [%]")),
734
- "cagr_pct": _sf(stats.get("CAGR [%]")),
735
- "buy_hold_return_pct": _sf(stats.get("Buy & Hold Return [%]")),
736
- "alpha_pct": _sf(stats.get("Alpha [%]")),
737
- "beta": _sf(stats.get("Beta")),
738
- "sharpe_ratio": _sf(stats.get("Sharpe Ratio")),
739
- "sortino_ratio": _sf(stats.get("Sortino Ratio")),
740
- "calmar_ratio": _sf(stats.get("Calmar Ratio")),
741
- "max_drawdown_pct": _sf(stats.get("Max. Drawdown [%]")),
742
- "avg_drawdown_pct": _sf(stats.get("Avg. Drawdown [%]")),
743
- "max_drawdown_duration_days": _dd(stats.get("Max. Drawdown Duration")),
744
- "avg_drawdown_duration_days": _dd(stats.get("Avg. Drawdown Duration")),
745
- "equity_final": _sf(stats.get("Equity Final [$]")),
746
- "equity_peak": _sf(stats.get("Equity Peak [$]")),
747
- "commissions_total": _sf(stats.get("Commissions [$]")),
748
- "exposure_time_pct": _sf(stats.get("Exposure Time [%]")),
749
- "total_trades": int(stats.get("# Trades", 0)),
750
- "win_rate_pct": _sf(stats.get("Win Rate [%]")),
751
- "best_trade_pct": _sf(stats.get("Best Trade [%]")),
752
- "worst_trade_pct": _sf(stats.get("Worst Trade [%]")),
753
- "avg_trade_pct": _sf(stats.get("Avg. Trade [%]")),
754
- "max_trade_duration_days": _dd(stats.get("Max. Trade Duration")),
755
- "avg_trade_duration_days": _dd(stats.get("Avg. Trade Duration")),
756
- "profit_factor": _sf(stats.get("Profit Factor")),
757
- "expectancy_pct": _sf(stats.get("Expectancy [%]")),
758
- "sqn": _sf(stats.get("SQN")),
759
- "kelly_criterion": _sf(stats.get("Kelly Criterion")),
760
- }
761
-
762
- # --- Strategy name and parameters ---
763
- strategy_name = "Unknown"
764
- parameters = {}
765
- strategy = stats.get("_strategy")
766
- if strategy is not None:
767
- strategy_class = strategy if isinstance(strategy, type) else strategy.__class__
768
- strategy_name = strategy_class.__name__
769
- for attr in vars(strategy_class):
770
- if attr.startswith("_"):
771
- continue
772
- val = getattr(strategy_class, attr, None)
773
- if callable(val):
774
- continue
775
- if isinstance(val, (int, float, str, bool)):
776
- parameters[attr] = val
777
-
778
- # --- Equity curve ---
779
- equity_curve = []
780
- ec = stats.get("_equity_curve")
781
- if ec is not None and hasattr(ec, "index"):
782
- equity_series = ec["Equity"]
783
- step = max(1, len(equity_series) // 500)
784
- for i in range(0, len(equity_series), step):
785
- equity_curve.append({
786
- "date": equity_series.index[i].strftime("%Y-%m-%d"),
787
- "equity": round(float(equity_series.iloc[i]), 2),
788
- })
789
- if equity_curve and equity_curve[-1]["date"] != equity_series.index[-1].strftime("%Y-%m-%d"):
790
- equity_curve.append({
791
- "date": equity_series.index[-1].strftime("%Y-%m-%d"),
792
- "equity": round(float(equity_series.iloc[-1]), 2),
793
- })
794
-
795
- # --- Drawdown curve (computed from equity peak) ---
796
- drawdown_curve = []
797
- if ec is not None and hasattr(ec, "index"):
798
- equity_series = ec["Equity"]
799
- running_max = equity_series.cummax()
800
- drawdown = ((equity_series - running_max) / running_max) * 100
801
- step = max(1, len(drawdown) // 500)
802
- for i in range(0, len(drawdown), step):
803
- dd_val = round(float(drawdown.iloc[i]), 4)
804
- if dd_val < -0.01:
805
- drawdown_curve.append({
806
- "date": drawdown.index[i].strftime("%Y-%m-%d"),
807
- "equity": round(float(equity_series.iloc[i]), 2),
808
- "drawdown_pct": dd_val,
809
- })
810
-
811
- # --- Trade log ---
812
- trades_list = []
813
- trades = stats.get("_trades")
814
- if trades is not None and hasattr(trades, "iterrows"):
815
- for i, trade in trades.iterrows():
816
- size = trade.get("Size", 0)
817
- entry_time = trade.get("EntryTime")
818
- exit_time = trade.get("ExitTime")
819
- duration = 0
820
- if hasattr(entry_time, "strftime") and hasattr(exit_time, "strftime"):
821
- duration = (exit_time - entry_time).days
822
- trades_list.append({
823
- "trade_number": i + 1,
824
- "side": "LONG" if size > 0 else "SHORT",
825
- "size": abs(int(size)) if size else 0,
826
- "entry_bar": int(trade.get("EntryBar", 0)),
827
- "exit_bar": int(trade.get("ExitBar", 0)),
828
- "entry_date": entry_time.strftime("%Y-%m-%d") if hasattr(entry_time, "strftime") else str(entry_time),
829
- "exit_date": exit_time.strftime("%Y-%m-%d") if hasattr(exit_time, "strftime") else str(exit_time),
830
- "entry_price": _sf(trade.get("EntryPrice")),
831
- "exit_price": _sf(trade.get("ExitPrice")),
832
- "pnl_abs": _sf(trade.get("PnL")),
833
- "pnl_pct": _sf(trade.get("ReturnPct")), # already in % form
834
- "duration_days": duration,
835
- })
836
-
837
- # --- Monthly returns ---
838
- monthly_returns = []
839
- if ec is not None and hasattr(ec, "index"):
840
- try:
841
- equity_series = ec["Equity"]
842
- monthly = equity_series.resample("ME").last()
843
- pct = monthly.pct_change().dropna() * 100
844
- for date, ret in pct.items():
845
- monthly_returns.append({
846
- "year": date.year,
847
- "month": date.month,
848
- "return_pct": _sf(ret),
849
- })
850
- except Exception:
851
- pass
852
-
853
- # --- Dates as YYYY-MM-DD ---
854
- start_val = stats.get("Start")
855
- end_val = stats.get("End")
856
- start_date = start_val.strftime("%Y-%m-%d") if hasattr(start_val, "strftime") else str(start_val)
857
- end_date = end_val.strftime("%Y-%m-%d") if hasattr(end_val, "strftime") else str(end_val)
858
-
859
- return {
860
- "name": name[:200],
861
- "symbol": symbol[:50],
862
- "description": description[:2000],
863
- "tags": tags[:20],
864
- "strategy_name": strategy_name,
865
- "start_date": start_date,
866
- "end_date": end_date,
867
- "starting_capital": round(float(equity_curve[0]["equity"]), 2) if equity_curve else 0,
868
- "parameters": parameters,
869
- "summary": summary,
870
- "equity_curve": equity_curve,
871
- "drawdown_curve": drawdown_curve,
872
- "trades": trades_list,
873
- "monthly_returns": monthly_returns,
874
- }
681
+ def save_optimization_result(
682
+ self,
683
+ stats,
684
+ heatmap,
685
+ name: str,
686
+ symbol: str = "",
687
+ description: str = "",
688
+ maximize="Sharpe Ratio",
689
+ param_ranges: dict = None,
690
+ ) -> dict:
691
+ """
692
+ Save optimization results to Rupeezy for viewing on the developer portal.
693
+
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()
698
+
699
+ Args:
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).
703
+ name (str): A label for this optimization run (e.g. "SMA Grid Search").
704
+ symbol (str, optional): Primary instrument symbol (e.g. "NIFTY").
705
+ description (str, optional): Notes about this optimization run.
706
+ maximize: The metric that was optimized. Should match the `maximize`
707
+ argument passed to Backtest.optimize(). Can be a string
708
+ metric name (e.g. "Sharpe Ratio") or a callable.
709
+ Defaults to "Sharpe Ratio".
710
+ param_ranges (dict, optional): Explicit parameter range definitions.
711
+ Keys are parameter names, values are range() objects or lists.
712
+ Example: {"sma_fast": range(5, 51, 5), "sma_slow": range(20, 201, 10)}
713
+ If not provided, ranges are inferred from the heatmap index.
714
+
715
+ Returns:
716
+ dict: {"status": "success", "optimization_id": "opt_xxx", "backtest_id": "bt_xxx"}
717
+
718
+ Example::
719
+
720
+ stats, heatmap = bt.optimize(
721
+ sma_fast=range(5, 51, 5),
722
+ sma_slow=range(20, 201, 10),
723
+ maximize='Sharpe Ratio',
724
+ return_heatmap=True,
725
+ )
726
+ client.save_optimization_result(
727
+ stats=stats,
728
+ heatmap=heatmap,
729
+ name="SMA Crossover Grid Search",
730
+ symbol="NIFTY",
731
+ maximize='Sharpe Ratio',
732
+ param_ranges={"sma_fast": range(5, 51, 5), "sma_slow": range(20, 201, 10)},
733
+ )
734
+ """
735
+ is_maximize = True
736
+ objective_metric = maximize
737
+
738
+ if isinstance(maximize, bool):
739
+ is_maximize = maximize
740
+ objective_metric = "Sharpe Ratio"
741
+
742
+ from .backtest import serialize_optimization
743
+ payload = serialize_optimization(
744
+ result=stats,
745
+ heatmap=heatmap,
746
+ name=name,
747
+ symbol=symbol,
748
+ description=description,
749
+ objective_metric=objective_metric,
750
+ maximize=is_maximize,
751
+ param_ranges=param_ranges,
752
+ )
753
+ endpoint = "/strategies/optimizations"
754
+ return self._make_api_request("POST", endpoint, data=payload)
@@ -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
+ )