|
import streamlit as st |
|
import yfinance as yf |
|
import pandas as pd |
|
import numpy as np |
|
import matplotlib.pyplot as plt |
|
from datetime import timedelta |
|
|
|
|
|
st.title("Extended MACD-RSI Combo Strategy for SPY") |
|
st.markdown(""" |
|
This app demonstrates an extended MACD-RSI based trading strategy on SPY with the following features: |
|
- **Multiple Simultaneous Positions:** Each buy signal creates a new position. |
|
- **Dynamic Trailing Stop:** Each open position is updated with a trailing stop. |
|
- **Configurable Parameters:** Adjust strategy parameters via the sidebar. |
|
- **Buy Rule:** |
|
Buy a fraction of available cash when: |
|
- The MACD line crosses above its signal line. |
|
- RSI is below 50. |
|
- No buy has been executed in the last few days. |
|
- **Sell Rule:** |
|
For each position: |
|
- **Partial Sell:** Sell a fraction of the position when the price reaches a target multiple of the entry price and RSI is above 50. |
|
- **Trailing Stop Exit:** If the price falls below the position’s dynamic trailing stop, sell the entire position. |
|
""") |
|
|
|
|
|
st.sidebar.header("Strategy Parameters") |
|
initial_capital = st.sidebar.number_input("Initial Capital ($)", min_value=1000, max_value=1000000, value=100000, step=1000) |
|
buy_fraction = st.sidebar.slider("Buy Fraction (of available cash)", 0.05, 0.50, 0.15, 0.05) |
|
sell_fraction = st.sidebar.slider("Partial Sell Fraction", 0.10, 0.90, 0.40, 0.05) |
|
target_multiplier = st.sidebar.slider("Target Multiplier", 1.01, 1.20, 1.08, 0.01) |
|
trailing_stop_pct = st.sidebar.slider("Trailing Stop (%)", 0.01, 0.20, 0.08, 0.01) |
|
min_days_between_buys = st.sidebar.number_input("Minimum Days Between Buys", min_value=1, max_value=10, value=2) |
|
|
|
|
|
@st.cache_data |
|
def load_data(ticker, period="1y"): |
|
data = yf.download(ticker, period=period) |
|
data.dropna(inplace=True) |
|
return data |
|
|
|
data_load_state = st.text("Loading SPY data...") |
|
data = load_data("SPY", period="1y") |
|
data_load_state.text("Loading SPY data...done!") |
|
|
|
|
|
|
|
|
|
data['EMA12'] = data['Close'].ewm(span=12, adjust=False).mean() |
|
data['EMA26'] = data['Close'].ewm(span=26, adjust=False).mean() |
|
data['MACD'] = data['EMA12'] - data['EMA26'] |
|
data['MACD_signal'] = data['MACD'].ewm(span=9, adjust=False).mean() |
|
|
|
|
|
delta = data['Close'].diff() |
|
gain = delta.where(delta > 0, 0) |
|
loss = -delta.where(delta < 0, 0) |
|
avg_gain = gain.rolling(window=14).mean() |
|
avg_loss = loss.rolling(window=14).mean() |
|
rs = avg_gain / avg_loss |
|
data['RSI'] = 100 - (100 / (1 + rs)) |
|
|
|
|
|
data['Buy'] = False |
|
data['Sell'] = False |
|
|
|
|
|
cash = initial_capital |
|
equity_curve = [] |
|
last_buy_date = None |
|
open_positions = [] |
|
completed_trades = [] |
|
|
|
|
|
for i in range(1, len(data)): |
|
today = data.index[i] |
|
price = float(data['Close'].iloc[i]) |
|
rsi_today = float(data['RSI'].iloc[i]) |
|
|
|
|
|
macd_today = float(data['MACD'].iloc[i]) |
|
signal_today = float(data['MACD_signal'].iloc[i]) |
|
macd_yesterday = float(data['MACD'].iloc[i-1]) |
|
signal_yesterday = float(data['MACD_signal'].iloc[i-1]) |
|
|
|
buy_condition = (macd_yesterday < signal_yesterday) and (macd_today > signal_today) and (rsi_today < 50) |
|
|
|
if buy_condition and (last_buy_date is None or (today - last_buy_date).days >= min_days_between_buys): |
|
allocation = cash * buy_fraction |
|
if allocation > 0: |
|
shares_bought = allocation / price |
|
cash -= allocation |
|
last_buy_date = today |
|
open_positions.append({ |
|
"entry_date": today, |
|
"entry_price": price, |
|
"allocated": allocation, |
|
"shares": shares_bought, |
|
"highest": price, |
|
"trailing_stop": price * (1 - trailing_stop_pct) |
|
}) |
|
data.at[today, 'Buy'] = True |
|
|
|
|
|
positions_to_remove = [] |
|
for idx, pos in enumerate(open_positions): |
|
current_highest = pos["highest"] |
|
if price > current_highest: |
|
pos["highest"] = price |
|
pos["trailing_stop"] = pos["highest"] * (1 - trailing_stop_pct) |
|
|
|
|
|
if price >= (pos["entry_price"] * target_multiplier) and rsi_today > 50: |
|
shares_to_sell = pos["shares"] * sell_fraction |
|
cash += shares_to_sell * price |
|
pos["shares"] -= shares_to_sell |
|
pos["allocated"] -= shares_to_sell * pos["entry_price"] |
|
data.at[today, 'Sell'] = True |
|
if pos["shares"] < 0.001: |
|
completed_trades.append({ |
|
"entry_date": pos["entry_date"], |
|
"exit_date": today, |
|
"entry_price": pos["entry_price"], |
|
"exit_price": price, |
|
"allocated": pos["allocated"] |
|
}) |
|
positions_to_remove.append(idx) |
|
continue |
|
|
|
|
|
current_trailing_stop = pos["trailing_stop"] |
|
if price < current_trailing_stop: |
|
cash += pos["shares"] * price |
|
completed_trades.append({ |
|
"entry_date": pos["entry_date"], |
|
"exit_date": today, |
|
"entry_price": pos["entry_price"], |
|
"exit_price": price, |
|
"allocated": pos["allocated"] |
|
}) |
|
positions_to_remove.append(idx) |
|
|
|
for idx in reversed(positions_to_remove): |
|
del open_positions[idx] |
|
|
|
|
|
position_value = sum(pos["shares"] * price for pos in open_positions) |
|
equity_curve.append(cash + position_value) |
|
|
|
|
|
performance = pd.DataFrame({ |
|
'Date': data.index[1:len(equity_curve)+1], |
|
'Equity': equity_curve |
|
}).set_index('Date') |
|
|
|
|
|
st.subheader("Equity Curve") |
|
fig, ax = plt.subplots(figsize=(10, 4)) |
|
ax.plot(performance.index, performance['Equity'], label="Total Equity") |
|
ax.set_xlabel("Date") |
|
ax.set_ylabel("Equity ($)") |
|
ax.legend() |
|
st.pyplot(fig) |
|
|
|
st.subheader("SPY Price with Buy/Sell Signals") |
|
fig2, ax2 = plt.subplots(figsize=(10, 4)) |
|
ax2.plot(data.index, data['Close'], label="SPY Close Price", color='black') |
|
ax2.scatter(data.index[data['Buy']], data['Close'][data['Buy']], marker="^", color="green", label="Buy Signal", s=100) |
|
ax2.scatter(data.index[data['Sell']], data['Close'][data['Sell']], marker="v", color="red", label="Sell Signal", s=100) |
|
ax2.set_xlabel("Date") |
|
ax2.set_ylabel("Price ($)") |
|
ax2.legend() |
|
st.pyplot(fig2) |
|
|
|
|
|
final_equity = equity_curve[-1] |
|
return_pct = ((final_equity - initial_capital) / initial_capital) * 100 |
|
st.subheader("Strategy Performance Metrics") |
|
st.write(f"**Initial Capital:** ${initial_capital:,.2f}") |
|
st.write(f"**Final Equity:** ${final_equity:,.2f}") |
|
st.write(f"**Return:** {return_pct:.2f}%") |
|
|
|
st.markdown(""" |
|
*This extended demo is for educational purposes only and does not constitute financial advice. Always test your strategies extensively before trading with real money.* |
|
""") |