File size: 8,434 Bytes
f023109 a215027 f023109 a215027 f023109 a215027 f023109 a215027 f023109 6439442 |
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 198 |
import streamlit as st
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from ta.trend import MACD
from ta.momentum import RSIIndicator
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")
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!")
# Calculate technical indicators: MACD and RSI
macd_indicator = MACD(close=data['Close'])
# Squeeze the returned values to ensure a 1-dimensional array
data['MACD'] = pd.Series(macd_indicator.macd().squeeze(), index=data.index)
data['MACD_signal'] = pd.Series(macd_indicator.macd_signal().squeeze(), index=data.index)
rsi_indicator = RSIIndicator(close=data['Close'], window=14)
data['RSI'] = rsi_indicator.rsi()
# Initialize signal flags for plotting
data['Buy'] = False
data['Sell'] = False
# Backtesting parameters
initial_capital = 100000
cash = initial_capital
equity_curve = []
# To enforce the buy cooldown, track the last buy date.
last_buy_date = None
# List to track open positions; each position is a dictionary with details.
open_positions = [] # keys: entry_date, entry_price, shares, allocated, highest, trailing_stop
# Lists to store completed trades for analysis
completed_trades = []
# Backtesting simulation loop
for i in range(1, len(data)):
today = data.index[i]
price = data['Close'].iloc[i]
rsi_today = data['RSI'].iloc[i]
# --- Check for a buy signal ---
# Signal: MACD crossover (yesterday below signal, today above signal) and RSI below 50.
macd_today = data['MACD'].iloc[i]
signal_today = data['MACD_signal'].iloc[i]
macd_yesterday = data['MACD'].iloc[i - 1]
signal_yesterday = data['MACD_signal'].iloc[i - 1]
buy_condition = (macd_yesterday < signal_yesterday) and (macd_today > signal_today) and (rsi_today < 50)
# Enforce cooldown: if a buy occurred recently, skip.
if last_buy_date is not None and (today - last_buy_date).days < min_days_between_buys:
buy_condition = False
if buy_condition:
allocation = cash * buy_fraction
if allocation > 0:
shares_bought = allocation / price
cash -= allocation
last_buy_date = today
# Initialize the open position with its own trailing stop.
position = {
"entry_date": today,
"entry_price": price,
"allocated": allocation,
"shares": shares_bought,
"highest": price, # track highest price achieved for this position
"trailing_stop": price * (1 - trailing_stop_pct)
}
open_positions.append(position)
data.at[today, 'Buy'] = True
st.write(f"Buy: {today.date()} | Price: {price:.2f} | Shares: {shares_bought:.2f}")
# --- Update open positions for trailing stops and partial sell targets ---
positions_to_remove = []
for idx, pos in enumerate(open_positions):
# Update the highest price if the current price is higher.
if price > pos["highest"]:
pos["highest"] = price
# Update trailing stop: trailing stop is highest price * (1 - trailing_stop_pct)
pos["trailing_stop"] = pos["highest"] * (1 - trailing_stop_pct)
# Check for partial sell condition:
target_price = pos["entry_price"] * target_multiplier
if price >= target_price and rsi_today > 50:
# Sell a fraction of this position.
shares_to_sell = pos["shares"] * sell_fraction
sell_value = shares_to_sell * price
cash += sell_value
pos["allocated"] -= shares_to_sell * pos["entry_price"]
pos["shares"] -= shares_to_sell
data.at[today, 'Sell'] = True
st.write(f"Partial Sell: {today.date()} | Price: {price:.2f} | Shares Sold: {shares_to_sell:.2f}")
# If the position is nearly closed, mark it for complete removal.
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 to next position without checking trailing stop.
continue
# Check trailing stop: if current price falls below the trailing stop, sell the entire position.
if price < pos["trailing_stop"]:
sell_value = pos["shares"] * price
cash += sell_value
st.write(f"Trailing Stop Hit: {today.date()} | Price: {price:.2f} | Shares Sold: {pos['shares']:.2f}")
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)
# Remove positions that have been fully closed (reverse sort indices to remove safely)
for idx in sorted(positions_to_remove, reverse=True):
del open_positions[idx]
# Calculate the current value of all open positions.
position_value = sum([pos["shares"] * price for pos in open_positions])
total_equity = cash + position_value
equity_curve.append(total_equity)
# Build performance DataFrame for visualization.
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)
st.subheader("Strategy Performance Metrics")
final_equity = equity_curve[-1]
return_pct = ((final_equity - initial_capital) / initial_capital) * 100
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.*
""") |