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.
- {vortex_api-2.1.0/vortex_api.egg-info → vortex_api-2.1.1}/PKG-INFO +1 -1
- {vortex_api-2.1.0 → vortex_api-2.1.1}/pyproject.toml +2 -2
- vortex_api-2.1.1/vortex_api/__version__.py +1 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1}/vortex_api/api.py +18 -359
- vortex_api-2.1.1/vortex_api/backtest/__init__.py +117 -0
- vortex_api-2.1.1/vortex_api/backtest/_backtestingpy.py +287 -0
- vortex_api-2.1.1/vortex_api/backtest/_backtrader.py +464 -0
- vortex_api-2.1.1/vortex_api/backtest/_common.py +295 -0
- vortex_api-2.1.1/vortex_api/backtest/_vectorbt.py +377 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1/vortex_api.egg-info}/PKG-INFO +1 -1
- {vortex_api-2.1.0 → vortex_api-2.1.1}/vortex_api.egg-info/SOURCES.txt +6 -1
- vortex_api-2.1.0/vortex_api/__version__.py +0 -1
- {vortex_api-2.1.0 → vortex_api-2.1.1}/LICENSE +0 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1}/README.md +0 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1}/setup.cfg +0 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1}/vortex_api/__init__.py +0 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1}/vortex_api/vortex_feed.py +0 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1}/vortex_api.egg-info/dependency_links.txt +0 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1}/vortex_api.egg-info/requires.txt +0 -0
- {vortex_api-2.1.0 → vortex_api-2.1.1}/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.1.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
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
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
739
|
-
|
|
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
|
+
)
|