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.*
""")