vortex-api 2.0.6__tar.gz → 2.1.0__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.6
3
+ Version: 2.1.0
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.6"
7
+ version = "2.1.0"
8
8
  description = "Vortex APIs to place orders in Rupeezy application"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -38,5 +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"]
42
- exclude = ["myenv"]
41
+ include = ["vortex_api"]
@@ -0,0 +1 @@
1
+ __version__ = "2.1.0"
@@ -674,6 +674,81 @@ class VortexAPI:
674
674
 
675
675
  return self._make_api_request("POST", endpoint, data=payload)
676
676
 
677
+ def save_optimization_result(
678
+ self,
679
+ stats,
680
+ heatmap,
681
+ name: str,
682
+ symbol: str = "",
683
+ description: str = "",
684
+ maximize="Sharpe Ratio",
685
+ param_ranges: dict = None,
686
+ ) -> dict:
687
+ """
688
+ Save optimization results to Rupeezy for viewing on the developer portal.
689
+
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.
692
+
693
+ 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.
699
+ name (str): A label for this optimization run (e.g. "SMA Grid Search").
700
+ symbol (str, optional): Primary instrument symbol (e.g. "NIFTY").
701
+ description (str, optional): Notes about this optimization run.
702
+ maximize: The metric that was optimized. Should match the `maximize`
703
+ argument passed to Backtest.optimize(). Can be a string
704
+ metric name (e.g. "Sharpe Ratio") or a callable.
705
+ Defaults to "Sharpe Ratio".
706
+ param_ranges (dict, optional): Explicit parameter range definitions.
707
+ Keys are parameter names, values are range() objects or lists.
708
+ Example: {"sma_fast": range(5, 51, 5), "sma_slow": range(20, 201, 10)}
709
+ If not provided, ranges are inferred from the heatmap index.
710
+
711
+ Returns:
712
+ dict: {"status": "success", "optimization_id": "opt_xxx", "backtest_id": "bt_xxx"}
713
+
714
+ Example::
715
+
716
+ stats, heatmap = bt.optimize(
717
+ sma_fast=range(5, 51, 5),
718
+ sma_slow=range(20, 201, 10),
719
+ maximize='Sharpe Ratio',
720
+ return_heatmap=True,
721
+ )
722
+ client.save_optimization_result(
723
+ stats=stats,
724
+ heatmap=heatmap,
725
+ name="SMA Crossover Grid Search",
726
+ symbol="NIFTY",
727
+ maximize='Sharpe Ratio',
728
+ param_ranges={"sma_fast": range(5, 51, 5), "sma_slow": range(20, 201, 10)},
729
+ )
730
+ """
731
+ is_maximize = True
732
+ objective_metric = maximize
733
+
734
+ if isinstance(maximize, bool):
735
+ is_maximize = maximize
736
+ objective_metric = "Sharpe Ratio"
737
+
738
+ payload = _serialize_optimization(
739
+ stats=stats,
740
+ heatmap=heatmap,
741
+ name=name,
742
+ symbol=symbol,
743
+ description=description,
744
+ objective_metric=objective_metric,
745
+ maximize=is_maximize,
746
+ param_ranges=param_ranges,
747
+ )
748
+
749
+ endpoint = "/strategies/optimizations"
750
+ return self._make_api_request("POST", endpoint, data=payload)
751
+
677
752
  # ─── Backtest serialization helpers (module-level) ───────────────────────────
678
753
 
679
754
  def _safe_float(val):
@@ -710,47 +785,63 @@ def _duration_to_days(val):
710
785
 
711
786
 
712
787
  def _serialize_stats(stats, name, symbol, description, tags):
713
- """Convert backtesting.py Stats object to a JSON-serializable dict."""
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
714
804
 
715
- # --- Summary metrics ---
716
805
  summary = {
717
- "return_pct": _safe_float(stats.get("Return [%]")),
718
- "return_ann_pct": _safe_float(stats.get("Return (Ann.) [%]")),
719
- "volatility_ann_pct": _safe_float(stats.get("Volatility (Ann.) [%]")),
720
- "cagr_pct": _safe_float(stats.get("CAGR [%]")),
721
- "buy_hold_return_pct": _safe_float(stats.get("Buy & Hold Return [%]")),
722
- "alpha_pct": _safe_float(stats.get("Alpha [%]")),
723
- "beta": _safe_float(stats.get("Beta")),
724
- "sharpe_ratio": _safe_float(stats.get("Sharpe Ratio")),
725
- "sortino_ratio": _safe_float(stats.get("Sortino Ratio")),
726
- "calmar_ratio": _safe_float(stats.get("Calmar Ratio")),
727
- "max_drawdown_pct": _safe_float(stats.get("Max. Drawdown [%]")),
728
- "avg_drawdown_pct": _safe_float(stats.get("Avg. Drawdown [%]")),
729
- "max_drawdown_duration_days": _duration_to_days(stats.get("Max. Drawdown Duration")),
730
- "avg_drawdown_duration_days": _duration_to_days(stats.get("Avg. Drawdown Duration")),
731
- "equity_final": _safe_float(stats.get("Equity Final [$]")),
732
- "equity_peak": _safe_float(stats.get("Equity Peak [$]")),
733
- "commissions_total": _safe_float(stats.get("Commissions [$]")),
734
- "exposure_time_pct": _safe_float(stats.get("Exposure Time [%]")),
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 [%]")),
735
824
  "total_trades": int(stats.get("# Trades", 0)),
736
- "win_rate_pct": _safe_float(stats.get("Win Rate [%]")),
737
- "best_trade_pct": _safe_float(stats.get("Best Trade [%]")),
738
- "worst_trade_pct": _safe_float(stats.get("Worst Trade [%]")),
739
- "avg_trade_pct": _safe_float(stats.get("Avg. Trade [%]")),
740
- "max_trade_duration_days": _duration_to_days(stats.get("Max. Trade Duration")),
741
- "avg_trade_duration_days": _duration_to_days(stats.get("Avg. Trade Duration")),
742
- "profit_factor": _safe_float(stats.get("Profit Factor")),
743
- "expectancy_pct": _safe_float(stats.get("Expectancy [%]")),
744
- "sqn": _safe_float(stats.get("SQN")),
745
- "kelly_criterion": _safe_float(stats.get("Kelly Criterion")),
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")),
746
835
  }
747
836
 
748
- # --- Strategy parameters ---
837
+ # --- Strategy name and parameters ---
838
+ strategy_name = "Unknown"
749
839
  parameters = {}
750
840
  strategy = stats.get("_strategy")
751
841
  if strategy is not None:
752
842
  strategy_class = strategy if isinstance(strategy, type) else strategy.__class__
753
- for attr in dir(strategy_class):
843
+ strategy_name = strategy_class.__name__
844
+ for attr in vars(strategy_class):
754
845
  if attr.startswith("_"):
755
846
  continue
756
847
  val = getattr(strategy_class, attr, None)
@@ -759,7 +850,7 @@ def _serialize_stats(stats, name, symbol, description, tags):
759
850
  if isinstance(val, (int, float, str, bool)):
760
851
  parameters[attr] = val
761
852
 
762
- # --- Equity curve (downsampled for storage) ---
853
+ # --- Equity curve ---
763
854
  equity_curve = []
764
855
  ec = stats.get("_equity_curve")
765
856
  if ec is not None and hasattr(ec, "index"):
@@ -767,16 +858,16 @@ def _serialize_stats(stats, name, symbol, description, tags):
767
858
  step = max(1, len(equity_series) // 500)
768
859
  for i in range(0, len(equity_series), step):
769
860
  equity_curve.append({
770
- "date": equity_series.index[i].isoformat(),
861
+ "date": equity_series.index[i].strftime("%Y-%m-%d"),
771
862
  "equity": round(float(equity_series.iloc[i]), 2),
772
863
  })
773
- if equity_curve and equity_curve[-1]["date"] != equity_series.index[-1].isoformat():
864
+ if equity_curve and equity_curve[-1]["date"] != equity_series.index[-1].strftime("%Y-%m-%d"):
774
865
  equity_curve.append({
775
- "date": equity_series.index[-1].isoformat(),
866
+ "date": equity_series.index[-1].strftime("%Y-%m-%d"),
776
867
  "equity": round(float(equity_series.iloc[-1]), 2),
777
868
  })
778
869
 
779
- # --- Drawdown curve ---
870
+ # --- Drawdown curve (computed from equity peak) ---
780
871
  drawdown_curve = []
781
872
  if ec is not None and hasattr(ec, "index"):
782
873
  equity_series = ec["Equity"]
@@ -784,10 +875,13 @@ def _serialize_stats(stats, name, symbol, description, tags):
784
875
  drawdown = ((equity_series - running_max) / running_max) * 100
785
876
  step = max(1, len(drawdown) // 500)
786
877
  for i in range(0, len(drawdown), step):
787
- drawdown_curve.append({
788
- "date": drawdown.index[i].isoformat(),
789
- "drawdown_pct": round(float(drawdown.iloc[i]), 4),
790
- })
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
+ })
791
885
 
792
886
  # --- Trade log ---
793
887
  trades_list = []
@@ -795,19 +889,24 @@ def _serialize_stats(stats, name, symbol, description, tags):
795
889
  if trades is not None and hasattr(trades, "iterrows"):
796
890
  for i, trade in trades.iterrows():
797
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
798
897
  trades_list.append({
799
898
  "trade_number": i + 1,
800
899
  "side": "LONG" if size > 0 else "SHORT",
801
900
  "size": abs(int(size)) if size else 0,
802
901
  "entry_bar": int(trade.get("EntryBar", 0)),
803
902
  "exit_bar": int(trade.get("ExitBar", 0)),
804
- "entry_date": _safe_isoformat(trade.get("EntryTime")),
805
- "exit_date": _safe_isoformat(trade.get("ExitTime")),
806
- "entry_price": _safe_float(trade.get("EntryPrice")),
807
- "exit_price": _safe_float(trade.get("ExitPrice")),
808
- "pnl_abs": _safe_float(trade.get("PnL")),
809
- "pnl_pct": round(float(trade.get("ReturnPct", 0)) * 100, 4),
810
- "duration_days": _duration_to_days(trade.get("Duration")),
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,
811
910
  })
812
911
 
813
912
  # --- Monthly returns ---
@@ -816,33 +915,31 @@ def _serialize_stats(stats, name, symbol, description, tags):
816
915
  try:
817
916
  equity_series = ec["Equity"]
818
917
  monthly = equity_series.resample("ME").last()
819
- pct = monthly.pct_change() * 100
918
+ pct = monthly.pct_change().dropna() * 100
820
919
  for date, ret in pct.items():
821
- if ret is not None and not (isinstance(ret, float) and math.isnan(ret)):
822
- monthly_returns.append({
823
- "year": date.year,
824
- "month": date.month,
825
- "return_pct": round(float(ret), 4),
826
- })
920
+ monthly_returns.append({
921
+ "year": date.year,
922
+ "month": date.month,
923
+ "return_pct": _sf(ret),
924
+ })
827
925
  except Exception:
828
926
  pass
829
927
 
830
- # --- Strategy class name ---
831
- strategy_name = "Unknown"
832
- if strategy is not None:
833
- strategy_class = strategy if isinstance(strategy, type) else strategy.__class__
834
- strategy_name = strategy_class.__name__
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)
835
933
 
836
- # --- Assemble payload ---
837
934
  return {
838
935
  "name": name[:200],
839
936
  "symbol": symbol[:50],
840
937
  "description": description[:2000],
841
938
  "tags": tags[:20],
842
939
  "strategy_name": strategy_name,
843
- "start_date": _safe_isoformat(stats.get("Start")),
844
- "end_date": _safe_isoformat(stats.get("End")),
845
- "starting_capital": _safe_float(equity_curve[0]["equity"] if equity_curve else None),
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,
846
943
  "parameters": parameters,
847
944
  "summary": summary,
848
945
  "equity_curve": equity_curve,
@@ -851,3 +948,148 @@ def _serialize_stats(stats, name, symbol, description, tags):
851
948
  "monthly_returns": monthly_returns,
852
949
  }
853
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
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex_api
3
- Version: 2.0.6
3
+ Version: 2.1.0
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
@@ -1 +0,0 @@
1
- __version__ = "2.0.6"
File without changes
File without changes
File without changes