Add application file
Browse files
app.py
ADDED
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import yfinance as yf
|
3 |
+
import pandas as pd
|
4 |
+
import numpy as np
|
5 |
+
import matplotlib.pyplot as plt
|
6 |
+
from ta.trend import MACD
|
7 |
+
from ta.momentum import RSIIndicator
|
8 |
+
from datetime import timedelta
|
9 |
+
|
10 |
+
st.title("Extended MACD-RSI Combo Strategy for SPY")
|
11 |
+
st.markdown("""
|
12 |
+
This app demonstrates an extended MACD-RSI based trading strategy on SPY with the following features:
|
13 |
+
- **Multiple Simultaneous Positions:** Each buy signal creates a new position.
|
14 |
+
- **Dynamic Trailing Stop:** Each open position is updated with a trailing stop.
|
15 |
+
- **Configurable Parameters:** Adjust strategy parameters via the sidebar.
|
16 |
+
- **Buy Rule:**
|
17 |
+
Buy 15% of available cash when:
|
18 |
+
- The MACD line crosses above its signal line.
|
19 |
+
- RSI is below 50.
|
20 |
+
- No buy has been executed in the last 2 days.
|
21 |
+
- **Sell Rule:**
|
22 |
+
For each position:
|
23 |
+
- **Partial Sell:** Sell 40% of the position when the price reaches 1.08× the entry price and RSI is above 50.
|
24 |
+
- **Trailing Stop Exit:** If the price falls below the position’s dynamic trailing stop, sell the entire position.
|
25 |
+
""")
|
26 |
+
|
27 |
+
st.sidebar.header("Strategy Parameters")
|
28 |
+
buy_fraction = st.sidebar.slider("Buy Fraction (of available cash)", 0.05, 0.50, 0.15, 0.05)
|
29 |
+
sell_fraction = st.sidebar.slider("Partial Sell Fraction", 0.10, 0.90, 0.40, 0.05)
|
30 |
+
target_multiplier = st.sidebar.slider("Target Multiplier", 1.01, 1.20, 1.08, 0.01)
|
31 |
+
trailing_stop_pct = st.sidebar.slider("Trailing Stop (%)", 0.01, 0.20, 0.08, 0.01)
|
32 |
+
min_days_between_buys = st.sidebar.number_input("Minimum Days Between Buys", min_value=1, max_value=10, value=2)
|
33 |
+
|
34 |
+
@st.cache_data
|
35 |
+
def load_data(ticker, period="1y"):
|
36 |
+
data = yf.download(ticker, period=period)
|
37 |
+
data.dropna(inplace=True)
|
38 |
+
return data
|
39 |
+
|
40 |
+
data_load_state = st.text("Loading SPY data...")
|
41 |
+
data = load_data("SPY", period="1y")
|
42 |
+
data_load_state.text("Loading SPY data...done!")
|
43 |
+
|
44 |
+
# Calculate technical indicators: MACD and RSI
|
45 |
+
macd_indicator = MACD(close=data['Close'])
|
46 |
+
data['MACD'] = macd_indicator.macd()
|
47 |
+
data['MACD_signal'] = macd_indicator.macd_signal()
|
48 |
+
|
49 |
+
rsi_indicator = RSIIndicator(close=data['Close'], window=14)
|
50 |
+
data['RSI'] = rsi_indicator.rsi()
|
51 |
+
|
52 |
+
# Initialize signal flags for plotting
|
53 |
+
data['Buy'] = False
|
54 |
+
data['Sell'] = False
|
55 |
+
|
56 |
+
# Backtesting parameters
|
57 |
+
initial_capital = 100000
|
58 |
+
cash = initial_capital
|
59 |
+
equity_curve = []
|
60 |
+
|
61 |
+
# To enforce the buy cooldown, track the last buy date.
|
62 |
+
last_buy_date = None
|
63 |
+
|
64 |
+
# List to track open positions; each position is a dictionary with details.
|
65 |
+
open_positions = [] # keys: entry_date, entry_price, shares, allocated, highest, trailing_stop
|
66 |
+
|
67 |
+
# Lists to store completed trades for analysis
|
68 |
+
completed_trades = []
|
69 |
+
|
70 |
+
# Backtesting simulation loop
|
71 |
+
for i in range(1, len(data)):
|
72 |
+
today = data.index[i]
|
73 |
+
price = data['Close'].iloc[i]
|
74 |
+
rsi_today = data['RSI'].iloc[i]
|
75 |
+
|
76 |
+
# --- Check for a buy signal ---
|
77 |
+
# Signal: MACD crossover (yesterday below signal, today above signal) and RSI below 50.
|
78 |
+
macd_today = data['MACD'].iloc[i]
|
79 |
+
signal_today = data['MACD_signal'].iloc[i]
|
80 |
+
macd_yesterday = data['MACD'].iloc[i - 1]
|
81 |
+
signal_yesterday = data['MACD_signal'].iloc[i - 1]
|
82 |
+
|
83 |
+
buy_condition = (macd_yesterday < signal_yesterday) and (macd_today > signal_today) and (rsi_today < 50)
|
84 |
+
|
85 |
+
# Enforce cooldown: if a buy occurred recently, skip.
|
86 |
+
if last_buy_date is not None and (today - last_buy_date).days < min_days_between_buys:
|
87 |
+
buy_condition = False
|
88 |
+
|
89 |
+
if buy_condition:
|
90 |
+
allocation = cash * buy_fraction
|
91 |
+
if allocation > 0:
|
92 |
+
shares_bought = allocation / price
|
93 |
+
cash -= allocation
|
94 |
+
last_buy_date = today
|
95 |
+
# Initialize the open position with its own trailing stop.
|
96 |
+
position = {
|
97 |
+
"entry_date": today,
|
98 |
+
"entry_price": price,
|
99 |
+
"allocated": allocation,
|
100 |
+
"shares": shares_bought,
|
101 |
+
"highest": price, # track highest price achieved for this position
|
102 |
+
"trailing_stop": price * (1 - trailing_stop_pct)
|
103 |
+
}
|
104 |
+
open_positions.append(position)
|
105 |
+
data.at[today, 'Buy'] = True
|
106 |
+
st.write(f"Buy: {today.date()} | Price: {price:.2f} | Shares: {shares_bought:.2f}")
|
107 |
+
|
108 |
+
# --- Update open positions for trailing stops and partial sell targets ---
|
109 |
+
positions_to_remove = []
|
110 |
+
for idx, pos in enumerate(open_positions):
|
111 |
+
# Update the highest price if the current price is higher.
|
112 |
+
if price > pos["highest"]:
|
113 |
+
pos["highest"] = price
|
114 |
+
# Update trailing stop: trailing stop is highest price * (1 - trailing_stop_pct)
|
115 |
+
pos["trailing_stop"] = pos["highest"] * (1 - trailing_stop_pct)
|
116 |
+
|
117 |
+
# Check for partial sell condition:
|
118 |
+
target_price = pos["entry_price"] * target_multiplier
|
119 |
+
if price >= target_price and rsi_today > 50:
|
120 |
+
# Sell a fraction of this position.
|
121 |
+
shares_to_sell = pos["shares"] * sell_fraction
|
122 |
+
sell_value = shares_to_sell * price
|
123 |
+
cash += sell_value
|
124 |
+
pos["allocated"] -= shares_to_sell * pos["entry_price"]
|
125 |
+
pos["shares"] -= shares_to_sell
|
126 |
+
data.at[today, 'Sell'] = True
|
127 |
+
st.write(f"Partial Sell: {today.date()} | Price: {price:.2f} | Shares Sold: {shares_to_sell:.2f}")
|
128 |
+
# If the position is nearly closed, mark it for complete removal.
|
129 |
+
if pos["shares"] < 0.001:
|
130 |
+
completed_trades.append({
|
131 |
+
"entry_date": pos["entry_date"],
|
132 |
+
"exit_date": today,
|
133 |
+
"entry_price": pos["entry_price"],
|
134 |
+
"exit_price": price,
|
135 |
+
"allocated": pos["allocated"]
|
136 |
+
})
|
137 |
+
positions_to_remove.append(idx)
|
138 |
+
# Continue to next position without checking trailing stop.
|
139 |
+
continue
|
140 |
+
|
141 |
+
# Check trailing stop: if current price falls below the trailing stop, sell the entire position.
|
142 |
+
if price < pos["trailing_stop"]:
|
143 |
+
sell_value = pos["shares"] * price
|
144 |
+
cash += sell_value
|
145 |
+
st.write(f"Trailing Stop Hit: {today.date()} | Price: {price:.2f} | Shares Sold: {pos['shares']:.2f}")
|
146 |
+
completed_trades.append({
|
147 |
+
"entry_date": pos["entry_date"],
|
148 |
+
"exit_date": today,
|
149 |
+
"entry_price": pos["entry_price"],
|
150 |
+
"exit_price": price,
|
151 |
+
"allocated": pos["allocated"]
|
152 |
+
})
|
153 |
+
positions_to_remove.append(idx)
|
154 |
+
|
155 |
+
# Remove positions that have been fully closed (reverse sort indices to remove safely)
|
156 |
+
for idx in sorted(positions_to_remove, reverse=True):
|
157 |
+
del open_positions[idx]
|
158 |
+
|
159 |
+
# Calculate the current value of all open positions.
|
160 |
+
position_value = sum([pos["shares"] * price for pos in open_positions])
|
161 |
+
total_equity = cash + position_value
|
162 |
+
equity_curve.append(total_equity)
|
163 |
+
|
164 |
+
# Build performance DataFrame for visualization.
|
165 |
+
performance = pd.DataFrame({
|
166 |
+
'Date': data.index[1:len(equity_curve)+1],
|
167 |
+
'Equity': equity_curve
|
168 |
+
}).set_index('Date')
|
169 |
+
|
170 |
+
st.subheader("Equity Curve")
|
171 |
+
fig, ax = plt.subplots(figsize=(10, 4))
|
172 |
+
ax.plot(performance.index, performance['Equity'], label="Total Equity")
|
173 |
+
ax.set_xlabel("Date")
|
174 |
+
ax.set_ylabel("Equity ($)")
|
175 |
+
ax.legend()
|
176 |
+
st.pyplot(fig)
|
177 |
+
|
178 |
+
st.subheader("SPY Price with Buy/Sell Signals")
|
179 |
+
fig2, ax2 = plt.subplots(figsize=(10, 4))
|
180 |
+
ax2.plot(data.index, data['Close'], label="SPY Close Price", color='black')
|
181 |
+
ax2.scatter(data.index[data['Buy']], data['Close'][data['Buy']], marker="^", color="green", label="Buy Signal", s=100)
|
182 |
+
ax2.scatter(data.index[data['Sell']], data['Close'][data['Sell']], marker="v", color="red", label="Sell Signal", s=100)
|
183 |
+
ax2.set_xlabel("Date")
|
184 |
+
ax2.set_ylabel("Price ($)")
|
185 |
+
ax2.legend()
|
186 |
+
st.pyplot(fig2)
|
187 |
+
|
188 |
+
st.subheader("Strategy Performance Metrics")
|
189 |
+
final_equity = equity_curve[-1]
|
190 |
+
return_pct = ((final_equity - initial_capital) / initial_capital) * 100
|
191 |
+
st.write(f"**Initial Capital:** ${initial_capital:,.2f}")
|
192 |
+
st.write(f"**Final Equity:** ${final_equity:,.2f}")
|
193 |
+
st.write(f"**Return:** {return_pct:.2f}%")
|
194 |
+
|
195 |
+
st.markdown("""
|
196 |
+
*This extended demo is for educational purposes only and does not constitute financial advice. Always test your strategies extensively before trading with real money.*
|
197 |
+
""")
|