Spaces:
Running
Running
Main Commit
Browse files- app.py +122 -0
- pattern_finder.py +176 -0
- requirements.txt +7 -0
app.py
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import pandas as pd
|
3 |
+
import yfinance as yf
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
import requests
|
6 |
+
from bs4 import BeautifulSoup
|
7 |
+
from pattern_finder import score_downward_trend, score_candle, calculate_risk_reward
|
8 |
+
import urllib3
|
9 |
+
from datetime import datetime, timedelta
|
10 |
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
11 |
+
|
12 |
+
|
13 |
+
def load_sp500_tickers():
|
14 |
+
"""Load S&P 500 tickers from Wikipedia."""
|
15 |
+
url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
|
16 |
+
response = requests.get(url, verify=False)
|
17 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
18 |
+
table = soup.find('table', {'id': 'constituents'})
|
19 |
+
tickers = []
|
20 |
+
if table:
|
21 |
+
for row in table.find_all('tr')[1:]:
|
22 |
+
cells = row.find_all('td')
|
23 |
+
if cells:
|
24 |
+
ticker = cells[0].text.strip()
|
25 |
+
tickers.append(ticker)
|
26 |
+
return tickers
|
27 |
+
|
28 |
+
|
29 |
+
|
30 |
+
def load_data(ticker):
|
31 |
+
"""Load stock data using yfinance."""
|
32 |
+
end_date = datetime.today()
|
33 |
+
start_date = end_date - timedelta(days=365) # Get 1 year of data
|
34 |
+
data = yf.download(ticker, start=start_date, end=end_date)
|
35 |
+
return data
|
36 |
+
|
37 |
+
|
38 |
+
|
39 |
+
def calculate_sma(data, window):
|
40 |
+
"""Calculate the Simple Moving Average (SMA) for a given window."""
|
41 |
+
return data['Close'].rolling(window=window).mean()
|
42 |
+
|
43 |
+
def calculate_ema(data, window):
|
44 |
+
"""Calculate the Exponential Moving Average (EMA) for a given window."""
|
45 |
+
return data['Close'].ewm(span=window, adjust=False).mean()
|
46 |
+
|
47 |
+
def average_downtrend(data, method, window=4):
|
48 |
+
"""Calculate the average difference between consecutive prices for the last 'window' candles."""
|
49 |
+
if len(data) < window:
|
50 |
+
return 0.0
|
51 |
+
price_diffs = data[method].diff().iloc[-window:]
|
52 |
+
avg_diff = price_diffs.mean()
|
53 |
+
return avg_diff if avg_diff < 0 else 0.0
|
54 |
+
|
55 |
+
|
56 |
+
def score_today_candle(data, window=4):
|
57 |
+
if len(data) < window + 1:
|
58 |
+
return 0
|
59 |
+
|
60 |
+
trend_score = score_downward_trend(data.iloc[-window:], window=window)
|
61 |
+
candle_score = score_candle(data.iloc[-1], data, len(data) - 1)
|
62 |
+
risk_reward = calculate_risk_reward(data, len(data) - 1)
|
63 |
+
|
64 |
+
# Combine scores (you can adjust the weights as needed)
|
65 |
+
total_score = trend_score + candle_score + (risk_reward * 10)
|
66 |
+
|
67 |
+
return total_score
|
68 |
+
|
69 |
+
def scan_sp500(top_n=25, progress=gr.Progress()):
|
70 |
+
tickers = load_sp500_tickers()
|
71 |
+
scores = []
|
72 |
+
tickers.append("QQQ")
|
73 |
+
|
74 |
+
for i, ticker in enumerate(progress.tqdm(tickers)):
|
75 |
+
data = load_data(ticker)
|
76 |
+
if not data.empty:
|
77 |
+
score = score_today_candle(data)
|
78 |
+
if score > 0:
|
79 |
+
scores.append((ticker, score))
|
80 |
+
|
81 |
+
scores = sorted(scores, key=lambda x: x[1], reverse=True)
|
82 |
+
return scores[:top_n]
|
83 |
+
|
84 |
+
def next_business_day(date):
|
85 |
+
next_day = date + timedelta(days=1)
|
86 |
+
while next_day.weekday() >= 5: # 5 = Saturday, 6 = Sunday
|
87 |
+
next_day += timedelta(days=1)
|
88 |
+
return next_day
|
89 |
+
|
90 |
+
|
91 |
+
|
92 |
+
def gradio_scan_sp500(top_n, progress=gr.Progress()):
|
93 |
+
progress(0, desc="Downloading Data")
|
94 |
+
tickers = load_sp500_tickers()
|
95 |
+
tickers.append("QQQ")
|
96 |
+
|
97 |
+
progress(0.3, desc="Running Scanner")
|
98 |
+
results = scan_sp500(top_n, progress)
|
99 |
+
|
100 |
+
# Get the last date of the data and find the next business day
|
101 |
+
last_data = load_data(results[0][0]) # Load data for the first ticker in results
|
102 |
+
last_date = last_data.index[-1].date()
|
103 |
+
next_market_day = next_business_day(last_date)
|
104 |
+
date_created = next_market_day.strftime("%Y-%m-%d")
|
105 |
+
|
106 |
+
output = f"Scan Results for Market Open on: {date_created}\n\n"
|
107 |
+
output += "Top {} stocks based on pattern finder score:\n\n".format(top_n)
|
108 |
+
for ticker, score in results:
|
109 |
+
output += "{}: Total Score = {:.2f}\n".format(ticker, score)
|
110 |
+
return output
|
111 |
+
|
112 |
+
iface = gr.Interface(
|
113 |
+
fn=gradio_scan_sp500,
|
114 |
+
inputs=gr.Slider(minimum=1, maximum=100, step=1, label="Number of top stocks to display", value=25),
|
115 |
+
outputs="text",
|
116 |
+
title="S&P 500 Stock Scanner",
|
117 |
+
description="Scan S&P 500 stocks and display top N stocks based on today's candle score.",
|
118 |
+
allow_flagging="never",
|
119 |
+
)
|
120 |
+
|
121 |
+
if __name__ == "__main__":
|
122 |
+
iface.launch()
|
pattern_finder.py
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
def load_data(ticker):
|
5 |
+
try:
|
6 |
+
# Load data from CSV
|
7 |
+
data = pd.read_csv(f'tickers/{ticker}.csv', index_col="Date", parse_dates=['Date'])
|
8 |
+
data.sort_index(inplace=True) # Ensure data is sorted by date
|
9 |
+
return data
|
10 |
+
except FileNotFoundError:
|
11 |
+
print(f"Data for {ticker} not found.")
|
12 |
+
return None
|
13 |
+
|
14 |
+
def sma(data, period):
|
15 |
+
return data['Close'].rolling(window=period).mean()
|
16 |
+
|
17 |
+
def ema(data, period):
|
18 |
+
return data['Close'].ewm(span=period, adjust=False).mean()
|
19 |
+
|
20 |
+
def score_downward_trend(data, window=4):
|
21 |
+
"""
|
22 |
+
Score the downward trend based on price action and volume.
|
23 |
+
"""
|
24 |
+
if len(data) < window:
|
25 |
+
return 0 # Not enough data
|
26 |
+
|
27 |
+
score = 0
|
28 |
+
for j in range(1, window):
|
29 |
+
if data['High'].iloc[j] < data['High'].iloc[j-1]:
|
30 |
+
score += 1 # Increment score for each lower high
|
31 |
+
if data['Close'].iloc[j] < data['Close'].iloc[j-1]:
|
32 |
+
score += 1 # Increment score for each lower close
|
33 |
+
if data['Volume'].iloc[j] > data['Volume'].iloc[j-1]:
|
34 |
+
score += 0.5 # Increment score for increasing volume during downtrend
|
35 |
+
|
36 |
+
return score
|
37 |
+
|
38 |
+
def score_candle(candle, data, i):
|
39 |
+
"""
|
40 |
+
Score the candle based on its pattern, volume, and position relative to moving averages.
|
41 |
+
"""
|
42 |
+
open_price, close_price, low_price, high_price = candle['Open'], candle['Close'], candle['Low'], candle['High']
|
43 |
+
prev_candle = data.iloc[i-1]
|
44 |
+
|
45 |
+
score = 0
|
46 |
+
body = abs(close_price - open_price)
|
47 |
+
bottom_wick_length = min(open_price, close_price) - low_price
|
48 |
+
top_wick_length = high_price - max(open_price, close_price)
|
49 |
+
|
50 |
+
# Add 16 points if candle is green and there's a significant gap
|
51 |
+
if close_price > open_price and low_price > prev_candle['High']:
|
52 |
+
score += 16
|
53 |
+
|
54 |
+
# Rest of the existing scoring logic
|
55 |
+
if bottom_wick_length > 2 * body:
|
56 |
+
score += 10
|
57 |
+
if abs(open_price - close_price) < (0.1 * (high_price - low_price)):
|
58 |
+
score += 15
|
59 |
+
if bottom_wick_length > top_wick_length:
|
60 |
+
score += 8
|
61 |
+
|
62 |
+
# Volume analysis
|
63 |
+
if candle['Volume'] > data['Volume'].rolling(window=20).mean().iloc[i]:
|
64 |
+
score += 5
|
65 |
+
|
66 |
+
# Moving average analysis
|
67 |
+
ema_20 = ema(data.iloc[:i+1], 20).iloc[-1]
|
68 |
+
sma_50 = sma(data.iloc[:i+1], 50).iloc[-1]
|
69 |
+
sma_200 = sma(data.iloc[:i+1], 200).iloc[-1]
|
70 |
+
|
71 |
+
if close_price > ema_20 and open_price < ema_20:
|
72 |
+
score += 5
|
73 |
+
if close_price > sma_50:
|
74 |
+
score += 3
|
75 |
+
if close_price > sma_200:
|
76 |
+
score += 2
|
77 |
+
|
78 |
+
# Momentum indicator
|
79 |
+
rsi = calculate_rsi(data.iloc[:i+1], period=14).iloc[-1]
|
80 |
+
if rsi < 30:
|
81 |
+
score += 5
|
82 |
+
|
83 |
+
penalty = 0
|
84 |
+
conditions_met = 0
|
85 |
+
|
86 |
+
if candle['High'] > prev_candle['High']:
|
87 |
+
conditions_met += 1
|
88 |
+
if candle['Low'] > prev_candle['High']:
|
89 |
+
conditions_met += 1
|
90 |
+
if candle['Close'] > max(prev_candle['Close'], prev_candle['Open']):
|
91 |
+
conditions_met += 1
|
92 |
+
if candle['Open'] > max(prev_candle['Open'], prev_candle['Close']):
|
93 |
+
conditions_met += 1
|
94 |
+
|
95 |
+
current_avg = (candle['Open'] + candle['Close'] + candle['High'] + candle['Low']) / 4
|
96 |
+
prev_avg = (prev_candle['Open'] + prev_candle['Close'] + prev_candle['High'] + prev_candle['Low']) / 4
|
97 |
+
if current_avg > prev_avg:
|
98 |
+
conditions_met += 1
|
99 |
+
|
100 |
+
if conditions_met == 3:
|
101 |
+
penalty = -10
|
102 |
+
elif conditions_met == 4:
|
103 |
+
penalty = -12
|
104 |
+
elif conditions_met == 5:
|
105 |
+
penalty = -17
|
106 |
+
|
107 |
+
score += penalty
|
108 |
+
|
109 |
+
return score
|
110 |
+
|
111 |
+
def calculate_rsi(data, period=14):
|
112 |
+
delta = data['Close'].diff()
|
113 |
+
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
|
114 |
+
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
|
115 |
+
rs = gain / loss
|
116 |
+
return 100 - (100 / (1 + rs))
|
117 |
+
|
118 |
+
def calculate_risk_reward(data, entry_index, stop_loss_percent=0.02, target_percent=0.06):
|
119 |
+
entry_price = data['Close'].iloc[entry_index]
|
120 |
+
stop_loss = entry_price * (1 - stop_loss_percent)
|
121 |
+
target = entry_price * (1 + target_percent)
|
122 |
+
risk = entry_price - stop_loss
|
123 |
+
reward = target - entry_price
|
124 |
+
return reward / risk
|
125 |
+
|
126 |
+
def find_reversal_patterns(data, window=4, candle_score_threshold=20, trend_score_threshold=5, risk_reward_threshold=2):
|
127 |
+
patterns = []
|
128 |
+
|
129 |
+
for i in range(window, len(data)):
|
130 |
+
trend_score = score_downward_trend(data.iloc[i-window:i], window=window)
|
131 |
+
if trend_score >= trend_score_threshold:
|
132 |
+
candle_score = score_candle(data.iloc[i], data, i)
|
133 |
+
if candle_score >= candle_score_threshold:
|
134 |
+
risk_reward = calculate_risk_reward(data, i)
|
135 |
+
if risk_reward >= risk_reward_threshold:
|
136 |
+
format_date = data.index[i].strftime('%Y-%m-%d')
|
137 |
+
patterns.append((format_date, trend_score, candle_score, risk_reward))
|
138 |
+
|
139 |
+
return patterns
|
140 |
+
|
141 |
+
def back_reversal_finder(data, window=4, candle_score_threshold=20, trend_score_threshold=4.5, risk_reward_threshold=1.5):
|
142 |
+
patterns = []
|
143 |
+
|
144 |
+
for i in range(window, len(data)):
|
145 |
+
trend_score = score_downward_trend(data.iloc[i-window:i], window=window)
|
146 |
+
if trend_score >= trend_score_threshold:
|
147 |
+
candle_score = score_candle(data.iloc[i], data, i)
|
148 |
+
if candle_score >= candle_score_threshold:
|
149 |
+
risk_reward = calculate_risk_reward(data, i)
|
150 |
+
if risk_reward >= risk_reward_threshold:
|
151 |
+
format_date = data.index[i].strftime('%Y-%m-%d')
|
152 |
+
patterns.append(format_date)
|
153 |
+
|
154 |
+
return patterns
|
155 |
+
|
156 |
+
def check_for_reversal_patterns(ticker, window=4, candle_score_threshold=20, trend_score_threshold=5, risk_reward_threshold=2):
|
157 |
+
data = load_data(ticker)
|
158 |
+
if data is None:
|
159 |
+
return
|
160 |
+
|
161 |
+
patterns = find_reversal_patterns(data, window=window, candle_score_threshold=candle_score_threshold,
|
162 |
+
trend_score_threshold=trend_score_threshold, risk_reward_threshold=risk_reward_threshold)
|
163 |
+
|
164 |
+
if patterns:
|
165 |
+
print(f"{ticker}: Potential reversal patterns found:")
|
166 |
+
for date, trend_score, candle_score, risk_reward in patterns:
|
167 |
+
print(f"Date: {date}, Trend Score: {trend_score:.2f}, Candle Score: {candle_score:.2f}, Risk-Reward: {risk_reward:.2f}")
|
168 |
+
else:
|
169 |
+
print(f"{ticker}: No clear reversal patterns detected.")
|
170 |
+
|
171 |
+
def main():
|
172 |
+
ticker = input("Enter Ticker: ").upper()
|
173 |
+
check_for_reversal_patterns(ticker)
|
174 |
+
|
175 |
+
if __name__ == '__main__':
|
176 |
+
main()
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
pandas
|
2 |
+
numpy
|
3 |
+
yfinance
|
4 |
+
gradio
|
5 |
+
requests
|
6 |
+
beautifulsoup4
|
7 |
+
urllib3
|