vortex-api 2.0.7__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.7
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.7"
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" }
@@ -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):
@@ -871,4 +946,150 @@ def _serialize_stats(stats, name, symbol, description, tags):
871
946
  "drawdown_curve": drawdown_curve,
872
947
  "trades": trades_list,
873
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,
874
1095
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex_api
3
- Version: 2.0.7
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.7"
File without changes
File without changes
File without changes