riskoptima 2.3.0__tar.gz → 2.3.2__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.
- {riskoptima-2.3.0 → riskoptima-2.3.2}/PKG-INFO +20 -1
- {riskoptima-2.3.0 → riskoptima-2.3.2}/README.md +19 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/pyproject.toml +1 -1
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/__init__.py +4 -1
- riskoptima-2.3.2/riskoptima/backtest/__init__.py +34 -0
- riskoptima-2.3.2/riskoptima/backtest/sma.py +302 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/optim/mean_variance.py +13 -9
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/options/binomial.py +17 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/options/black_scholes.py +8 -2
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/reporting/market_risk.py +25 -8
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/riskoptima.py +36 -218
- riskoptima-2.3.0/riskoptima/backtest/__init__.py +0 -13
- {riskoptima-2.3.0 → riskoptima-2.3.2}/LICENSE +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/backtest/engine.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/backtest/portfolio.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/backtest/strategy.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/core/__init__.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/core/types.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/credit/__init__.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/credit/metrics.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/credit/models.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/credit/portfolio.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/credit/simulation.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/data/__init__.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/optim/__init__.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/optim/constraints.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/optim/costs.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/options/__init__.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/options/greeks.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/options/implied_vol.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/options/monte_carlo.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/reporting/__init__.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/risk/__init__.py +0 -0
- {riskoptima-2.3.0 → riskoptima-2.3.2}/riskoptima/risk/factor_model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: riskoptima
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: RiskOptima is a powerful Python toolkit for financial risk analysis, portfolio optimization, and advanced quantitative modeling. It integrates state-of-the-art methodologies, including Monte Carlo simulations, Value at Risk (VaR), Conditional VaR (CVaR), Black-Scholes, Heston, and Merton Jump Diffusion models, to aid investors in making data-driven investment decisions.
|
|
5
5
|
Home-page: https://github.com/JordiCorbilla/RiskOptima
|
|
6
6
|
License: MIT
|
|
@@ -108,6 +108,25 @@ equity_curve, weights_history = run_backtest(prices, strategy, config, cost_mode
|
|
|
108
108
|
|
|
109
109
|
See `examples/example_factor_backtest.py` for a runnable end-to-end example.
|
|
110
110
|
|
|
111
|
+
### SMA Crossover Strategy
|
|
112
|
+
|
|
113
|
+
RiskOptima includes reusable SMA crossover helpers for a simple long-only trend-following workflow. The short moving average crossing above the long moving average creates an entry signal; a bearish cross, stop loss, or take profit closes the trade.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from riskoptima.backtest import build_sma_signal_frame, run_sma_strategy_with_risk
|
|
117
|
+
|
|
118
|
+
signals = build_sma_signal_frame(prices[["Close"]], short_window=20, long_window=50)
|
|
119
|
+
trades = run_sma_strategy_with_risk(
|
|
120
|
+
"SPY",
|
|
121
|
+
start="2024-01-01",
|
|
122
|
+
end="2025-01-01",
|
|
123
|
+
stop_loss=0.05,
|
|
124
|
+
take_profit=0.10,
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The notebook `05-portfolio_sma_strategy.ipynb` shows single-asset, equal-weight multi-asset, and custom-weight portfolio runs.
|
|
129
|
+
|
|
111
130
|
### Offline sample datasets
|
|
112
131
|
|
|
113
132
|
RiskOptima includes small synthetic datasets for deterministic examples:
|
|
@@ -80,6 +80,25 @@ equity_curve, weights_history = run_backtest(prices, strategy, config, cost_mode
|
|
|
80
80
|
|
|
81
81
|
See `examples/example_factor_backtest.py` for a runnable end-to-end example.
|
|
82
82
|
|
|
83
|
+
### SMA Crossover Strategy
|
|
84
|
+
|
|
85
|
+
RiskOptima includes reusable SMA crossover helpers for a simple long-only trend-following workflow. The short moving average crossing above the long moving average creates an entry signal; a bearish cross, stop loss, or take profit closes the trade.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from riskoptima.backtest import build_sma_signal_frame, run_sma_strategy_with_risk
|
|
89
|
+
|
|
90
|
+
signals = build_sma_signal_frame(prices[["Close"]], short_window=20, long_window=50)
|
|
91
|
+
trades = run_sma_strategy_with_risk(
|
|
92
|
+
"SPY",
|
|
93
|
+
start="2024-01-01",
|
|
94
|
+
end="2025-01-01",
|
|
95
|
+
stop_loss=0.05,
|
|
96
|
+
take_profit=0.10,
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The notebook `05-portfolio_sma_strategy.ipynb` shows single-asset, equal-weight multi-asset, and custom-weight portfolio runs.
|
|
101
|
+
|
|
83
102
|
### Offline sample datasets
|
|
84
103
|
|
|
85
104
|
RiskOptima includes small synthetic datasets for deterministic examples:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "riskoptima"
|
|
3
|
-
version = "2.3.
|
|
3
|
+
version = "2.3.2"
|
|
4
4
|
description = "RiskOptima is a powerful Python toolkit for financial risk analysis, portfolio optimization, and advanced quantitative modeling. It integrates state-of-the-art methodologies, including Monte Carlo simulations, Value at Risk (VaR), Conditional VaR (CVaR), Black-Scholes, Heston, and Merton Jump Diffusion models, to aid investors in making data-driven investment decisions."
|
|
5
5
|
authors = ["Jordi Corbilla <jordi.coll.corbilla@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#----------------------------------------------------------------------------
|
|
12
12
|
# Created By : Jordi Corbilla
|
|
13
13
|
# Created Date: 2025
|
|
14
|
-
# version ='2.3.
|
|
14
|
+
# version ='2.3.2'
|
|
15
15
|
# ---------------------------------------------------------------------------
|
|
16
16
|
|
|
17
17
|
from .riskoptima import RiskOptima
|
|
@@ -19,6 +19,7 @@ from .core import MarketData, Portfolio, BacktestConfig, RiskReport
|
|
|
19
19
|
from .risk import FactorRiskModel
|
|
20
20
|
from .optim import Constraints, optimize_max_sharpe, optimize_min_variance, SimpleCostModel
|
|
21
21
|
from .backtest import run_backtest, Strategy, SMACrossStrategy, PortfolioState
|
|
22
|
+
from .backtest import build_sma_signal_frame, run_sma_strategy_with_risk
|
|
22
23
|
from .credit import expected_loss, credit_var, merton_pd
|
|
23
24
|
from .reporting import build_market_risk_report
|
|
24
25
|
from .options import black_scholes_price, black_scholes_greeks, implied_volatility
|
|
@@ -38,6 +39,8 @@ __all__ = [
|
|
|
38
39
|
"Strategy",
|
|
39
40
|
"SMACrossStrategy",
|
|
40
41
|
"PortfolioState",
|
|
42
|
+
"build_sma_signal_frame",
|
|
43
|
+
"run_sma_strategy_with_risk",
|
|
41
44
|
"expected_loss",
|
|
42
45
|
"credit_var",
|
|
43
46
|
"merton_pd",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# __init__.py
|
|
3
|
+
###############################################################################
|
|
4
|
+
# Product: RiskOptima
|
|
5
|
+
# Author: Jordi Corbilla
|
|
6
|
+
# Description: RiskOptima module
|
|
7
|
+
###############################################################################
|
|
8
|
+
|
|
9
|
+
from .engine import run_backtest
|
|
10
|
+
from .strategy import Strategy, SMACrossStrategy
|
|
11
|
+
from .portfolio import PortfolioState
|
|
12
|
+
from .sma import (
|
|
13
|
+
build_sma_signal_frame,
|
|
14
|
+
trades_from_sma_signals,
|
|
15
|
+
run_sma_strategy_with_risk,
|
|
16
|
+
run_strategy_on_portfolio,
|
|
17
|
+
run_and_plot_sma_strategy,
|
|
18
|
+
plot_sma_strategy_cumulative_return,
|
|
19
|
+
plot_sma_strategy_trades,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"run_backtest",
|
|
24
|
+
"Strategy",
|
|
25
|
+
"SMACrossStrategy",
|
|
26
|
+
"PortfolioState",
|
|
27
|
+
"build_sma_signal_frame",
|
|
28
|
+
"trades_from_sma_signals",
|
|
29
|
+
"run_sma_strategy_with_risk",
|
|
30
|
+
"run_strategy_on_portfolio",
|
|
31
|
+
"run_and_plot_sma_strategy",
|
|
32
|
+
"plot_sma_strategy_cumulative_return",
|
|
33
|
+
"plot_sma_strategy_trades",
|
|
34
|
+
]
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# sma.py
|
|
3
|
+
###############################################################################
|
|
4
|
+
# Product: RiskOptima
|
|
5
|
+
# Author: Jordi Corbilla
|
|
6
|
+
# Description: SMA crossover helpers
|
|
7
|
+
###############################################################################
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import os
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
TRADE_COLUMNS = [
|
|
20
|
+
"Ticker",
|
|
21
|
+
"Entry Date",
|
|
22
|
+
"Exit Date",
|
|
23
|
+
"Entry Price",
|
|
24
|
+
"Exit Price",
|
|
25
|
+
"Return",
|
|
26
|
+
"Exit Reason",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
PORTFOLIO_TRADE_COLUMNS = TRADE_COLUMNS + ["Weight", "Weighted Return"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _close_frame(prices) -> pd.DataFrame:
|
|
33
|
+
data = pd.DataFrame(prices).copy()
|
|
34
|
+
if isinstance(data.columns, pd.MultiIndex):
|
|
35
|
+
if "Close" in data.columns.get_level_values(0):
|
|
36
|
+
close_data = data.xs("Close", axis=1, level=0)
|
|
37
|
+
elif "Close" in data.columns.get_level_values(-1):
|
|
38
|
+
close_data = data.xs("Close", axis=1, level=-1)
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError("prices must contain a Close column or a single price column")
|
|
41
|
+
data = close_data.iloc[:, [0]].copy()
|
|
42
|
+
data.columns = ["Close"]
|
|
43
|
+
elif "Close" in data.columns:
|
|
44
|
+
data = data[["Close"]].copy()
|
|
45
|
+
elif data.shape[1] == 1:
|
|
46
|
+
data.columns = ["Close"]
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError("prices must contain a Close column or a single price column")
|
|
49
|
+
data["Close"] = pd.to_numeric(data["Close"], errors="coerce")
|
|
50
|
+
return data.dropna(subset=["Close"])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def download_close_prices(ticker: str, start: str, end: str) -> pd.DataFrame:
|
|
54
|
+
"""
|
|
55
|
+
Downloads close prices with yfinance.
|
|
56
|
+
"""
|
|
57
|
+
import yfinance as yf
|
|
58
|
+
|
|
59
|
+
return yf.download(ticker, start=start, end=end, progress=False, auto_adjust=False)[["Close"]].copy()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_sma_signal_frame(prices, short_window: int = 20, long_window: int = 50) -> pd.DataFrame:
|
|
63
|
+
"""
|
|
64
|
+
Builds close, SMA, and crossover signal columns.
|
|
65
|
+
|
|
66
|
+
Signal is +1 on bullish short-over-long crosses, -1 on bearish crosses, and 0 otherwise.
|
|
67
|
+
"""
|
|
68
|
+
if short_window <= 0 or long_window <= 0:
|
|
69
|
+
raise ValueError("short_window and long_window must be positive")
|
|
70
|
+
if short_window >= long_window:
|
|
71
|
+
raise ValueError("short_window must be smaller than long_window")
|
|
72
|
+
|
|
73
|
+
df = _close_frame(prices)
|
|
74
|
+
df[f"SMA{short_window}"] = df["Close"].rolling(short_window).mean()
|
|
75
|
+
df[f"SMA{long_window}"] = df["Close"].rolling(long_window).mean()
|
|
76
|
+
|
|
77
|
+
short_ma = df[f"SMA{short_window}"]
|
|
78
|
+
long_ma = df[f"SMA{long_window}"]
|
|
79
|
+
bullish = (short_ma > long_ma) & (short_ma.shift(1) <= long_ma.shift(1))
|
|
80
|
+
bearish = (short_ma < long_ma) & (short_ma.shift(1) >= long_ma.shift(1))
|
|
81
|
+
|
|
82
|
+
df["Signal"] = 0
|
|
83
|
+
df.loc[bullish, "Signal"] = 1
|
|
84
|
+
df.loc[bearish, "Signal"] = -1
|
|
85
|
+
return df
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def trades_from_sma_signals(
|
|
89
|
+
signal_frame: pd.DataFrame,
|
|
90
|
+
ticker: str,
|
|
91
|
+
stop_loss: float = None,
|
|
92
|
+
take_profit: float = None,
|
|
93
|
+
) -> pd.DataFrame:
|
|
94
|
+
"""
|
|
95
|
+
Converts SMA entry/exit signals into a long-only trade log.
|
|
96
|
+
"""
|
|
97
|
+
required = {"Close", "Signal"}
|
|
98
|
+
missing = required - set(signal_frame.columns)
|
|
99
|
+
if missing:
|
|
100
|
+
raise ValueError(f"signal_frame is missing required columns: {sorted(missing)}")
|
|
101
|
+
|
|
102
|
+
trades = []
|
|
103
|
+
position = None
|
|
104
|
+
entry_price = None
|
|
105
|
+
entry_date = None
|
|
106
|
+
|
|
107
|
+
for exit_date, row in signal_frame.iterrows():
|
|
108
|
+
price = float(row["Close"])
|
|
109
|
+
signal = int(row["Signal"])
|
|
110
|
+
|
|
111
|
+
if position is None and signal == 1:
|
|
112
|
+
position = "long"
|
|
113
|
+
entry_price = price
|
|
114
|
+
entry_date = exit_date
|
|
115
|
+
elif position == "long":
|
|
116
|
+
pnl = (price - entry_price) / entry_price
|
|
117
|
+
hit_stop = stop_loss is not None and pnl <= -stop_loss
|
|
118
|
+
hit_take = take_profit is not None and pnl >= take_profit
|
|
119
|
+
|
|
120
|
+
if signal == -1 or hit_stop or hit_take:
|
|
121
|
+
trades.append(
|
|
122
|
+
{
|
|
123
|
+
"Ticker": ticker,
|
|
124
|
+
"Entry Date": entry_date,
|
|
125
|
+
"Exit Date": exit_date,
|
|
126
|
+
"Entry Price": entry_price,
|
|
127
|
+
"Exit Price": price,
|
|
128
|
+
"Return": pnl,
|
|
129
|
+
"Exit Reason": (
|
|
130
|
+
"Sell Signal" if signal == -1 else "Stop Loss" if hit_stop else "Take Profit"
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
position = None
|
|
135
|
+
|
|
136
|
+
return pd.DataFrame(trades, columns=TRADE_COLUMNS)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run_sma_strategy_with_risk(
|
|
140
|
+
ticker: str,
|
|
141
|
+
start: str,
|
|
142
|
+
end: str,
|
|
143
|
+
stop_loss: float = None,
|
|
144
|
+
take_profit: float = None,
|
|
145
|
+
short_window: int = 20,
|
|
146
|
+
long_window: int = 50,
|
|
147
|
+
prices: pd.DataFrame = None,
|
|
148
|
+
) -> pd.DataFrame:
|
|
149
|
+
"""
|
|
150
|
+
Runs an SMA crossover strategy for one ticker and returns a trade log.
|
|
151
|
+
"""
|
|
152
|
+
price_data = prices if prices is not None else download_close_prices(ticker, start, end)
|
|
153
|
+
signals = build_sma_signal_frame(price_data, short_window=short_window, long_window=long_window)
|
|
154
|
+
return trades_from_sma_signals(signals, ticker=ticker, stop_loss=stop_loss, take_profit=take_profit)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def run_strategy_on_portfolio(
|
|
158
|
+
asset_table: pd.DataFrame,
|
|
159
|
+
start: str,
|
|
160
|
+
end: str,
|
|
161
|
+
stop_loss: float = None,
|
|
162
|
+
take_profit: float = None,
|
|
163
|
+
short_window: int = 20,
|
|
164
|
+
long_window: int = 50,
|
|
165
|
+
) -> pd.DataFrame:
|
|
166
|
+
"""
|
|
167
|
+
Runs the SMA strategy across an asset table with Asset and Weight columns.
|
|
168
|
+
"""
|
|
169
|
+
if not {"Asset", "Weight"}.issubset(asset_table.columns):
|
|
170
|
+
raise ValueError("asset_table must contain Asset and Weight columns")
|
|
171
|
+
|
|
172
|
+
results = []
|
|
173
|
+
for _, row in asset_table.iterrows():
|
|
174
|
+
trades_df = run_sma_strategy_with_risk(
|
|
175
|
+
row["Asset"],
|
|
176
|
+
start,
|
|
177
|
+
end,
|
|
178
|
+
stop_loss=stop_loss,
|
|
179
|
+
take_profit=take_profit,
|
|
180
|
+
short_window=short_window,
|
|
181
|
+
long_window=long_window,
|
|
182
|
+
)
|
|
183
|
+
if trades_df.empty:
|
|
184
|
+
continue
|
|
185
|
+
trades_df["Weight"] = row["Weight"]
|
|
186
|
+
trades_df["Weighted Return"] = trades_df["Return"] * row["Weight"]
|
|
187
|
+
results.append(trades_df)
|
|
188
|
+
|
|
189
|
+
if not results:
|
|
190
|
+
return pd.DataFrame(columns=PORTFOLIO_TRADE_COLUMNS)
|
|
191
|
+
|
|
192
|
+
all_trades = pd.concat(results, ignore_index=True)
|
|
193
|
+
return all_trades.sort_values(by="Entry Date")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def plot_sma_strategy_cumulative_return(trade_log: pd.DataFrame, title="Portfolio Return"):
|
|
197
|
+
"""
|
|
198
|
+
Plots cumulative weighted returns from an SMA trade log.
|
|
199
|
+
"""
|
|
200
|
+
import matplotlib.pyplot as plt
|
|
201
|
+
|
|
202
|
+
fig, ax = plt.subplots(figsize=(20, 12))
|
|
203
|
+
|
|
204
|
+
if not trade_log.empty:
|
|
205
|
+
trade_log = trade_log.sort_values("Exit Date").copy()
|
|
206
|
+
trade_log["Cumulative Return"] = (1 + trade_log["Weighted Return"]).cumprod()
|
|
207
|
+
plt.plot(trade_log["Exit Date"], trade_log["Cumulative Return"], marker="o")
|
|
208
|
+
|
|
209
|
+
plt.title(title)
|
|
210
|
+
plt.xlabel("Date")
|
|
211
|
+
plt.ylabel("Cumulative Return")
|
|
212
|
+
plt.grid(alpha=0.3)
|
|
213
|
+
plt.text(
|
|
214
|
+
0.995,
|
|
215
|
+
-0.20,
|
|
216
|
+
"Created by RiskOptima",
|
|
217
|
+
fontsize=12,
|
|
218
|
+
color="gray",
|
|
219
|
+
alpha=0.7,
|
|
220
|
+
transform=ax.transAxes,
|
|
221
|
+
ha="right",
|
|
222
|
+
)
|
|
223
|
+
plt.tight_layout()
|
|
224
|
+
plt.show()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def plot_sma_strategy_trades(signal_frame: pd.DataFrame, ticker: str):
|
|
228
|
+
"""
|
|
229
|
+
Plots close prices, SMA bands, and buy/sell markers.
|
|
230
|
+
"""
|
|
231
|
+
import matplotlib.pyplot as plt
|
|
232
|
+
|
|
233
|
+
df = signal_frame.copy()
|
|
234
|
+
sma_cols = [col for col in df.columns if col.startswith("SMA")]
|
|
235
|
+
|
|
236
|
+
fig, ax = plt.subplots(figsize=(20, 12))
|
|
237
|
+
plt.plot(df.index, df["Close"], label="Close Price", alpha=0.5)
|
|
238
|
+
for col in sma_cols:
|
|
239
|
+
plt.plot(df.index, df[col], label=col, alpha=0.8)
|
|
240
|
+
|
|
241
|
+
plt.scatter(df.index[df["Signal"] == 1], df["Close"][df["Signal"] == 1], marker="^", color="green", s=100, label="Buy Signal")
|
|
242
|
+
plt.scatter(df.index[df["Signal"] == -1], df["Close"][df["Signal"] == -1], marker="v", color="red", s=100, label="Sell Signal")
|
|
243
|
+
plt.title(f"{ticker} - SMA Strategy with Signals")
|
|
244
|
+
plt.xlabel("Date")
|
|
245
|
+
plt.ylabel("Price")
|
|
246
|
+
plt.legend()
|
|
247
|
+
plt.grid(alpha=0.3)
|
|
248
|
+
plt.tight_layout()
|
|
249
|
+
|
|
250
|
+
plots_folder = "plots"
|
|
251
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
252
|
+
if not os.path.exists(plots_folder):
|
|
253
|
+
os.makedirs(plots_folder)
|
|
254
|
+
plot_path = os.path.join(plots_folder, f"riskoptima_sma_strategy_{timestamp}.png")
|
|
255
|
+
plt.savefig(plot_path, dpi=150, bbox_inches="tight")
|
|
256
|
+
plt.show()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def normalize_asset_table(tickers) -> pd.DataFrame:
|
|
260
|
+
"""
|
|
261
|
+
Normalizes a ticker string, ticker list, or asset table into Asset/Weight rows.
|
|
262
|
+
"""
|
|
263
|
+
if isinstance(tickers, str):
|
|
264
|
+
return pd.DataFrame([{"Asset": tickers, "Weight": 1.0}])
|
|
265
|
+
if isinstance(tickers, Iterable) and not isinstance(tickers, pd.DataFrame):
|
|
266
|
+
tickers = list(tickers)
|
|
267
|
+
return pd.DataFrame([{"Asset": ticker, "Weight": 1.0 / len(tickers)} for ticker in tickers])
|
|
268
|
+
if isinstance(tickers, pd.DataFrame):
|
|
269
|
+
return tickers.copy()
|
|
270
|
+
raise ValueError("Tickers must be a string, list, or DataFrame.")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def run_and_plot_sma_strategy(
|
|
274
|
+
tickers,
|
|
275
|
+
start_date,
|
|
276
|
+
end_date,
|
|
277
|
+
stop_loss=None,
|
|
278
|
+
take_profit=None,
|
|
279
|
+
short_window: int = 20,
|
|
280
|
+
long_window: int = 50,
|
|
281
|
+
):
|
|
282
|
+
"""
|
|
283
|
+
Runs and plots SMA signals plus cumulative weighted trade returns.
|
|
284
|
+
"""
|
|
285
|
+
asset_table = normalize_asset_table(tickers)
|
|
286
|
+
portfolio_trades = run_strategy_on_portfolio(
|
|
287
|
+
asset_table,
|
|
288
|
+
start=start_date,
|
|
289
|
+
end=end_date,
|
|
290
|
+
stop_loss=stop_loss,
|
|
291
|
+
take_profit=take_profit,
|
|
292
|
+
short_window=short_window,
|
|
293
|
+
long_window=long_window,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
for ticker in asset_table["Asset"]:
|
|
297
|
+
prices = download_close_prices(ticker, start_date, end_date)
|
|
298
|
+
signals = build_sma_signal_frame(prices, short_window=short_window, long_window=long_window)
|
|
299
|
+
plot_sma_strategy_trades(signals, ticker)
|
|
300
|
+
|
|
301
|
+
plot_sma_strategy_cumulative_return(portfolio_trades, title="SMA Strategy - Cumulative Return")
|
|
302
|
+
return portfolio_trades
|
|
@@ -41,10 +41,12 @@ def optimize_max_sharpe(
|
|
|
41
41
|
return -(port_ret - risk_free_rate) / port_vol
|
|
42
42
|
|
|
43
43
|
cons = [_sum_to_one_constraint()]
|
|
44
|
-
cons += factor_constraint_func(init_guess, factor_exposures, constraints.factor_bounds)
|
|
45
|
-
|
|
46
|
-
result = minimize(neg_sharpe, init_guess, method="SLSQP", bounds=bounds, constraints=cons)
|
|
47
|
-
|
|
44
|
+
cons += factor_constraint_func(init_guess, factor_exposures, constraints.factor_bounds)
|
|
45
|
+
|
|
46
|
+
result = minimize(neg_sharpe, init_guess, method="SLSQP", bounds=bounds, constraints=cons)
|
|
47
|
+
if not result.success:
|
|
48
|
+
raise ValueError(f"Max Sharpe optimization failed: {result.message}")
|
|
49
|
+
return pd.Series(result.x, index=expected_returns.index)
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
def optimize_min_variance(
|
|
@@ -67,8 +69,10 @@ def optimize_min_variance(
|
|
|
67
69
|
cons = [_sum_to_one_constraint()]
|
|
68
70
|
if target_return is not None and expected_returns is not None:
|
|
69
71
|
cons.append({"type": "eq", "fun": lambda w: np.dot(w, expected_returns.values) - target_return})
|
|
70
|
-
cons += factor_constraint_func(init_guess, factor_exposures, constraints.factor_bounds)
|
|
71
|
-
|
|
72
|
-
result = minimize(portfolio_vol, init_guess, method="SLSQP", bounds=bounds, constraints=cons)
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
cons += factor_constraint_func(init_guess, factor_exposures, constraints.factor_bounds)
|
|
73
|
+
|
|
74
|
+
result = minimize(portfolio_vol, init_guess, method="SLSQP", bounds=bounds, constraints=cons)
|
|
75
|
+
if not result.success:
|
|
76
|
+
raise ValueError(f"Minimum variance optimization failed: {result.message}")
|
|
77
|
+
index = expected_returns.index if expected_returns is not None else cov.index
|
|
78
|
+
return pd.Series(result.x, index=index)
|
|
@@ -18,8 +18,25 @@ def binomial_tree_price(S, K, T, r, sigma, steps=100, option_type="call", q=0.0,
|
|
|
18
18
|
option_type = option_type.lower()
|
|
19
19
|
if option_type not in {"call", "put"}:
|
|
20
20
|
raise ValueError("option_type must be 'call' or 'put'")
|
|
21
|
+
if S <= 0 or K <= 0:
|
|
22
|
+
raise ValueError("S and K must be positive")
|
|
23
|
+
if T < 0:
|
|
24
|
+
raise ValueError("T must be non-negative")
|
|
25
|
+
if sigma < 0:
|
|
26
|
+
raise ValueError("sigma must be non-negative")
|
|
21
27
|
if steps <= 0:
|
|
22
28
|
raise ValueError("steps must be positive")
|
|
29
|
+
if T == 0:
|
|
30
|
+
intrinsic = max(S - K, 0.0) if option_type == "call" else max(K - S, 0.0)
|
|
31
|
+
return float(intrinsic)
|
|
32
|
+
if sigma == 0:
|
|
33
|
+
discounted_strike = K * np.exp(-r * T)
|
|
34
|
+
deterministic = (
|
|
35
|
+
max(S * np.exp(-q * T) - discounted_strike, 0.0)
|
|
36
|
+
if option_type == "call"
|
|
37
|
+
else max(discounted_strike - S * np.exp(-q * T), 0.0)
|
|
38
|
+
)
|
|
39
|
+
return float(deterministic)
|
|
23
40
|
|
|
24
41
|
dt = T / steps
|
|
25
42
|
u = np.exp(sigma * np.sqrt(dt))
|
|
@@ -24,10 +24,16 @@ def black_scholes_price(S, K, T, r, sigma, option_type="call", q=0.0):
|
|
|
24
24
|
Prices a European option using the Black-Scholes-Merton formula.
|
|
25
25
|
"""
|
|
26
26
|
option_type = _option_type(option_type)
|
|
27
|
-
if
|
|
27
|
+
if S <= 0 or K <= 0:
|
|
28
|
+
raise ValueError("S and K must be positive")
|
|
29
|
+
if T < 0:
|
|
30
|
+
raise ValueError("T must be non-negative")
|
|
31
|
+
if sigma < 0:
|
|
32
|
+
raise ValueError("sigma must be non-negative")
|
|
33
|
+
if T == 0:
|
|
28
34
|
intrinsic = max(S - K, 0.0) if option_type == "call" else max(K - S, 0.0)
|
|
29
35
|
return float(intrinsic)
|
|
30
|
-
if sigma
|
|
36
|
+
if sigma == 0:
|
|
31
37
|
forward_intrinsic = (
|
|
32
38
|
max(S * np.exp(-q * T) - K * np.exp(-r * T), 0.0)
|
|
33
39
|
if option_type == "call"
|
|
@@ -15,6 +15,28 @@ from scipy.stats import norm
|
|
|
15
15
|
from riskoptima.core import RiskReport
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _validated_weights(weights, columns) -> pd.Series:
|
|
19
|
+
if weights is None:
|
|
20
|
+
return pd.Series(np.repeat(1.0 / len(columns), len(columns)), index=columns, dtype=float)
|
|
21
|
+
|
|
22
|
+
if isinstance(weights, pd.Series):
|
|
23
|
+
missing = [col for col in columns if col not in weights.index]
|
|
24
|
+
if missing:
|
|
25
|
+
raise ValueError(f"weights are missing assets: {missing}")
|
|
26
|
+
w = weights.reindex(columns).astype(float)
|
|
27
|
+
else:
|
|
28
|
+
values = np.asarray(weights, dtype=float)
|
|
29
|
+
if values.ndim != 1 or len(values) != len(columns):
|
|
30
|
+
raise ValueError("weights must be a 1D array-like with one value per return column")
|
|
31
|
+
w = pd.Series(values, index=columns, dtype=float)
|
|
32
|
+
|
|
33
|
+
if not np.isfinite(w).all():
|
|
34
|
+
raise ValueError("weights must be finite")
|
|
35
|
+
if np.isclose(w.sum(), 0.0):
|
|
36
|
+
raise ValueError("weights must not sum to zero")
|
|
37
|
+
return w / w.sum()
|
|
38
|
+
|
|
39
|
+
|
|
18
40
|
def _portfolio_returns(returns, weights=None) -> pd.Series:
|
|
19
41
|
data = pd.DataFrame(returns).copy() if not isinstance(returns, pd.Series) else returns.to_frame("portfolio")
|
|
20
42
|
data = data.apply(pd.to_numeric, errors="coerce").dropna(how="all")
|
|
@@ -23,14 +45,9 @@ def _portfolio_returns(returns, weights=None) -> pd.Series:
|
|
|
23
45
|
return data.iloc[:, 0].dropna().rename("portfolio")
|
|
24
46
|
|
|
25
47
|
clean = data.dropna(how="any")
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
w = pd.Series(weights, index=clean.columns if len(weights) == clean.shape[1] else None, dtype=float)
|
|
30
|
-
if list(w.index) != list(clean.columns):
|
|
31
|
-
w = pd.Series(np.asarray(weights, dtype=float), index=clean.columns)
|
|
32
|
-
if not np.isclose(w.sum(), 1.0):
|
|
33
|
-
w = w / w.sum()
|
|
48
|
+
if clean.empty:
|
|
49
|
+
raise ValueError("returns must contain at least one complete finite row")
|
|
50
|
+
w = _validated_weights(weights, clean.columns)
|
|
34
51
|
return clean.dot(w).rename("portfolio")
|
|
35
52
|
|
|
36
53
|
|
|
@@ -79,7 +79,7 @@ warnings.filterwarnings(
|
|
|
79
79
|
|
|
80
80
|
class RiskOptima:
|
|
81
81
|
TRADING_DAYS = 260 # default is 260, though 252 is also common
|
|
82
|
-
VERSION = '2.3.
|
|
82
|
+
VERSION = '2.3.2'
|
|
83
83
|
|
|
84
84
|
@staticmethod
|
|
85
85
|
def get_trading_days():
|
|
@@ -3479,234 +3479,52 @@ class RiskOptima:
|
|
|
3479
3479
|
|
|
3480
3480
|
@staticmethod
|
|
3481
3481
|
def run_sma_strategy_with_risk(ticker: str, start: str, end: str, stop_loss: float = None, take_profit: float = None):
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
df = yf.download(ticker, start=start, end=end, progress=False, auto_adjust=False)[['Close']].copy()
|
|
3493
|
-
|
|
3494
|
-
df['SMA20'] = df['Close'].rolling(20).mean()
|
|
3495
|
-
df['SMA50'] = df['Close'].rolling(50).mean()
|
|
3496
|
-
|
|
3497
|
-
df['Signal'] = 0
|
|
3498
|
-
df.loc[df.index[50]:, 'Signal'] = (
|
|
3499
|
-
(df['SMA20'][50:] > df['SMA50'][50:]) &
|
|
3500
|
-
(df['SMA20'].shift(1)[50:] <= df['SMA50'].shift(1)[50:])
|
|
3501
|
-
).astype(int) - (
|
|
3502
|
-
(df['SMA20'][50:] < df['SMA50'][50:]) &
|
|
3503
|
-
(df['SMA20'].shift(1)[50:] >= df['SMA50'].shift(1)[50:])
|
|
3504
|
-
).astype(int)
|
|
3505
|
-
|
|
3506
|
-
trades = []
|
|
3507
|
-
position = None
|
|
3508
|
-
entry_price = None
|
|
3509
|
-
entry_date = None
|
|
3510
|
-
|
|
3511
|
-
for exit_date, row in df.iterrows():
|
|
3512
|
-
price = row['Close'].item() if hasattr(row['Close'], 'item') else float(row['Close'])
|
|
3513
|
-
signal = row['Signal'].item() if hasattr(row['Signal'], 'item') else int(row['Signal'])
|
|
3514
|
-
|
|
3515
|
-
if position is None and signal == 1:
|
|
3516
|
-
position = 'long'
|
|
3517
|
-
entry_price = price
|
|
3518
|
-
entry_date = exit_date
|
|
3519
|
-
|
|
3520
|
-
elif position == 'long':
|
|
3521
|
-
pnl = (price - entry_price) / entry_price
|
|
3522
|
-
hit_stop = stop_loss is not None and pnl <= -stop_loss
|
|
3523
|
-
hit_take = take_profit is not None and pnl >= take_profit
|
|
3524
|
-
|
|
3525
|
-
if signal == -1 or hit_stop or hit_take:
|
|
3526
|
-
trades.append({
|
|
3527
|
-
'Ticker': ticker,
|
|
3528
|
-
'Entry Date': entry_date,
|
|
3529
|
-
'Exit Date': exit_date,
|
|
3530
|
-
'Entry Price': entry_price,
|
|
3531
|
-
'Exit Price': price,
|
|
3532
|
-
'Return': pnl,
|
|
3533
|
-
'Exit Reason': (
|
|
3534
|
-
'Sell Signal' if signal == -1 else
|
|
3535
|
-
'Stop Loss' if hit_stop else
|
|
3536
|
-
'Take Profit'
|
|
3537
|
-
)
|
|
3538
|
-
})
|
|
3539
|
-
position = None
|
|
3540
|
-
|
|
3541
|
-
return pd.DataFrame(trades, columns=trade_columns)
|
|
3482
|
+
from riskoptima.backtest.sma import run_sma_strategy_with_risk
|
|
3483
|
+
|
|
3484
|
+
return run_sma_strategy_with_risk(
|
|
3485
|
+
ticker=ticker,
|
|
3486
|
+
start=start,
|
|
3487
|
+
end=end,
|
|
3488
|
+
stop_loss=stop_loss,
|
|
3489
|
+
take_profit=take_profit,
|
|
3490
|
+
)
|
|
3542
3491
|
|
|
3543
3492
|
@staticmethod
|
|
3544
|
-
def run_strategy_on_portfolio(asset_table: pd.DataFrame, start: str, end: str,
|
|
3545
|
-
stop_loss: float = None, take_profit: float = None):
|
|
3546
|
-
|
|
3547
|
-
print(asset_table)
|
|
3548
|
-
for _, row in asset_table.iterrows():
|
|
3549
|
-
ticker = row['Asset']
|
|
3550
|
-
weight = row['Weight']
|
|
3551
|
-
trades_df = RiskOptima.run_sma_strategy_with_risk(
|
|
3552
|
-
ticker, start, end,
|
|
3553
|
-
stop_loss=stop_loss, take_profit=take_profit
|
|
3554
|
-
)
|
|
3555
|
-
trades_df['Weight'] = weight
|
|
3556
|
-
trades_df['Weighted Return'] = trades_df['Return'] * weight if 'Return' in trades_df else pd.Series(dtype=float)
|
|
3557
|
-
results.append(trades_df)
|
|
3558
|
-
|
|
3559
|
-
if not results:
|
|
3560
|
-
return pd.DataFrame(columns=[
|
|
3561
|
-
'Ticker', 'Entry Date', 'Exit Date', 'Entry Price', 'Exit Price',
|
|
3562
|
-
'Return', 'Exit Reason', 'Weight', 'Weighted Return'
|
|
3563
|
-
])
|
|
3493
|
+
def run_strategy_on_portfolio(asset_table: pd.DataFrame, start: str, end: str,
|
|
3494
|
+
stop_loss: float = None, take_profit: float = None):
|
|
3495
|
+
from riskoptima.backtest.sma import run_strategy_on_portfolio
|
|
3564
3496
|
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3497
|
+
return run_strategy_on_portfolio(
|
|
3498
|
+
asset_table=asset_table,
|
|
3499
|
+
start=start,
|
|
3500
|
+
end=end,
|
|
3501
|
+
stop_loss=stop_loss,
|
|
3502
|
+
take_profit=take_profit,
|
|
3503
|
+
)
|
|
3570
3504
|
|
|
3571
3505
|
@staticmethod
|
|
3572
3506
|
def plot_sma_strategy_cumulative_return(trade_log: pd.DataFrame, title="Portfolio Return"):
|
|
3573
|
-
|
|
3574
|
-
fig, ax = plt.subplots(figsize=(20, 12))
|
|
3575
|
-
plt.title(title)
|
|
3576
|
-
plt.xlabel("Date")
|
|
3577
|
-
plt.ylabel("Cumulative Return")
|
|
3578
|
-
plt.grid(alpha=0.3)
|
|
3579
|
-
plt.text(
|
|
3580
|
-
0.995, -0.20, f"Created by RiskOptima v{RiskOptima.VERSION}",
|
|
3581
|
-
fontsize=12, color='gray', alpha=0.7, transform=ax.transAxes, ha='right'
|
|
3582
|
-
)
|
|
3583
|
-
plt.tight_layout()
|
|
3584
|
-
plt.show()
|
|
3585
|
-
return
|
|
3507
|
+
from riskoptima.backtest.sma import plot_sma_strategy_cumulative_return
|
|
3586
3508
|
|
|
3587
|
-
trade_log
|
|
3588
|
-
trade_log['Cumulative Return'] = (1 + trade_log['Weighted Return']).cumprod()
|
|
3589
|
-
|
|
3590
|
-
fig, ax = plt.subplots(figsize=(20, 12))
|
|
3591
|
-
|
|
3592
|
-
plt.plot(trade_log['Exit Date'], trade_log['Cumulative Return'], marker='o')
|
|
3593
|
-
plt.title(title)
|
|
3594
|
-
plt.xlabel("Date")
|
|
3595
|
-
plt.ylabel("Cumulative Return")
|
|
3596
|
-
plt.grid(alpha=0.3)
|
|
3597
|
-
|
|
3598
|
-
plt.text(
|
|
3599
|
-
0.995, -0.20, f"Created by RiskOptima v{RiskOptima.VERSION}",
|
|
3600
|
-
fontsize=12, color='gray', alpha=0.7, transform=ax.transAxes, ha='right'
|
|
3601
|
-
)
|
|
3602
|
-
|
|
3603
|
-
plt.tight_layout()
|
|
3604
|
-
|
|
3605
|
-
plots_folder = "plots"
|
|
3606
|
-
|
|
3607
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
3608
|
-
|
|
3609
|
-
if not os.path.exists(plots_folder):
|
|
3610
|
-
os.makedirs(plots_folder)
|
|
3611
|
-
|
|
3612
|
-
plot_path = os.path.join(plots_folder, f"riskoptima_sma_strategy_cum_ret_{timestamp}.png")
|
|
3613
|
-
|
|
3614
|
-
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
|
|
3615
|
-
plt.show()
|
|
3509
|
+
return plot_sma_strategy_cumulative_return(trade_log=trade_log, title=title)
|
|
3616
3510
|
|
|
3617
3511
|
@staticmethod
|
|
3618
|
-
def plot_sma_strategy_trades(df: pd.DataFrame, ticker: str):
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
plt.plot(df.index, df['SMA50'], label='SMA50', alpha=0.8)
|
|
3623
|
-
|
|
3624
|
-
plt.scatter(df.index[df['Signal'] == 1], df['Close'][df['Signal'] == 1],
|
|
3625
|
-
marker='^', color='green', s=100, label='Buy Signal')
|
|
3626
|
-
plt.scatter(df.index[df['Signal'] == -1], df['Close'][df['Signal'] == -1],
|
|
3627
|
-
marker='v', color='red', s=100, label='Sell Signal')
|
|
3628
|
-
|
|
3629
|
-
plt.title(f"{ticker} - SMA Strategy with Signals")
|
|
3630
|
-
plt.xlabel("Date")
|
|
3631
|
-
plt.ylabel("Price")
|
|
3632
|
-
plt.legend()
|
|
3633
|
-
plt.grid(alpha=0.3)
|
|
3634
|
-
|
|
3635
|
-
plt.text(
|
|
3636
|
-
0.995, -0.20, f"Created by RiskOptima v{RiskOptima.VERSION}",
|
|
3637
|
-
fontsize=12, color='gray', alpha=0.7, transform=ax.transAxes, ha='right'
|
|
3638
|
-
)
|
|
3639
|
-
|
|
3640
|
-
plt.tight_layout()
|
|
3641
|
-
|
|
3642
|
-
plots_folder = "plots"
|
|
3643
|
-
|
|
3644
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
3645
|
-
|
|
3646
|
-
if not os.path.exists(plots_folder):
|
|
3647
|
-
os.makedirs(plots_folder)
|
|
3648
|
-
|
|
3649
|
-
plot_path = os.path.join(plots_folder, f"riskoptima_sma_strategy_{timestamp}.png")
|
|
3650
|
-
|
|
3651
|
-
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
|
|
3652
|
-
|
|
3653
|
-
plt.show()
|
|
3512
|
+
def plot_sma_strategy_trades(df: pd.DataFrame, ticker: str):
|
|
3513
|
+
from riskoptima.backtest.sma import plot_sma_strategy_trades
|
|
3514
|
+
|
|
3515
|
+
return plot_sma_strategy_trades(signal_frame=df, ticker=ticker)
|
|
3654
3516
|
|
|
3655
3517
|
@staticmethod
|
|
3656
|
-
def run_and_plot_sma_strategy(tickers, start_date, end_date, stop_loss=None, take_profit=None):
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
# Run portfolio strategy
|
|
3668
|
-
portfolio_trades = RiskOptima.run_strategy_on_portfolio(
|
|
3669
|
-
asset_table, start=start_date, end=end_date,
|
|
3670
|
-
stop_loss=stop_loss, take_profit=take_profit
|
|
3671
|
-
)
|
|
3672
|
-
|
|
3673
|
-
# If only one ticker, also show price chart with signals
|
|
3674
|
-
if len(asset_table) == 1:
|
|
3675
|
-
ticker = asset_table.iloc[0]['Asset']
|
|
3676
|
-
df = yf.download(ticker, start=start_date, end=end_date, progress=False, auto_adjust=False)[['Close']]
|
|
3677
|
-
df['SMA20'] = df['Close'].rolling(20).mean()
|
|
3678
|
-
df['SMA50'] = df['Close'].rolling(50).mean()
|
|
3679
|
-
df['Signal'] = 0
|
|
3680
|
-
df.loc[df.index[50]:, 'Signal'] = (
|
|
3681
|
-
(df['SMA20'][50:] > df['SMA50'][50:]) &
|
|
3682
|
-
(df['SMA20'].shift(1)[50:] <= df['SMA50'].shift(1)[50:])
|
|
3683
|
-
).astype(int) - (
|
|
3684
|
-
(df['SMA20'][50:] < df['SMA50'][50:]) &
|
|
3685
|
-
(df['SMA20'].shift(1)[50:] >= df['SMA50'].shift(1)[50:])
|
|
3686
|
-
).astype(int)
|
|
3687
|
-
|
|
3688
|
-
RiskOptima.plot_sma_strategy_trades(df, ticker)
|
|
3689
|
-
|
|
3690
|
-
else:
|
|
3691
|
-
for ticker in asset_table['Asset']:
|
|
3692
|
-
df = yf.download(ticker, start=start_date, end=end_date, progress=False, auto_adjust=False)[['Close']]
|
|
3693
|
-
df['SMA20'] = df['Close'].rolling(20).mean()
|
|
3694
|
-
df['SMA50'] = df['Close'].rolling(50).mean()
|
|
3695
|
-
df['Signal'] = 0
|
|
3696
|
-
df.loc[df.index[50]:, 'Signal'] = (
|
|
3697
|
-
(df['SMA20'][50:] > df['SMA50'][50:]) &
|
|
3698
|
-
(df['SMA20'].shift(1)[50:] <= df['SMA50'].shift(1)[50:])
|
|
3699
|
-
).astype(int) - (
|
|
3700
|
-
(df['SMA20'][50:] < df['SMA50'][50:]) &
|
|
3701
|
-
(df['SMA20'].shift(1)[50:] >= df['SMA50'].shift(1)[50:])
|
|
3702
|
-
).astype(int)
|
|
3703
|
-
|
|
3704
|
-
RiskOptima.plot_sma_strategy_trades(df, ticker)
|
|
3705
|
-
|
|
3706
|
-
# Always plot cumulative return
|
|
3707
|
-
RiskOptima.plot_sma_strategy_cumulative_return(portfolio_trades, title="SMA Strategy - Cumulative Return")
|
|
3708
|
-
|
|
3709
|
-
return portfolio_trades
|
|
3518
|
+
def run_and_plot_sma_strategy(tickers, start_date, end_date, stop_loss=None, take_profit=None):
|
|
3519
|
+
from riskoptima.backtest.sma import run_and_plot_sma_strategy
|
|
3520
|
+
|
|
3521
|
+
return run_and_plot_sma_strategy(
|
|
3522
|
+
tickers=tickers,
|
|
3523
|
+
start_date=start_date,
|
|
3524
|
+
end_date=end_date,
|
|
3525
|
+
stop_loss=stop_loss,
|
|
3526
|
+
take_profit=take_profit,
|
|
3527
|
+
)
|
|
3710
3528
|
|
|
3711
3529
|
@staticmethod
|
|
3712
3530
|
def implied_volatility_screener(symbol='AMZN', lookback_days=30):
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
###############################################################################
|
|
2
|
-
# __init__.py
|
|
3
|
-
###############################################################################
|
|
4
|
-
# Product: RiskOptima
|
|
5
|
-
# Author: Jordi Corbilla
|
|
6
|
-
# Description: RiskOptima module
|
|
7
|
-
###############################################################################
|
|
8
|
-
|
|
9
|
-
from .engine import run_backtest
|
|
10
|
-
from .strategy import Strategy, SMACrossStrategy
|
|
11
|
-
from .portfolio import PortfolioState
|
|
12
|
-
|
|
13
|
-
__all__ = ["run_backtest", "Strategy", "SMACrossStrategy", "PortfolioState"]
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|