Spaces:
Sleeping
Sleeping
File size: 10,032 Bytes
fbc251b 76cf431 fbc251b a9b62f9 fbc251b 4029617 fbc251b 9911ddc 4029617 fbc251b 76cf431 fbc251b 96543c9 fbc251b 96543c9 fbc251b 6fbfeb1 fbc251b 4029617 fbc251b 4029617 de5d0fc 4029617 fbc251b 32f8f8b fbc251b 4029617 fbc251b 4029617 39b0c3f fbc251b 4029617 fbc251b 54ea77d 96543c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | import streamlit as st
import pandas as pd
import numpy as np
import yfinance as yf
import plotly.graph_objs as go
from datetime import datetime, timedelta
# Set Streamlit page configuration
st.set_page_config(page_title="EMA Envelope Strategy Optimization", layout="wide")
# Title and Description
st.title("EMA Envelope Strategy Optimization")
st.write("""
This tool backtests and optimizes an 'EMA Envelope' strategy.
It generates buy signals when the price drops below the lower envelope and sell signals when it rises above the upper envelope.
Key parameters include the ATR lookback, envelope width, and a sensitivity threshold.
You can optimize parameters and adjust them post-run to see their impact on signals and the equity curve.
""")
# Sidebar: How to use the app
with st.sidebar.expander("How to Use", expanded=False):
st.write("""
1. **Select Ticker**: Choose the asset ticker symbol (e.g., AAPL, TSLA) and date range for historical data.
2. **Run Strategy**: Click "Run Strategy" to perform optimization.
3. **Adjust Parameters**: After running the strategy, you can adjust the moving average windows, envelope settings, and threshold to see updated results live.
""")
st.sidebar.title("Input Parameters")
# Sidebar: Select Ticker and Date Range
with st.sidebar.expander("Asset Settings", expanded=True):
ticker = st.text_input("Asset Symbol", value="GOOGL", help="Ticker symbol (e.g., GOOGL, AAPL)")
start_date = st.date_input("Start Date", value=pd.to_datetime("2020-01-01"), help="Select the start date for historical data.")
end_date = st.date_input("End Date", value=pd.to_datetime("today") + pd.DateOffset(1), help="Select the end date for historical data.")
# Function to download data with yfinance adjustments
@st.cache_data
def download_data(ticker, start, end):
data = yf.download(ticker, start=start, end=end, auto_adjust=False)
if isinstance(data.columns, pd.MultiIndex):
data.columns = data.columns.get_level_values(0)
if data.empty:
raise ValueError(f"No data fetched for {ticker} from {start} to {end}.")
return data
# Function to calculate cumulative return with the strategy
def calculate_cumulative_return(data, atr_lookback, envelope_pct, atr_smoothing=14, threshold_multiplier=1, signal_threshold=0):
data['TR'] = np.maximum(data['High'] - data['Low'],
np.maximum(np.abs(data['High'] - data['Close'].shift(1)),
np.abs(data['Low'] - data['Close'].shift(1))))
data['ATR'] = data['TR'].ewm(span=atr_smoothing, adjust=False).mean()
data['Volatility'] = data['Close'].pct_change().rolling(window=atr_lookback).std()
data['Lookback'] = (data['ATR'] / data['ATR'].max() * atr_lookback * data['Volatility']).apply(np.ceil)
data = data.dropna()
data['EMA'] = data['Close'].ewm(span=atr_lookback, adjust=False).mean()
data['Upper_Envelope'] = data['EMA'] * (1 + envelope_pct * threshold_multiplier)
data['Lower_Envelope'] = data['EMA'] * (1 - envelope_pct * threshold_multiplier)
data['Signal'] = 0
data.loc[data['Close'] >= data['Upper_Envelope'] * (1 - abs(signal_threshold)), 'Signal'] = -1
data.loc[data['Close'] <= data['Lower_Envelope'] * (1 + abs(signal_threshold)), 'Signal'] = 1
data['Return'] = data['Close'].pct_change()
data['Strategy_Return'] = data['Return'] * data['Signal'].shift(1)
data['Cumulative_Return'] = (1 + data['Strategy_Return']).cumprod()
return data
# Grid search and optimization
def optimize_parameters(data, atr_lookback_range, envelope_pct_range, threshold_multiplier_range):
best_return = -np.inf
best_params = (None, None, None)
for atr_lookback in atr_lookback_range:
for envelope_pct in envelope_pct_range:
for threshold_multiplier in threshold_multiplier_range:
temp_data = calculate_cumulative_return(data.copy(), atr_lookback, envelope_pct, threshold_multiplier=threshold_multiplier)
cumulative_return = temp_data['Cumulative_Return'].iloc[-1]
if cumulative_return > best_return:
best_return = cumulative_return
best_params = (atr_lookback, envelope_pct, threshold_multiplier)
return best_params
# Run Button to Fetch Data and Run Strategy
run_button = st.sidebar.button("Run Strategy")
if run_button:
data = download_data(ticker, start_date, end_date)
# Default ranges for optimization
atr_lookback_range = range(10, 21)
envelope_pct_range = np.linspace(0.01, 0.05, 9)
threshold_multiplier_range = np.linspace(0.5, 1.5, 5)
# Optimize parameters
best_params = optimize_parameters(data, atr_lookback_range, envelope_pct_range, threshold_multiplier_range)
# Store best params in session state
st.session_state["best_params"] = best_params
st.session_state["data"] = data
st.session_state["signal_threshold"] = 0.00 # Default threshold
# If best_params are stored in session state, allow adjustments post-run
if "best_params" in st.session_state:
best_params = st.session_state["best_params"]
# Allow adjustments for ATR Lookback, Envelope Percentage, Threshold Multiplier, and Signal Threshold
atr_lookback = st.sidebar.slider(
"ATR Lookback", 10, 20, best_params[0], 1,
help="ATR Lookback determines how far back in time the system looks to calculate the Average True Range (ATR), a measure of market volatility. Increasing this value will smooth volatility and lead to slower, more stable envelope adjustments."
)
envelope_pct = st.sidebar.slider(
"Envelope Percentage", 0.01, 0.05, best_params[1], 0.001,
help="Envelope Percentage defines the distance between the EMA and the upper and lower envelopes. A higher percentage widens the envelope, leading to fewer signals, while a lower percentage tightens the envelope, leading to more frequent signals."
)
threshold_multiplier = st.sidebar.slider(
"Threshold Multiplier", 0.5, 1.5, best_params[2], 0.1,
help="Threshold Multiplier adjusts how sensitive the envelope is to price changes. Increasing this multiplier makes the envelope more lenient, reducing the number of signals, while decreasing it makes the envelope more restrictive."
)
signal_threshold = st.sidebar.slider(
"Signal Threshold", -0.05, 0.05, st.session_state["signal_threshold"], 0.01,
help=("Signal Threshold adjusts the sensitivity of buy/sell signals relative to the EMA envelopes. "
"Lower (negative) values make the strategy more lenient by generating more signals with smaller price deviations. "
"Higher (positive) values make the strategy more restrictive, requiring larger price movements to trigger fewer but potentially more reliable signals.")
)
st.session_state["signal_threshold"] = signal_threshold
# Calculate strategy with adjusted parameters
optimized_data = calculate_cumulative_return(st.session_state["data"], atr_lookback, envelope_pct,
threshold_multiplier=threshold_multiplier, signal_threshold=signal_threshold)
# Display best parameters in JSON format
st.json({
"Best Parameters": {
"ATR Lookback": best_params[0],
"Envelope Percentage": best_params[1],
"Threshold Multiplier": best_params[2]
}
})
# Plot the stock price, EMA, and envelopes with adjusted parameters
fig = go.Figure()
# Add price trace
fig.add_trace(go.Scatter(x=optimized_data.index, y=optimized_data['Close'], mode='lines', name='Close Price'))
# Add volume as a secondary Y-axis (transparent fill)
fig.add_trace(go.Bar(x=optimized_data.index, y=st.session_state["data"]['Volume'], name='Volume', yaxis='y2', opacity=0.3, marker_color='gray'))
# Add EMA and Envelopes
fig.add_trace(go.Scatter(x=optimized_data.index, y=optimized_data['EMA'], mode='lines', name='EMA', line=dict(dash='dash')))
fig.add_trace(go.Scatter(x=optimized_data.index, y=optimized_data['Upper_Envelope'], mode='lines', name='Upper Envelope', line=dict(dash='dash')))
fig.add_trace(go.Scatter(x=optimized_data.index, y=optimized_data['Lower_Envelope'], mode='lines', name='Lower Envelope', line=dict(dash='dash')))
# Buy/Sell Signals
buy_signals = optimized_data[optimized_data['Signal'] == 1]
sell_signals = optimized_data[optimized_data['Signal'] == -1]
fig.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['Close'], mode='markers', name='Buy Signal', marker=dict(color='green', symbol='triangle-up', size=15)))
fig.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['Close'], mode='markers', name='Sell Signal', marker=dict(color='red', symbol='triangle-down', size=15)))
# Update layout to include secondary Y-axis for volume
fig.update_layout(
title=f'{ticker} Exponential Moving Average Envelope Strategy',
xaxis_title='Date',
yaxis_title='Price',
yaxis2=dict(title='Volume', overlaying='y', side='right', showgrid=False),
legend=dict(orientation="h", yanchor="bottom", y=1.20, xanchor="center", x=0.5, traceorder='normal'),
height=600,
margin=dict(t=30, b=30)
)
st.plotly_chart(fig, use_container_width=True)
# Plot the equity curve
fig_equity = go.Figure()
fig_equity.add_trace(go.Scatter(x=optimized_data.index, y=optimized_data['Cumulative_Return'], mode='lines', name='Equity Curve'))
fig_equity.update_layout(
title='Equity Curve',
xaxis_title='Date',
yaxis_title='Cumulative Return',
legend=dict(orientation="h", yanchor="bottom", y=1.05, xanchor="center", x=0.5),
height=400
)
st.plotly_chart(fig_equity, use_container_width=True)
hide_menu_style = """
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_menu_style, unsafe_allow_html=True) |