Market / app.py
mgbam's picture
Update app.py
032a6d6 verified
raw
history blame
8.56 kB
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'])
# Convert outputs to 1-dimensional lists
macd_values = np.array(macd_indicator.macd()).flatten().tolist()
macd_signal_values = np.array(macd_indicator.macd_signal()).flatten().tolist()
# Create the Series using the lists
data['MACD'] = pd.Series(macd_values, index=data.index)
data['MACD_signal'] = pd.Series(macd_signal_values, 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.*
""")