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)