Spaces:
Sleeping
Sleeping
Stefan
commited on
Commit
·
6937a88
1
Parent(s):
9201b91
Add aplication files
Browse files- .DS_Store +0 -0
- Dockerfile +18 -0
- app/.DS_Store +0 -0
- app/__init__.py +0 -0
- app/data-formatted.csv +0 -0
- app/models.py +0 -0
- app/routes.py +262 -0
- main.py +26 -0
- requirements.txt +28 -0
.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
Dockerfile
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use the official Python image
|
2 |
+
FROM python:3.11-slim
|
3 |
+
|
4 |
+
# Set the working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy requirements and install dependencies
|
8 |
+
COPY requirements.txt .
|
9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
10 |
+
|
11 |
+
# Copy the rest of the application
|
12 |
+
COPY . .
|
13 |
+
|
14 |
+
# Expose the port FastAPI will run on
|
15 |
+
EXPOSE 7860
|
16 |
+
|
17 |
+
# Run the FastAPI app with Uvicorn
|
18 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
app/.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
app/__init__.py
ADDED
File without changes
|
app/data-formatted.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
app/models.py
ADDED
File without changes
|
app/routes.py
ADDED
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from collections import Counter
|
2 |
+
from fastapi import APIRouter
|
3 |
+
import pandas as pd
|
4 |
+
import numpy as np
|
5 |
+
import requests
|
6 |
+
from bs4 import BeautifulSoup
|
7 |
+
import os
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
data = pd.read_csv('app/data-formatted.csv')
|
11 |
+
HF_API_KEY = os.getenv("HF_API_KEY")
|
12 |
+
|
13 |
+
|
14 |
+
def getBerzaNews(symbol):
|
15 |
+
url = f'https://www.mse.mk/en/symbol/{symbol}'
|
16 |
+
response = requests.get(url)
|
17 |
+
content = BeautifulSoup(response.text, 'html.parser')
|
18 |
+
|
19 |
+
# finding links to news
|
20 |
+
aElements = content.find_all('a', href=True)
|
21 |
+
newsLinks = [link['href'] for link in aElements if link['href'].startswith('/en/news')]
|
22 |
+
|
23 |
+
news = []
|
24 |
+
for link in newsLinks:
|
25 |
+
response = requests.get(f'https://www.mse.mk{link}')
|
26 |
+
content = BeautifulSoup(response.text, 'html.parser')
|
27 |
+
try:
|
28 |
+
# print(content.find(id='content').text)
|
29 |
+
# print('-----------------------------------')
|
30 |
+
news.append(content.find(id='content').text)
|
31 |
+
except Exception as e:
|
32 |
+
continue
|
33 |
+
|
34 |
+
return news
|
35 |
+
|
36 |
+
|
37 |
+
|
38 |
+
def analyzeSentiment(symbol):
|
39 |
+
API_URL = "https://api-inference.huggingface.co/models/mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis"
|
40 |
+
headers = {"Authorization": f"Bearer {HF_API_KEY}"}
|
41 |
+
|
42 |
+
def query(text):
|
43 |
+
payload = {"inputs": text}
|
44 |
+
response = requests.post(API_URL, headers=headers, json=payload)
|
45 |
+
if response.status_code != 200:
|
46 |
+
print(f"Error: {response.status_code}, {response.text}")
|
47 |
+
return None
|
48 |
+
return response.json()
|
49 |
+
|
50 |
+
def getMax(result):
|
51 |
+
# Ensure the response has the expected structure
|
52 |
+
if not result or not isinstance(result, list):
|
53 |
+
return "neutral" # Default to neutral if the response is invalid
|
54 |
+
|
55 |
+
result = result[0] if isinstance(result[0], list) else result
|
56 |
+
max_label = max(result, key=lambda x: x['score'])['label']
|
57 |
+
return max_label
|
58 |
+
|
59 |
+
# Fetch news articles
|
60 |
+
texts = getBerzaNews(symbol)
|
61 |
+
if not texts:
|
62 |
+
return 'No news for ' + symbol
|
63 |
+
|
64 |
+
# Collect sentiment labels
|
65 |
+
sentiment_labels = []
|
66 |
+
for text in texts:
|
67 |
+
try:
|
68 |
+
result = query(text) # Query the Hugging Face API
|
69 |
+
if result:
|
70 |
+
sentiment_label = getMax(result) # Get the label
|
71 |
+
sentiment_labels.append(sentiment_label)
|
72 |
+
except Exception as e:
|
73 |
+
print(f"Error processing text: {e}")
|
74 |
+
continue
|
75 |
+
|
76 |
+
if not sentiment_labels:
|
77 |
+
return 'No news for ' + symbol
|
78 |
+
|
79 |
+
# Count occurrences of each sentiment
|
80 |
+
sentiment_counts = Counter(sentiment_labels)
|
81 |
+
|
82 |
+
# Find the sentiment with the most occurrences
|
83 |
+
most_common = sentiment_counts.most_common()
|
84 |
+
|
85 |
+
if len(most_common) == 0:
|
86 |
+
return "neutral" # Default to neutral if no sentiments found
|
87 |
+
|
88 |
+
# Check for ties
|
89 |
+
max_count = most_common[0][1]
|
90 |
+
top_sentiments = [sentiment for sentiment, count in most_common if count == max_count]
|
91 |
+
|
92 |
+
if len(top_sentiments) > 1:
|
93 |
+
return "neutral" # Return neutral in case of ties
|
94 |
+
return top_sentiments[0]
|
95 |
+
|
96 |
+
def predict_future_price(input_data):
|
97 |
+
input_data = input_data.drop(columns=['DATE'])
|
98 |
+
data_to_dictionary = input_data.to_dict(orient='list')
|
99 |
+
|
100 |
+
url = 'https://stefan155-das-lstm-model-api.hf.space/predict/'
|
101 |
+
|
102 |
+
payload = {
|
103 |
+
"input_data": data_to_dictionary,
|
104 |
+
}
|
105 |
+
|
106 |
+
response = requests.post(url, json=payload)
|
107 |
+
return response.json()['prediction']
|
108 |
+
|
109 |
+
|
110 |
+
# Function to resample data for timeframes
|
111 |
+
def resample_data(data, timeframe):
|
112 |
+
data["DATE"] = pd.to_datetime(data["DATE"]) # Ensure DATE is in datetime format
|
113 |
+
data = data.set_index("DATE") # Set DATE as the index
|
114 |
+
|
115 |
+
# Select only numeric columns for resampling
|
116 |
+
numeric_columns = data.select_dtypes(include=["number"]).columns
|
117 |
+
non_numeric_columns = data.select_dtypes(exclude=["number"]).columns
|
118 |
+
|
119 |
+
if timeframe == "1D":
|
120 |
+
resampled_data = data[numeric_columns].asfreq("D").fillna(method="ffill")
|
121 |
+
elif timeframe == "1W":
|
122 |
+
resampled_data = data[numeric_columns].resample("W").mean().fillna(0)
|
123 |
+
elif timeframe == "1M":
|
124 |
+
resampled_data = data[numeric_columns].resample("M").mean().fillna(0)
|
125 |
+
else:
|
126 |
+
raise ValueError("Invalid timeframe. Choose '1D', '1W', or '1M'.")
|
127 |
+
|
128 |
+
# Reset the index to bring DATE back as a column
|
129 |
+
resampled_data = resampled_data.reset_index()
|
130 |
+
|
131 |
+
# Reattach non-numeric columns (e.g., COMPANY)
|
132 |
+
if not non_numeric_columns.empty:
|
133 |
+
non_numeric_data = data[non_numeric_columns].reset_index().drop_duplicates(subset="DATE")
|
134 |
+
resampled_data = resampled_data.merge(non_numeric_data, on="DATE", how="left")
|
135 |
+
|
136 |
+
return resampled_data
|
137 |
+
|
138 |
+
|
139 |
+
# Function to calculate technical indicators
|
140 |
+
def calculate_technical_indicators(data, column="PRICE OF LAST TRANSACTION"):
|
141 |
+
data = data.sort_values(by="DATE").reset_index(drop=True)
|
142 |
+
|
143 |
+
# Oscillators
|
144 |
+
delta = data[column].diff()
|
145 |
+
gain = np.where(delta > 0, delta, 0)
|
146 |
+
loss = np.where(delta < 0, -delta, 0)
|
147 |
+
avg_gain = pd.Series(gain).rolling(window=14).mean()
|
148 |
+
avg_loss = pd.Series(loss).rolling(window=14).mean()
|
149 |
+
rs = avg_gain / avg_loss
|
150 |
+
data["RSI"] = 100 - (100 / (1 + rs))
|
151 |
+
|
152 |
+
# MACD
|
153 |
+
data["EMA12"] = data[column].ewm(span=12, adjust=False).mean()
|
154 |
+
data["EMA26"] = data[column].ewm(span=26, adjust=False).mean()
|
155 |
+
data["MACD"] = data["EMA12"] - data["EMA26"]
|
156 |
+
|
157 |
+
# Stochastic Oscillator
|
158 |
+
data["L14"] = data[column].rolling(window=14).min()
|
159 |
+
data["H14"] = data[column].rolling(window=14).max()
|
160 |
+
data["Stochastic"] = (data[column] - data["L14"]) / (data["H14"] - data["L14"]) * 100
|
161 |
+
|
162 |
+
# Williams %R
|
163 |
+
data["Williams %R"] = (data["H14"] - data[column]) / (data["H14"] - data["L14"]) * -100
|
164 |
+
|
165 |
+
# Rate of Change
|
166 |
+
data["ROC"] = data[column].pct_change(periods=12)
|
167 |
+
|
168 |
+
# Moving Averages
|
169 |
+
for window in [10, 20, 50]:
|
170 |
+
data[f"SMA{window}"] = data[column].rolling(window=window).mean()
|
171 |
+
data[f"EMA{window}"] = data[column].ewm(span=window, adjust=False).mean()
|
172 |
+
|
173 |
+
# Meters
|
174 |
+
oscillators_meter = "STRONG BUY" if data["RSI"].iloc[-1] > 70 else "NEUTRAL"
|
175 |
+
moving_averages_meter = "STRONG BUY" if data[f"SMA10"].iloc[-1] < data[column].iloc[-1] else "SELL"
|
176 |
+
|
177 |
+
return data, oscillators_meter, moving_averages_meter
|
178 |
+
|
179 |
+
|
180 |
+
|
181 |
+
|
182 |
+
@router.get("/stock-data/{ticker}")
|
183 |
+
async def get_stock_data(ticker: str):
|
184 |
+
print(f"Fetching data for ticker: {ticker}")
|
185 |
+
|
186 |
+
stock_data = data[data["COMPANY"] == ticker]
|
187 |
+
if stock_data.empty:
|
188 |
+
print("No data found for the given ticker.")
|
189 |
+
return {"error": "Ticker not found"}
|
190 |
+
|
191 |
+
# Process for each timeframe
|
192 |
+
timeframes = ["1D", "1W", "1M"]
|
193 |
+
timeframe_results = {}
|
194 |
+
for timeframe in timeframes:
|
195 |
+
try:
|
196 |
+
resampled_data = resample_data(stock_data, timeframe)
|
197 |
+
|
198 |
+
# Replace NaN/Inf/-Inf in resampled data
|
199 |
+
resampled_data.replace([np.inf, -np.inf, np.nan], 0, inplace=True)
|
200 |
+
|
201 |
+
indicators_data, oscillators_meter, moving_averages_meter = calculate_technical_indicators(resampled_data)
|
202 |
+
|
203 |
+
# Replace NaN/Inf/-Inf in indicators_data
|
204 |
+
indicators_data.replace([np.inf, -np.inf, np.nan], 0, inplace=True)
|
205 |
+
|
206 |
+
price_prediction = predict_future_price(indicators_data)
|
207 |
+
market_news_evaluation = analyzeSentiment(ticker)
|
208 |
+
# market_news_evaluation = 'neutral'
|
209 |
+
|
210 |
+
|
211 |
+
timeframe_results[timeframe] = {
|
212 |
+
"Price Prediction": price_prediction,
|
213 |
+
"Market News Evaluation": market_news_evaluation,
|
214 |
+
"GraphData": indicators_data[["DATE", "PRICE OF LAST TRANSACTION"]].to_dict(orient="records"),
|
215 |
+
"Oscillators": {
|
216 |
+
"RSI": indicators_data["RSI"].iloc[-1],
|
217 |
+
"MACD": indicators_data["MACD"].iloc[-1],
|
218 |
+
"Stochastic Oscillator": indicators_data["Stochastic"].iloc[-1],
|
219 |
+
"Williams %R": indicators_data["Williams %R"].iloc[-1],
|
220 |
+
"Rate of Change": indicators_data["ROC"].iloc[-1],
|
221 |
+
"METER": oscillators_meter,
|
222 |
+
},
|
223 |
+
"Moving Averages": {
|
224 |
+
"SMA10": indicators_data["SMA10"].iloc[-1],
|
225 |
+
"EMA10": indicators_data["EMA10"].iloc[-1],
|
226 |
+
"SMA20": indicators_data["SMA20"].iloc[-1],
|
227 |
+
"EMA20": indicators_data["EMA20"].iloc[-1],
|
228 |
+
"SMA50": indicators_data["SMA50"].iloc[-1],
|
229 |
+
"METER": moving_averages_meter,
|
230 |
+
},
|
231 |
+
}
|
232 |
+
except Exception as e:
|
233 |
+
print(f"Error processing timeframe {timeframe}: {e}")
|
234 |
+
timeframe_results[timeframe] = {"error": str(e)}
|
235 |
+
|
236 |
+
# Replace NaN/Inf/-Inf in stock_data
|
237 |
+
stock_data.replace([np.inf, -np.inf, np.nan], 0, inplace=True)
|
238 |
+
|
239 |
+
# reverse stock data
|
240 |
+
stock_data = stock_data.iloc[::-1]
|
241 |
+
|
242 |
+
# Construct response
|
243 |
+
response = {
|
244 |
+
"Ticker": ticker,
|
245 |
+
"Company Name": stock_data["COMPANY"].iloc[0],
|
246 |
+
"Current Price": stock_data["AVERAGE PRICE"].iloc[-1],
|
247 |
+
"MAX Price": stock_data["PRICE OF LAST TRANSACTION"].max(),
|
248 |
+
"MIN Price": stock_data["PRICE OF LAST TRANSACTION"].min(),
|
249 |
+
"Volume": stock_data["QUANTITY"].sum() if "QUANTITY" in stock_data.columns else None,
|
250 |
+
"REVENUE": stock_data[
|
251 |
+
"REVENUE IN BEST DENARS"].sum() if "REVENUE IN BEST DENARS" in stock_data.columns else None,
|
252 |
+
"AVERAGE PRICE": stock_data["AVERAGE PRICE"].iloc[-1],
|
253 |
+
"Timeframes": timeframe_results,
|
254 |
+
}
|
255 |
+
|
256 |
+
# Ensure JSON compliance
|
257 |
+
response = {
|
258 |
+
key: (float(value) if isinstance(value, (np.float64, np.int64)) else value)
|
259 |
+
for key, value in response.items()
|
260 |
+
}
|
261 |
+
|
262 |
+
return response
|
main.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from app.routes import router as api_router
|
3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
4 |
+
|
5 |
+
|
6 |
+
# Initialize the FastAPI app
|
7 |
+
app = FastAPI(
|
8 |
+
title="API for the DAS Homework",
|
9 |
+
description="This api is is used to serve for the DAS Homework web application",
|
10 |
+
version="1.0.0"
|
11 |
+
)
|
12 |
+
|
13 |
+
app.add_middleware(
|
14 |
+
CORSMiddleware,
|
15 |
+
allow_origins=["https://das-prototype.web.app"],
|
16 |
+
allow_credentials=True,
|
17 |
+
allow_methods=["*"], # Allow all HTTP methods (e.g., GET, POST)
|
18 |
+
allow_headers=["*"], # Allow all headers
|
19 |
+
)
|
20 |
+
|
21 |
+
# Include the API router
|
22 |
+
app.include_router(api_router)
|
23 |
+
|
24 |
+
@app.get("/")
|
25 |
+
async def root():
|
26 |
+
return {"message": "API for the DAS Homework"}
|
requirements.txt
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
absl-py==2.1.0
|
2 |
+
annotated-types==0.7.0
|
3 |
+
anyio==4.8.0
|
4 |
+
beautifulsoup4==4.12.3
|
5 |
+
certifi==2024.12.14
|
6 |
+
charset-normalizer==3.4.1
|
7 |
+
click==8.1.8
|
8 |
+
fastapi==0.115.6
|
9 |
+
h11==0.14.0
|
10 |
+
idna==3.10
|
11 |
+
markdown-it-py==3.0.0
|
12 |
+
mdurl==0.1.2
|
13 |
+
numpy==2.0.2
|
14 |
+
packaging==24.2
|
15 |
+
pandas==2.2.3
|
16 |
+
pydantic==2.10.5
|
17 |
+
pydantic_core==2.27.2
|
18 |
+
python-dateutil==2.9.0.post0
|
19 |
+
pytz==2024.2
|
20 |
+
requests==2.32.3
|
21 |
+
six==1.17.0
|
22 |
+
sniffio==1.3.1
|
23 |
+
soupsieve==2.6
|
24 |
+
starlette==0.41.3
|
25 |
+
typing_extensions==4.12.2
|
26 |
+
tzdata==2024.2
|
27 |
+
urllib3==2.3.0
|
28 |
+
uvicorn==0.34.0
|