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.
- {vortex_api-2.0.6/vortex_api.egg-info → vortex_api-2.1.0}/PKG-INFO +1 -1
- {vortex_api-2.0.6 → vortex_api-2.1.0}/pyproject.toml +2 -3
- vortex_api-2.1.0/vortex_api/__version__.py +1 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/vortex_api/api.py +306 -64
- {vortex_api-2.0.6 → vortex_api-2.1.0/vortex_api.egg-info}/PKG-INFO +1 -1
- vortex_api-2.0.6/vortex_api/__version__.py +0 -1
- {vortex_api-2.0.6 → vortex_api-2.1.0}/LICENSE +0 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/README.md +0 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/setup.cfg +0 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/vortex_api/__init__.py +0 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/vortex_api/vortex_feed.py +0 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/vortex_api.egg-info/SOURCES.txt +0 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/vortex_api.egg-info/dependency_links.txt +0 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/vortex_api.egg-info/requires.txt +0 -0
- {vortex_api-2.0.6 → vortex_api-2.1.0}/vortex_api.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortex_api"
|
|
7
|
-
version = "2.0
|
|
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
|
-
|
|
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":
|
|
718
|
-
"return_ann_pct":
|
|
719
|
-
"volatility_ann_pct":
|
|
720
|
-
"cagr_pct":
|
|
721
|
-
"buy_hold_return_pct":
|
|
722
|
-
"alpha_pct":
|
|
723
|
-
"beta":
|
|
724
|
-
"sharpe_ratio":
|
|
725
|
-
"sortino_ratio":
|
|
726
|
-
"calmar_ratio":
|
|
727
|
-
"max_drawdown_pct":
|
|
728
|
-
"avg_drawdown_pct":
|
|
729
|
-
"max_drawdown_duration_days":
|
|
730
|
-
"avg_drawdown_duration_days":
|
|
731
|
-
"equity_final":
|
|
732
|
-
"equity_peak":
|
|
733
|
-
"commissions_total":
|
|
734
|
-
"exposure_time_pct":
|
|
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":
|
|
737
|
-
"best_trade_pct":
|
|
738
|
-
"worst_trade_pct":
|
|
739
|
-
"avg_trade_pct":
|
|
740
|
-
"max_trade_duration_days":
|
|
741
|
-
"avg_trade_duration_days":
|
|
742
|
-
"profit_factor":
|
|
743
|
-
"expectancy_pct":
|
|
744
|
-
"sqn":
|
|
745
|
-
"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
|
-
|
|
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
|
|
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].
|
|
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].
|
|
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].
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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":
|
|
805
|
-
"exit_date":
|
|
806
|
-
"entry_price":
|
|
807
|
-
"exit_price":
|
|
808
|
-
"pnl_abs":
|
|
809
|
-
"pnl_pct":
|
|
810
|
-
"duration_days":
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
# ---
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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":
|
|
844
|
-
"end_date":
|
|
845
|
-
"starting_capital":
|
|
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 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.6"
|
|
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
|