Spaces:
Sleeping
Sleeping
Smart-Trader-EA commited on
Commit ยท
9333e4d
1
Parent(s): 9df3483
Fix Gradio compatibility issue
Browse files
app.py
CHANGED
|
@@ -5,9 +5,15 @@ import plotly.graph_objects as go
|
|
| 5 |
from prophet import Prophet
|
| 6 |
import os
|
| 7 |
import warnings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
warnings.filterwarnings('ignore')
|
| 9 |
|
| 10 |
-
#
|
|
|
|
| 11 |
os.environ["OMP_NUM_THREADS"] = "1"
|
| 12 |
os.environ["OPENBLAS_NUM_THREADS"] = "1"
|
| 13 |
os.environ["MKL_NUM_THREADS"] = "1"
|
|
@@ -17,24 +23,26 @@ RAW_DATA_DIR = "data/raw"
|
|
| 17 |
PROCESSED_DATA_DIR = "data/processed"
|
| 18 |
os.makedirs(PROCESSED_DATA_DIR, exist_ok=True)
|
| 19 |
|
| 20 |
-
# Predefined trading pairs with expected formats
|
|
|
|
| 21 |
TRADING_PAIRS = {
|
|
|
|
| 22 |
"EURUSD": {
|
| 23 |
-
"description": "Euro to US Dollar Forex Pair",
|
| 24 |
"date_format": "%d.%m.%Y %H:%M:%S.%f %z",
|
| 25 |
"has_timezone": True,
|
| 26 |
"decimal_separator": ".",
|
| 27 |
"required_columns": ["Open", "High", "Low", "Close"]
|
| 28 |
},
|
| 29 |
"BTCUSD": {
|
| 30 |
-
"description": "Bitcoin to US Dollar",
|
| 31 |
"date_format": "%Y-%m-%d %H:%M:%S",
|
| 32 |
"has_timezone": False,
|
| 33 |
"decimal_separator": ".",
|
| 34 |
"required_columns": ["Open", "High", "Low", "Close"]
|
| 35 |
},
|
| 36 |
"AAPL": {
|
| 37 |
-
"description": "Apple Inc. Stock",
|
| 38 |
"date_format": "%Y-%m-%d",
|
| 39 |
"has_timezone": False,
|
| 40 |
"decimal_separator": ".",
|
|
@@ -42,35 +50,44 @@ TRADING_PAIRS = {
|
|
| 42 |
}
|
| 43 |
}
|
| 44 |
|
|
|
|
|
|
|
| 45 |
def preprocess_data_file(raw_file_path, pair_name):
|
| 46 |
"""Preprocess raw data file to standardized format"""
|
| 47 |
print(f"๐ Preprocessing data for {pair_name}...")
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
try:
|
| 50 |
-
#
|
| 51 |
-
config = TRADING_PAIRS.get(pair_name, TRADING_PAIRS["EURUSD"])
|
| 52 |
-
|
| 53 |
-
# Read raw data with proper encoding
|
| 54 |
encodings = ['utf-8', 'latin1', 'ISO-8859-1', 'cp1252']
|
| 55 |
df = None
|
| 56 |
|
| 57 |
for encoding in encodings:
|
| 58 |
try:
|
| 59 |
-
|
|
|
|
| 60 |
print(f"โ
Successfully read {pair_name} data with {encoding} encoding")
|
| 61 |
break
|
| 62 |
except (UnicodeDecodeError, pd.errors.ParserError):
|
| 63 |
continue
|
| 64 |
|
| 65 |
if df is None:
|
| 66 |
-
raise Exception(f"โ Failed to read {pair_name} data with any encoding")
|
| 67 |
|
| 68 |
# Standardize column names (case-insensitive)
|
| 69 |
column_mapping = {}
|
| 70 |
for col in df.columns:
|
| 71 |
-
col_lower = col.lower()
|
| 72 |
|
| 73 |
-
if any(keyword in col_lower for keyword in ['date', 'time', 'timestamp']):
|
| 74 |
column_mapping[col] = 'datetime'
|
| 75 |
elif 'open' in col_lower:
|
| 76 |
column_mapping[col] = 'Open'
|
|
@@ -78,9 +95,9 @@ def preprocess_data_file(raw_file_path, pair_name):
|
|
| 78 |
column_mapping[col] = 'High'
|
| 79 |
elif 'low' in col_lower:
|
| 80 |
column_mapping[col] = 'Low'
|
| 81 |
-
elif 'close' in col_lower:
|
| 82 |
column_mapping[col] = 'Close'
|
| 83 |
-
elif 'volume' in col_lower:
|
| 84 |
column_mapping[col] = 'Volume'
|
| 85 |
|
| 86 |
if column_mapping:
|
|
@@ -89,33 +106,30 @@ def preprocess_data_file(raw_file_path, pair_name):
|
|
| 89 |
|
| 90 |
# Process datetime column
|
| 91 |
datetime_col = None
|
| 92 |
-
for col in ['datetime', '
|
| 93 |
if col in df.columns:
|
| 94 |
datetime_col = col
|
| 95 |
break
|
| 96 |
|
| 97 |
if datetime_col is None:
|
| 98 |
-
raise Exception("โ No datetime column found in data")
|
| 99 |
|
| 100 |
-
# Handle special EURUSD format with GMT
|
| 101 |
if pair_name == "EURUSD" and df[datetime_col].astype(str).str.contains('GMT').any():
|
| 102 |
print("๐ Handling EURUSD special datetime format...")
|
| 103 |
-
|
| 104 |
-
df[datetime_col] = df[datetime_col].str.replace(' GMT', '', regex=False)
|
| 105 |
-
|
| 106 |
-
# Parse with specified format
|
| 107 |
df[datetime_col] = pd.to_datetime(
|
| 108 |
df[datetime_col],
|
| 109 |
-
format=config['date_format'],
|
| 110 |
errors='coerce',
|
| 111 |
utc=True
|
| 112 |
)
|
| 113 |
else:
|
| 114 |
-
#
|
| 115 |
df[datetime_col] = pd.to_datetime(
|
| 116 |
df[datetime_col],
|
| 117 |
errors='coerce',
|
| 118 |
-
utc=config
|
| 119 |
)
|
| 120 |
|
| 121 |
# Remove rows with invalid dates
|
|
@@ -127,13 +141,20 @@ def preprocess_data_file(raw_file_path, pair_name):
|
|
| 127 |
df.set_index(datetime_col, inplace=True)
|
| 128 |
df.sort_index(inplace=True)
|
| 129 |
|
| 130 |
-
# Handle decimal separators
|
| 131 |
if config['decimal_separator'] != '.':
|
|
|
|
| 132 |
for col in ['Open', 'High', 'Low', 'Close', 'Volume']:
|
| 133 |
if col in df.columns:
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
-
# Fill missing values
|
| 137 |
for col in ['Open', 'High', 'Low', 'Close']:
|
| 138 |
if col in df.columns:
|
| 139 |
missing_before = df[col].isna().sum()
|
|
@@ -146,13 +167,15 @@ def preprocess_data_file(raw_file_path, pair_name):
|
|
| 146 |
df = df[~df.index.duplicated(keep='first')]
|
| 147 |
print(f"๐งน Removed {before_count - len(df)} duplicate entries")
|
| 148 |
|
| 149 |
-
#
|
| 150 |
missing_cols = [col for col in config['required_columns'] if col not in df.columns]
|
| 151 |
if missing_cols:
|
| 152 |
-
print(f"โ Missing required columns: {missing_cols}")
|
| 153 |
-
print(f"Available columns: {df.columns.tolist()}")
|
| 154 |
return None
|
| 155 |
|
|
|
|
|
|
|
|
|
|
| 156 |
# Save preprocessed data
|
| 157 |
processed_file = os.path.join(PROCESSED_DATA_DIR, f"{pair_name}_processed.csv")
|
| 158 |
df.to_csv(processed_file)
|
|
@@ -162,44 +185,47 @@ def preprocess_data_file(raw_file_path, pair_name):
|
|
| 162 |
|
| 163 |
except Exception as e:
|
| 164 |
print(f"โ Preprocessing error for {pair_name}: {str(e)}")
|
|
|
|
| 165 |
return None
|
| 166 |
|
| 167 |
def load_available_data():
|
| 168 |
-
"""Load and preprocess all available data files"""
|
|
|
|
| 169 |
available_data = {}
|
| 170 |
|
| 171 |
-
# Check if
|
| 172 |
if not os.path.exists(RAW_DATA_DIR):
|
| 173 |
-
print(f"โ ๏ธ Raw data directory not found: {RAW_DATA_DIR}")
|
| 174 |
-
# Check if data is in root directory instead
|
| 175 |
if os.path.exists("data") and os.path.isdir("data"):
|
| 176 |
-
for
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
| 186 |
if not os.path.exists(RAW_DATA_DIR):
|
| 187 |
-
print(f"โ Still cannot find raw data directory: {RAW_DATA_DIR}")
|
| 188 |
return available_data
|
| 189 |
|
| 190 |
print(f"๐ Scanning for data files in {RAW_DATA_DIR}...")
|
| 191 |
|
| 192 |
-
# Scan for CSV files in raw data directory
|
| 193 |
for filename in os.listdir(RAW_DATA_DIR):
|
| 194 |
if filename.endswith('.csv'):
|
| 195 |
-
# Extract pair name from filename
|
| 196 |
pair_name = filename.split('.')[0].upper()
|
| 197 |
|
| 198 |
-
#
|
|
|
|
| 199 |
if pair_name not in TRADING_PAIRS:
|
| 200 |
TRADING_PAIRS[pair_name] = {
|
| 201 |
-
"description": f"{pair_name} Trading Pair",
|
| 202 |
-
"date_format":
|
| 203 |
"has_timezone": False,
|
| 204 |
"decimal_separator": ".",
|
| 205 |
"required_columns": ["Open", "High", "Low", "Close"]
|
|
@@ -208,19 +234,19 @@ def load_available_data():
|
|
| 208 |
raw_file_path = os.path.join(RAW_DATA_DIR, filename)
|
| 209 |
processed_file_path = os.path.join(PROCESSED_DATA_DIR, f"{pair_name}_processed.csv")
|
| 210 |
|
| 211 |
-
# Check
|
| 212 |
if os.path.exists(processed_file_path):
|
| 213 |
raw_mod_time = os.path.getmtime(raw_file_path)
|
| 214 |
processed_mod_time = os.path.getmtime(processed_file_path)
|
| 215 |
|
| 216 |
if processed_mod_time > raw_mod_time:
|
| 217 |
-
print(f"โ
Using existing preprocessed data for {pair_name}")
|
| 218 |
try:
|
| 219 |
df = pd.read_csv(processed_file_path, index_col=0, parse_dates=True)
|
| 220 |
available_data[pair_name] = df
|
|
|
|
| 221 |
continue
|
| 222 |
except Exception as e:
|
| 223 |
-
print(f"โ ๏ธ Error loading preprocessed file: {str(e)}")
|
| 224 |
|
| 225 |
# Preprocess the file
|
| 226 |
print(f"๐ Processing {pair_name} data...")
|
|
@@ -229,68 +255,75 @@ def load_available_data():
|
|
| 229 |
available_data[pair_name] = df
|
| 230 |
print(f"โ
Successfully loaded {pair_name} with {len(df)} records")
|
| 231 |
else:
|
| 232 |
-
print(f"โ Failed to load {pair_name} data")
|
| 233 |
|
| 234 |
return available_data
|
| 235 |
|
| 236 |
-
#
|
| 237 |
-
print("๐ Initializing data processing system...")
|
| 238 |
-
available_data = load_available_data()
|
| 239 |
-
print(f"๐ Available trading pairs: {list(available_data.keys())}")
|
| 240 |
|
| 241 |
def get_available_pairs():
|
| 242 |
-
"""Get list of available trading pairs with status"""
|
| 243 |
if not available_data:
|
| 244 |
-
return "โ ๏ธ No data files found. Please upload CSV files to the 'data/raw' directory."
|
| 245 |
|
| 246 |
status = "โ
Available trading pairs:\n"
|
| 247 |
for pair in sorted(available_data.keys()):
|
| 248 |
df = available_data[pair]
|
| 249 |
records = len(df)
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
| 252 |
return status
|
| 253 |
|
| 254 |
-
def analyze_trading_pair(pair_name):
|
| 255 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
try:
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
matched_pair = None
|
| 263 |
-
for available_pair in available_data.keys():
|
| 264 |
-
if pair_name.upper() == available_pair.upper():
|
| 265 |
-
matched_pair = available_pair
|
| 266 |
-
break
|
| 267 |
|
| 268 |
-
if matched_pair is None:
|
| 269 |
-
available_pairs = ", ".join(available_data.keys())
|
| 270 |
-
return (
|
| 271 |
-
f"โ Data not available for '{pair_name}'\n"
|
| 272 |
-
f"Available pairs: {available_pairs}\n"
|
| 273 |
-
f"Upload your data to 'data/raw' directory and restart the app",
|
| 274 |
-
None, None, None
|
| 275 |
-
)
|
| 276 |
-
pair_name = matched_pair
|
| 277 |
-
|
| 278 |
-
# Get the data
|
| 279 |
-
hist = available_data[pair_name].copy()
|
| 280 |
-
print(f"๐ Loaded {len(hist)} records for {pair_name}")
|
| 281 |
-
|
| 282 |
# Basic data validation
|
| 283 |
required_cols = ['Open', 'High', 'Low', 'Close']
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
if missing_cols:
|
| 287 |
return (
|
| 288 |
-
f"โ Missing required columns: {', '.join(missing_cols)}\
|
| 289 |
-
|
| 290 |
-
None, None, None
|
| 291 |
)
|
| 292 |
|
| 293 |
-
#
|
| 294 |
fig = go.Figure()
|
| 295 |
|
| 296 |
# Add candlestick
|
|
@@ -303,48 +336,32 @@ def analyze_trading_pair(pair_name):
|
|
| 303 |
name='Price'
|
| 304 |
))
|
| 305 |
|
| 306 |
-
# Add moving averages
|
| 307 |
if len(hist) >= 20:
|
| 308 |
hist['MA20'] = hist['Close'].rolling(window=20, min_periods=1).mean()
|
| 309 |
-
fig.add_trace(go.Scatter(
|
| 310 |
-
x=hist.index,
|
| 311 |
-
y=hist['MA20'],
|
| 312 |
-
mode='lines',
|
| 313 |
-
name='20-period MA',
|
| 314 |
-
line=dict(color='blue', width=1.5)
|
| 315 |
-
))
|
| 316 |
|
| 317 |
if len(hist) >= 50:
|
| 318 |
hist['MA50'] = hist['Close'].rolling(window=50, min_periods=1).mean()
|
| 319 |
-
fig.add_trace(go.Scatter(
|
| 320 |
-
|
| 321 |
-
y=hist['MA50'],
|
| 322 |
-
mode='lines',
|
| 323 |
-
name='50-period MA',
|
| 324 |
-
line=dict(color='orange', width=1.5)
|
| 325 |
-
))
|
| 326 |
-
|
| 327 |
-
# Update layout
|
| 328 |
fig.update_layout(
|
| 329 |
title=f"{pair_name} Price Analysis",
|
| 330 |
xaxis_title="Date",
|
| 331 |
yaxis_title="Price",
|
| 332 |
template="plotly_white",
|
| 333 |
hovermode="x unified",
|
|
|
|
| 334 |
height=500,
|
| 335 |
-
margin=dict(l=50, r=50, t=50, b=50)
|
| 336 |
)
|
| 337 |
|
| 338 |
-
#
|
| 339 |
-
forecast_fig =
|
| 340 |
-
forecast_table =
|
| 341 |
-
forecast_result = ""
|
| 342 |
|
| 343 |
try:
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
# Prepare data for Prophet - USE ONLY RECENT DATA TO AVOID MEMORY ISSUES
|
| 347 |
-
# Take last 365 days (1 year) of data for forecasting
|
| 348 |
prophet_df = hist[['Close']].copy().last('365D').reset_index()
|
| 349 |
prophet_df.columns = ['ds', 'y']
|
| 350 |
prophet_df = prophet_df.dropna()
|
|
@@ -352,172 +369,149 @@ def analyze_trading_pair(pair_name):
|
|
| 352 |
print(f"๐ Using {len(prophet_df)} data points for forecasting")
|
| 353 |
|
| 354 |
if len(prophet_df) < 30:
|
| 355 |
-
forecast_result = f"โ ๏ธ Not enough recent data points for forecasting (have {len(prophet_df)}, need at least 30)"
|
| 356 |
-
|
| 357 |
else:
|
| 358 |
-
#
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
# Create forecast chart
|
| 380 |
-
forecast_fig = go.Figure()
|
| 381 |
-
|
| 382 |
-
# Historical data (only show last 90 days for clarity)
|
| 383 |
-
hist_recent = prophet_df[prophet_df['ds'] >= (prophet_df['ds'].max() - pd.Timedelta(days=90))]
|
| 384 |
-
forecast_fig.add_trace(go.Scatter(
|
| 385 |
-
x=hist_recent['ds'],
|
| 386 |
-
y=hist_recent['y'],
|
| 387 |
-
mode='lines',
|
| 388 |
-
name='Historical',
|
| 389 |
-
line=dict(color='blue', width=2)
|
| 390 |
-
))
|
| 391 |
-
|
| 392 |
-
# Forecast data
|
| 393 |
-
forecast_recent = forecast[forecast['ds'] >= (prophet_df['ds'].max() - pd.Timedelta(days=30))]
|
| 394 |
-
forecast_fig.add_trace(go.Scatter(
|
| 395 |
-
x=forecast_recent['ds'],
|
| 396 |
-
y=forecast_recent['yhat'],
|
| 397 |
-
mode='lines',
|
| 398 |
-
name='Forecast',
|
| 399 |
-
line=dict(color='red', width=2, dash='dash')
|
| 400 |
-
))
|
| 401 |
-
|
| 402 |
-
# Confidence interval
|
| 403 |
-
forecast_fig.add_trace(go.Scatter(
|
| 404 |
-
x=forecast_recent['ds'].tolist() + forecast_recent['ds'][::-1].tolist(),
|
| 405 |
-
y=forecast_recent['yhat_upper'].tolist() + forecast_recent['yhat_lower'][::-1].tolist(),
|
| 406 |
-
fill='toself',
|
| 407 |
-
fillcolor='rgba(255,0,0,0.1)',
|
| 408 |
-
line=dict(color='rgba(255,255,255,0)'),
|
| 409 |
-
name='95% CI'
|
| 410 |
-
))
|
| 411 |
-
|
| 412 |
-
forecast_fig.update_layout(
|
| 413 |
-
title=f"{pair_name} 30-Day Price Forecast",
|
| 414 |
-
xaxis_title="Date",
|
| 415 |
-
yaxis_title="Price",
|
| 416 |
-
template="plotly_white",
|
| 417 |
-
height=500,
|
| 418 |
-
hovermode="x unified"
|
| 419 |
-
)
|
| 420 |
-
|
| 421 |
-
# Create forecast table (next 30 days only)
|
| 422 |
-
future_dates = forecast[forecast['ds'] > prophet_df['ds'].max()].head(30)
|
| 423 |
-
|
| 424 |
-
# Format dates and prices
|
| 425 |
-
future_dates['Date'] = future_dates['ds'].dt.strftime('%Y-%m-%d')
|
| 426 |
-
future_dates['Predicted Price'] = future_dates['yhat'].apply(lambda x: f"{x:.5f}")
|
| 427 |
-
future_dates['Lower Bound'] = future_dates['yhat_lower'].apply(lambda x: f"{x:.5f}")
|
| 428 |
-
future_dates['Upper Bound'] = future_dates['yhat_upper'].apply(lambda x: f"{x:.5f}")
|
| 429 |
-
future_dates['Trend'] = future_dates['yhat'].diff().apply(
|
| 430 |
-
lambda x: "๐ Rising" if x > 0 else "๐ Falling" if x < 0 else "โก๏ธ Stable"
|
| 431 |
-
)
|
| 432 |
-
|
| 433 |
-
# Create table data
|
| 434 |
-
table_data = future_dates[['Date', 'Predicted Price', 'Lower Bound', 'Upper Bound', 'Trend']].values.tolist()
|
| 435 |
-
table_headers = ["Date", "Predicted Price", "Lower Bound", "Upper Bound", "Trend"]
|
| 436 |
-
|
| 437 |
-
forecast_table = gr.DataFrame(
|
| 438 |
-
headers=table_headers,
|
| 439 |
-
value=table_data,
|
| 440 |
-
datatype=["str", "str", "str", "str", "str"],
|
| 441 |
-
label=f"{pair_name} 30-Day Price Forecast Table",
|
| 442 |
-
interactive=False
|
| 443 |
-
)
|
| 444 |
-
|
| 445 |
-
# Get last forecast values
|
| 446 |
-
last_forecast = forecast.iloc[-1]
|
| 447 |
-
forecast_result = (
|
| 448 |
-
f"๐ฎ 30-Day Forecast:\n"
|
| 449 |
-
f"Predicted price: {last_forecast['yhat']:.5f}\n"
|
| 450 |
-
f"Range: {last_forecast['yhat_lower']:.5f} to {last_forecast['yhat_upper']:.5f}"
|
| 451 |
-
)
|
| 452 |
-
print("โ
Forecast generated successfully")
|
| 453 |
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
traceback.print_exc()
|
| 471 |
-
|
| 472 |
-
# Technical
|
| 473 |
current_price = hist['Close'].iloc[-1]
|
| 474 |
signal = "๐ Analyzing market conditions..."
|
| 475 |
|
| 476 |
-
|
|
|
|
| 477 |
ma20 = hist['MA20'].iloc[-1]
|
| 478 |
-
if current_price > ma20:
|
| 479 |
-
signal = "๐ BULLISH: Price above 20-period MA"
|
| 480 |
-
else:
|
| 481 |
-
signal = "๐ BEARISH: Price below 20-period MA"
|
| 482 |
-
|
| 483 |
-
if 'MA50' in hist.columns:
|
| 484 |
ma50 = hist['MA50'].iloc[-1]
|
|
|
|
| 485 |
if current_price > ma20 and ma20 > ma50:
|
| 486 |
-
signal = "๐ STRONG BULLISH: Golden Cross
|
| 487 |
elif current_price < ma20 and ma20 < ma50:
|
| 488 |
-
signal = "๐ฃ STRONG BEARISH: Death Cross
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
|
| 490 |
# Calculate performance metrics
|
| 491 |
start_price = hist['Close'].iloc[0]
|
| 492 |
total_return = (current_price / start_price - 1) * 100
|
| 493 |
-
volatility = hist['Close'].pct_change().std() * np.sqrt(252) * 100 # Annualized volatility
|
| 494 |
|
| 495 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
result_text = (
|
| 497 |
-
f"๐ {pair_name} Analysis Report\n"
|
| 498 |
-
f"{'=' *
|
| 499 |
-
f"๐ฐ Current Price: {current_price:.5f}\n"
|
| 500 |
-
f"๐ Total Return: {total_return:.2f}%\n"
|
| 501 |
-
f"โก Volatility: {volatility:.2f}%\n"
|
| 502 |
-
f"๐ฏ Signal: {signal}\n"
|
| 503 |
-
f"{'=' *
|
| 504 |
f"{forecast_result}"
|
| 505 |
)
|
| 506 |
|
| 507 |
print(f"โ
Analysis completed for {pair_name}")
|
| 508 |
return result_text, fig, forecast_fig, forecast_table
|
| 509 |
-
|
| 510 |
except Exception as e:
|
| 511 |
-
error_msg = f"โ Analysis
|
| 512 |
print(error_msg)
|
| 513 |
-
import traceback
|
| 514 |
traceback.print_exc()
|
| 515 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
|
| 517 |
-
# Create Gradio interface
|
| 518 |
with gr.Blocks(title="Trading Pair AI Analyzer") as demo:
|
| 519 |
gr.Markdown("# ๐ Trading Pair AI Analysis System")
|
| 520 |
-
gr.Markdown("### Analyze
|
| 521 |
|
| 522 |
with gr.Row():
|
| 523 |
with gr.Column(scale=2):
|
|
@@ -525,85 +519,75 @@ with gr.Blocks(title="Trading Pair AI Analyzer") as demo:
|
|
| 525 |
label="๐ Available Data",
|
| 526 |
value=get_available_pairs(),
|
| 527 |
interactive=False,
|
| 528 |
-
lines=5
|
|
|
|
| 529 |
)
|
| 530 |
|
| 531 |
with gr.Column(scale=1):
|
| 532 |
gr.Markdown("### โน๏ธ System Information")
|
| 533 |
system_info = gr.Textbox(
|
| 534 |
-
value=
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
| 538 |
)
|
| 539 |
|
| 540 |
with gr.Row():
|
| 541 |
-
with gr.Column():
|
| 542 |
pair_input = gr.Textbox(
|
| 543 |
-
label="๐ Trading Pair",
|
| 544 |
-
value="EURUSD",
|
| 545 |
placeholder="Enter pair name (e.g., EURUSD, BTCUSD, AAPL)"
|
| 546 |
)
|
| 547 |
-
analyze_btn = gr.Button("๐ Analyze", variant="primary")
|
| 548 |
-
|
| 549 |
-
with gr.Column():
|
| 550 |
-
gr.Markdown("### ๐ก Quick Tips")
|
| 551 |
-
gr.Markdown("""
|
| 552 |
-
- Use pair names from the available data list
|
| 553 |
-
- System automatically preprocesses your data
|
| 554 |
-
- First analysis may take 30-60 seconds
|
| 555 |
-
- Forecast table shows next 30 days of predicted prices
|
| 556 |
-
""")
|
| 557 |
-
|
| 558 |
-
result_output = gr.Textbox(label="๐ Analysis Results", lines=8)
|
| 559 |
-
|
| 560 |
-
with gr.Tabs():
|
| 561 |
-
with gr.TabItem("๐ Charts"):
|
| 562 |
-
with gr.Row():
|
| 563 |
-
price_chart = gr.Plot(label="๐ Price Chart & Technical Indicators")
|
| 564 |
-
forecast_chart = gr.Plot(label="๐ฎ 30-Day Price Forecast")
|
| 565 |
|
| 566 |
-
with gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
forecast_table = gr.DataFrame(
|
| 568 |
headers=["Date", "Predicted Price", "Lower Bound", "Upper Bound", "Trend"],
|
| 569 |
value=[],
|
| 570 |
datatype=["str", "str", "str", "str", "str"],
|
| 571 |
-
label="30-Day
|
| 572 |
-
interactive=False
|
|
|
|
| 573 |
)
|
| 574 |
|
| 575 |
-
with gr.Accordion("๐ Data
|
| 576 |
gr.Markdown("""
|
| 577 |
-
### How to Add Your Own Data
|
| 578 |
|
| 579 |
-
1.
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
- Volume (optional)
|
| 583 |
|
| 584 |
-
2.
|
| 585 |
-
- Go to your Space Files tab
|
| 586 |
-
- Create directories: `data/raw/`
|
| 587 |
-
- Upload your CSV files to `data/raw/`
|
| 588 |
-
- Example filenames: `EURUSD.csv`, `BTCUSD.csv`
|
| 589 |
|
| 590 |
-
3.
|
| 591 |
-
- Go to Settings โ Restart Space
|
| 592 |
-
- Wait 2-3 minutes for rebuild
|
| 593 |
|
| 594 |
-
4. **Your data will be automatically
|
| 595 |
""")
|
| 596 |
|
| 597 |
# Examples for quick testing
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
[
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
label="Try these examples:"
|
| 606 |
-
)
|
| 607 |
|
| 608 |
# Analysis function
|
| 609 |
analyze_btn.click(
|
|
|
|
| 5 |
from prophet import Prophet
|
| 6 |
import os
|
| 7 |
import warnings
|
| 8 |
+
import datetime
|
| 9 |
+
import shutil # For moving files
|
| 10 |
+
import traceback # For detailed error logging
|
| 11 |
+
|
| 12 |
+
# Suppress all warnings for a cleaner output
|
| 13 |
warnings.filterwarnings('ignore')
|
| 14 |
|
| 15 |
+
# --- Configuration and Environment Setup ---
|
| 16 |
+
# Performance optimization for Apple Silicon/MKL based libraries
|
| 17 |
os.environ["OMP_NUM_THREADS"] = "1"
|
| 18 |
os.environ["OPENBLAS_NUM_THREADS"] = "1"
|
| 19 |
os.environ["MKL_NUM_THREADS"] = "1"
|
|
|
|
| 23 |
PROCESSED_DATA_DIR = "data/processed"
|
| 24 |
os.makedirs(PROCESSED_DATA_DIR, exist_ok=True)
|
| 25 |
|
| 26 |
+
# Predefined trading pairs with expected formats.
|
| 27 |
+
# This dictionary will be dynamically updated in load_available_data.
|
| 28 |
TRADING_PAIRS = {
|
| 29 |
+
# Default configs for known pairs
|
| 30 |
"EURUSD": {
|
| 31 |
+
"description": "Euro to US Dollar Forex Pair (Sample)",
|
| 32 |
"date_format": "%d.%m.%Y %H:%M:%S.%f %z",
|
| 33 |
"has_timezone": True,
|
| 34 |
"decimal_separator": ".",
|
| 35 |
"required_columns": ["Open", "High", "Low", "Close"]
|
| 36 |
},
|
| 37 |
"BTCUSD": {
|
| 38 |
+
"description": "Bitcoin to US Dollar (Sample)",
|
| 39 |
"date_format": "%Y-%m-%d %H:%M:%S",
|
| 40 |
"has_timezone": False,
|
| 41 |
"decimal_separator": ".",
|
| 42 |
"required_columns": ["Open", "High", "Low", "Close"]
|
| 43 |
},
|
| 44 |
"AAPL": {
|
| 45 |
+
"description": "Apple Inc. Stock (Sample)",
|
| 46 |
"date_format": "%Y-%m-%d",
|
| 47 |
"has_timezone": False,
|
| 48 |
"decimal_separator": ".",
|
|
|
|
| 50 |
}
|
| 51 |
}
|
| 52 |
|
| 53 |
+
# --- Data Preprocessing Functions ---
|
| 54 |
+
|
| 55 |
def preprocess_data_file(raw_file_path, pair_name):
|
| 56 |
"""Preprocess raw data file to standardized format"""
|
| 57 |
print(f"๐ Preprocessing data for {pair_name}...")
|
| 58 |
|
| 59 |
+
# Get pair configuration, or use generic defaults for a new pair
|
| 60 |
+
config = TRADING_PAIRS.get(pair_name, {
|
| 61 |
+
"description": f"{pair_name} Trading Pair",
|
| 62 |
+
"date_format": None, # Use dynamic parsing for generic pairs
|
| 63 |
+
"has_timezone": False,
|
| 64 |
+
"decimal_separator": ".",
|
| 65 |
+
"required_columns": ["Open", "High", "Low", "Close"]
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
try:
|
| 69 |
+
# Read raw data with robust encoding and generic delimiter detection
|
|
|
|
|
|
|
|
|
|
| 70 |
encodings = ['utf-8', 'latin1', 'ISO-8859-1', 'cp1252']
|
| 71 |
df = None
|
| 72 |
|
| 73 |
for encoding in encodings:
|
| 74 |
try:
|
| 75 |
+
# Attempt to read CSV, let pandas infer delimiter
|
| 76 |
+
df = pd.read_csv(raw_file_path, encoding=encoding, sep=None, engine='python')
|
| 77 |
print(f"โ
Successfully read {pair_name} data with {encoding} encoding")
|
| 78 |
break
|
| 79 |
except (UnicodeDecodeError, pd.errors.ParserError):
|
| 80 |
continue
|
| 81 |
|
| 82 |
if df is None:
|
| 83 |
+
raise Exception(f"โ Failed to read {pair_name} data with any encoding/delimiter")
|
| 84 |
|
| 85 |
# Standardize column names (case-insensitive)
|
| 86 |
column_mapping = {}
|
| 87 |
for col in df.columns:
|
| 88 |
+
col_lower = col.lower().strip()
|
| 89 |
|
| 90 |
+
if any(keyword in col_lower for keyword in ['date', 'time', 'timestamp', 'ds']):
|
| 91 |
column_mapping[col] = 'datetime'
|
| 92 |
elif 'open' in col_lower:
|
| 93 |
column_mapping[col] = 'Open'
|
|
|
|
| 95 |
column_mapping[col] = 'High'
|
| 96 |
elif 'low' in col_lower:
|
| 97 |
column_mapping[col] = 'Low'
|
| 98 |
+
elif 'close' in col_lower or 'price' in col_lower:
|
| 99 |
column_mapping[col] = 'Close'
|
| 100 |
+
elif 'volume' in col_lower or 'vol' in col_lower:
|
| 101 |
column_mapping[col] = 'Volume'
|
| 102 |
|
| 103 |
if column_mapping:
|
|
|
|
| 106 |
|
| 107 |
# Process datetime column
|
| 108 |
datetime_col = None
|
| 109 |
+
for col in ['datetime', 'ds']:
|
| 110 |
if col in df.columns:
|
| 111 |
datetime_col = col
|
| 112 |
break
|
| 113 |
|
| 114 |
if datetime_col is None:
|
| 115 |
+
raise Exception("โ No datetime column found in data after renaming")
|
| 116 |
|
| 117 |
+
# Handle special EURUSD format with GMT (if pair is specifically EURUSD)
|
| 118 |
if pair_name == "EURUSD" and df[datetime_col].astype(str).str.contains('GMT').any():
|
| 119 |
print("๐ Handling EURUSD special datetime format...")
|
| 120 |
+
df[datetime_col] = df[datetime_col].astype(str).str.replace(' GMT', '', regex=False)
|
|
|
|
|
|
|
|
|
|
| 121 |
df[datetime_col] = pd.to_datetime(
|
| 122 |
df[datetime_col],
|
| 123 |
+
format=config['date_format'], # Use specific format for EURUSD
|
| 124 |
errors='coerce',
|
| 125 |
utc=True
|
| 126 |
)
|
| 127 |
else:
|
| 128 |
+
# Use robust generic datetime parsing for all other cases
|
| 129 |
df[datetime_col] = pd.to_datetime(
|
| 130 |
df[datetime_col],
|
| 131 |
errors='coerce',
|
| 132 |
+
utc=config.get('has_timezone', False) # Use config setting if available
|
| 133 |
)
|
| 134 |
|
| 135 |
# Remove rows with invalid dates
|
|
|
|
| 141 |
df.set_index(datetime_col, inplace=True)
|
| 142 |
df.sort_index(inplace=True)
|
| 143 |
|
| 144 |
+
# Handle non-standard decimal separators (e.g., European format ',')
|
| 145 |
if config['decimal_separator'] != '.':
|
| 146 |
+
print(f"๐ ๏ธ Fixing decimal separator from {config['decimal_separator']} to '.'")
|
| 147 |
for col in ['Open', 'High', 'Low', 'Close', 'Volume']:
|
| 148 |
if col in df.columns:
|
| 149 |
+
# Convert to string, replace comma with dot, then convert to float
|
| 150 |
+
df[col] = df[col].astype(str).str.replace(config['decimal_separator'], '.', regex=False).astype(float)
|
| 151 |
+
|
| 152 |
+
# Ensure price columns are numeric
|
| 153 |
+
for col in ['Open', 'High', 'Low', 'Close']:
|
| 154 |
+
if col in df.columns:
|
| 155 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 156 |
|
| 157 |
+
# Fill missing values (only after numeric conversion)
|
| 158 |
for col in ['Open', 'High', 'Low', 'Close']:
|
| 159 |
if col in df.columns:
|
| 160 |
missing_before = df[col].isna().sum()
|
|
|
|
| 167 |
df = df[~df.index.duplicated(keep='first')]
|
| 168 |
print(f"๐งน Removed {before_count - len(df)} duplicate entries")
|
| 169 |
|
| 170 |
+
# Final validation
|
| 171 |
missing_cols = [col for col in config['required_columns'] if col not in df.columns]
|
| 172 |
if missing_cols:
|
| 173 |
+
print(f"โ Missing required columns: {missing_cols}. Available: {df.columns.tolist()}")
|
|
|
|
| 174 |
return None
|
| 175 |
|
| 176 |
+
if len(df) < 2:
|
| 177 |
+
raise Exception("Dataset has fewer than 2 valid rows after preprocessing.")
|
| 178 |
+
|
| 179 |
# Save preprocessed data
|
| 180 |
processed_file = os.path.join(PROCESSED_DATA_DIR, f"{pair_name}_processed.csv")
|
| 181 |
df.to_csv(processed_file)
|
|
|
|
| 185 |
|
| 186 |
except Exception as e:
|
| 187 |
print(f"โ Preprocessing error for {pair_name}: {str(e)}")
|
| 188 |
+
traceback.print_exc()
|
| 189 |
return None
|
| 190 |
|
| 191 |
def load_available_data():
|
| 192 |
+
"""Load and preprocess all available data files, handling raw folder creation."""
|
| 193 |
+
global TRADING_PAIRS, available_data
|
| 194 |
available_data = {}
|
| 195 |
|
| 196 |
+
# Check if data exists in a generic 'data' folder and move it to 'data/raw'
|
| 197 |
if not os.path.exists(RAW_DATA_DIR):
|
| 198 |
+
print(f"โ ๏ธ Raw data directory not found: {RAW_DATA_DIR}. Checking 'data/'...")
|
|
|
|
| 199 |
if os.path.exists("data") and os.path.isdir("data"):
|
| 200 |
+
csv_files = [f for f in os.listdir("data") if f.endswith('.csv')]
|
| 201 |
+
if csv_files:
|
| 202 |
+
os.makedirs(RAW_DATA_DIR, exist_ok=True)
|
| 203 |
+
for filename in csv_files:
|
| 204 |
+
try:
|
| 205 |
+
shutil.move(os.path.join("data", filename), os.path.join(RAW_DATA_DIR, filename))
|
| 206 |
+
print(f"โ
Moved {filename} to {RAW_DATA_DIR}")
|
| 207 |
+
except Exception as e:
|
| 208 |
+
print(f"โ ๏ธ Could not move {filename}: {e}")
|
| 209 |
+
else:
|
| 210 |
+
print("No CSV files found in 'data/' to move.")
|
| 211 |
+
|
| 212 |
if not os.path.exists(RAW_DATA_DIR):
|
| 213 |
+
print(f"โ Still cannot find raw data directory: {RAW_DATA_DIR}. Please check your file structure.")
|
| 214 |
return available_data
|
| 215 |
|
| 216 |
print(f"๐ Scanning for data files in {RAW_DATA_DIR}...")
|
| 217 |
|
|
|
|
| 218 |
for filename in os.listdir(RAW_DATA_DIR):
|
| 219 |
if filename.endswith('.csv'):
|
| 220 |
+
# Extract pair name from filename (e.g., EURUSD.csv -> EURUSD)
|
| 221 |
pair_name = filename.split('.')[0].upper()
|
| 222 |
|
| 223 |
+
# --- Generalization Improvement ---
|
| 224 |
+
# If pair is not in TRADING_PAIRS, add it with generic defaults
|
| 225 |
if pair_name not in TRADING_PAIRS:
|
| 226 |
TRADING_PAIRS[pair_name] = {
|
| 227 |
+
"description": f"{pair_name} Trading Pair (Generic)",
|
| 228 |
+
"date_format": None, # Indicates dynamic parsing
|
| 229 |
"has_timezone": False,
|
| 230 |
"decimal_separator": ".",
|
| 231 |
"required_columns": ["Open", "High", "Low", "Close"]
|
|
|
|
| 234 |
raw_file_path = os.path.join(RAW_DATA_DIR, filename)
|
| 235 |
processed_file_path = os.path.join(PROCESSED_DATA_DIR, f"{pair_name}_processed.csv")
|
| 236 |
|
| 237 |
+
# Check for existing preprocessed file
|
| 238 |
if os.path.exists(processed_file_path):
|
| 239 |
raw_mod_time = os.path.getmtime(raw_file_path)
|
| 240 |
processed_mod_time = os.path.getmtime(processed_file_path)
|
| 241 |
|
| 242 |
if processed_mod_time > raw_mod_time:
|
|
|
|
| 243 |
try:
|
| 244 |
df = pd.read_csv(processed_file_path, index_col=0, parse_dates=True)
|
| 245 |
available_data[pair_name] = df
|
| 246 |
+
print(f"โ
Using existing preprocessed data for {pair_name} with {len(df)} records")
|
| 247 |
continue
|
| 248 |
except Exception as e:
|
| 249 |
+
print(f"โ ๏ธ Error loading preprocessed file for {pair_name}: {str(e)}. Reprocessing.")
|
| 250 |
|
| 251 |
# Preprocess the file
|
| 252 |
print(f"๐ Processing {pair_name} data...")
|
|
|
|
| 255 |
available_data[pair_name] = df
|
| 256 |
print(f"โ
Successfully loaded {pair_name} with {len(df)} records")
|
| 257 |
else:
|
| 258 |
+
print(f"โ Failed to load {pair_name} data. See error above.")
|
| 259 |
|
| 260 |
return available_data
|
| 261 |
|
| 262 |
+
# --- Analysis Functions ---
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
def get_available_pairs():
|
| 265 |
+
"""Get list of available trading pairs with status for UI"""
|
| 266 |
if not available_data:
|
| 267 |
+
return "โ ๏ธ No data files found. Please upload CSV files to the 'data/raw' directory and restart the app."
|
| 268 |
|
| 269 |
status = "โ
Available trading pairs:\n"
|
| 270 |
for pair in sorted(available_data.keys()):
|
| 271 |
df = available_data[pair]
|
| 272 |
records = len(df)
|
| 273 |
+
if records > 0:
|
| 274 |
+
date_range = f"{df.index.min().strftime('%Y-%m-%d')} to {df.index.max().strftime('%Y-%m-%d')}"
|
| 275 |
+
status += f"โข {pair}: {records} records ({date_range})\n"
|
| 276 |
+
else:
|
| 277 |
+
status += f"โข {pair}: 0 records (Data Error)\n"
|
| 278 |
return status
|
| 279 |
|
| 280 |
+
def analyze_trading_pair(pair_name: str):
|
| 281 |
+
"""
|
| 282 |
+
Analyzes a specific trading pair, generates candlestick chart,
|
| 283 |
+
calculates technical indicators, and runs a Prophet forecast.
|
| 284 |
+
"""
|
| 285 |
+
|
| 286 |
+
pair_name = pair_name.upper().strip() # Normalize input
|
| 287 |
+
print(f"\n๐ Starting analysis for {pair_name}")
|
| 288 |
+
|
| 289 |
+
# Initial return values for error case
|
| 290 |
+
default_error_fig = go.Figure().update_layout(title="Analysis Failed", xaxis_title="Time", yaxis_title="Price")
|
| 291 |
+
default_error_df = gr.DataFrame(headers=["Error"], value=[["Analysis failed"]])
|
| 292 |
+
|
| 293 |
+
# Check if data is available (case-insensitive search)
|
| 294 |
+
matched_pair = None
|
| 295 |
+
for available_pair in available_data.keys():
|
| 296 |
+
if pair_name == available_pair or pair_name.upper() == available_pair.upper():
|
| 297 |
+
matched_pair = available_pair
|
| 298 |
+
break
|
| 299 |
+
|
| 300 |
+
if matched_pair is None:
|
| 301 |
+
available_pairs = ", ".join(available_data.keys()) or "None"
|
| 302 |
+
return (
|
| 303 |
+
f"โ Data not available for '{pair_name}'\nAvailable pairs: {available_pairs}",
|
| 304 |
+
default_error_fig, default_error_fig, default_error_df
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
pair_name = matched_pair
|
| 308 |
+
hist = available_data[pair_name].copy()
|
| 309 |
+
|
| 310 |
try:
|
| 311 |
+
if len(hist) < 5:
|
| 312 |
+
return (
|
| 313 |
+
f"โ Data is too short for analysis. Only {len(hist)} records.",
|
| 314 |
+
default_error_fig, default_error_fig, default_error_df
|
| 315 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
# Basic data validation
|
| 318 |
required_cols = ['Open', 'High', 'Low', 'Close']
|
| 319 |
+
if not all(col in hist.columns for col in required_cols):
|
| 320 |
+
missing_cols = [col for col in required_cols if col not in hist.columns]
|
|
|
|
| 321 |
return (
|
| 322 |
+
f"โ Missing required columns: {', '.join(missing_cols)}\nAvailable columns: {', '.join(hist.columns)}",
|
| 323 |
+
default_error_fig, default_error_fig, default_error_df
|
|
|
|
| 324 |
)
|
| 325 |
|
| 326 |
+
# --- 1. Candlestick Chart with Technical Indicators (MAs) ---
|
| 327 |
fig = go.Figure()
|
| 328 |
|
| 329 |
# Add candlestick
|
|
|
|
| 336 |
name='Price'
|
| 337 |
))
|
| 338 |
|
| 339 |
+
# Add moving averages
|
| 340 |
if len(hist) >= 20:
|
| 341 |
hist['MA20'] = hist['Close'].rolling(window=20, min_periods=1).mean()
|
| 342 |
+
fig.add_trace(go.Scatter(x=hist.index, y=hist['MA20'], mode='lines', name='20-period MA', line=dict(color='blue', width=1.5)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
if len(hist) >= 50:
|
| 345 |
hist['MA50'] = hist['Close'].rolling(window=50, min_periods=1).mean()
|
| 346 |
+
fig.add_trace(go.Scatter(x=hist.index, y=hist['MA50'], mode='lines', name='50-period MA', line=dict(color='orange', width=1.5)))
|
| 347 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
fig.update_layout(
|
| 349 |
title=f"{pair_name} Price Analysis",
|
| 350 |
xaxis_title="Date",
|
| 351 |
yaxis_title="Price",
|
| 352 |
template="plotly_white",
|
| 353 |
hovermode="x unified",
|
| 354 |
+
xaxis_rangeslider_visible=False, # Hide the bottom slider for cleaner look
|
| 355 |
height=500,
|
|
|
|
| 356 |
)
|
| 357 |
|
| 358 |
+
# --- 2. Forecasting using Prophet ---
|
| 359 |
+
forecast_fig = default_error_fig
|
| 360 |
+
forecast_table = default_error_df
|
| 361 |
+
forecast_result = "No forecast data available"
|
| 362 |
|
| 363 |
try:
|
| 364 |
+
# Use last 1 year (365 days) of data for forecasting, ensures manageable size and recent relevance
|
|
|
|
|
|
|
|
|
|
| 365 |
prophet_df = hist[['Close']].copy().last('365D').reset_index()
|
| 366 |
prophet_df.columns = ['ds', 'y']
|
| 367 |
prophet_df = prophet_df.dropna()
|
|
|
|
| 369 |
print(f"๐ Using {len(prophet_df)} data points for forecasting")
|
| 370 |
|
| 371 |
if len(prophet_df) < 30:
|
| 372 |
+
forecast_result = f"โ ๏ธ Not enough recent data points for forecasting (have {len(prophet_df)}, need at least 30 historical daily points for good results)."
|
| 373 |
+
|
| 374 |
else:
|
| 375 |
+
# Initialize Prophet model with CRITICAL FIX (stan_backend=None)
|
| 376 |
+
model = Prophet(
|
| 377 |
+
daily_seasonality=False, # Use False unless intra-day data is used
|
| 378 |
+
yearly_seasonality=True,
|
| 379 |
+
interval_width=0.95,
|
| 380 |
+
changepoint_prior_scale=0.05,
|
| 381 |
+
stan_backend=None # CRITICAL FIX for better compatibility
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
# Automatically add custom daily seasonality if data is less than daily resolution
|
| 385 |
+
if (prophet_df['ds'].diff().min().total_seconds() < 86400 * 0.9): # < 90% of a day
|
| 386 |
+
model.add_seasonality(name='subdaily', period=1, fourier_order=5, prior_scale=0.1)
|
| 387 |
+
|
| 388 |
+
model.fit(prophet_df)
|
| 389 |
+
|
| 390 |
+
# Create future dataframe (30 days forecast)
|
| 391 |
+
future = model.make_future_dataframe(periods=30, freq='D')
|
| 392 |
+
forecast = model.predict(future)
|
| 393 |
+
|
| 394 |
+
# Create forecast chart
|
| 395 |
+
forecast_fig = go.Figure()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
+
# Historical data (only show last 90 days for clarity)
|
| 398 |
+
hist_recent = prophet_df[prophet_df['ds'] >= (prophet_df['ds'].max() - pd.Timedelta(days=90))]
|
| 399 |
+
forecast_fig.add_trace(go.Scatter(x=hist_recent['ds'], y=hist_recent['y'], mode='lines', name='Historical', line=dict(color='blue', width=2)))
|
| 400 |
+
|
| 401 |
+
# Forecast data
|
| 402 |
+
# Show forecast from last 30 days of historical data + future
|
| 403 |
+
forecast_recent = forecast[forecast['ds'] >= (prophet_df['ds'].max() - pd.Timedelta(days=30))]
|
| 404 |
+
forecast_fig.add_trace(go.Scatter(x=forecast_recent['ds'], y=forecast_recent['yhat'], mode='lines', name='Forecast', line=dict(color='red', width=2, dash='dash')))
|
| 405 |
+
|
| 406 |
+
# Confidence interval
|
| 407 |
+
forecast_fig.add_trace(go.Scatter(
|
| 408 |
+
x=forecast_recent['ds'].tolist() + forecast_recent['ds'][::-1].tolist(),
|
| 409 |
+
y=forecast_recent['yhat_upper'].tolist() + forecast_recent['yhat_lower'][::-1].tolist(),
|
| 410 |
+
fill='toself', fillcolor='rgba(255,0,0,0.1)', line=dict(color='rgba(255,255,255,0)'), name='95% CI'
|
| 411 |
+
))
|
| 412 |
+
|
| 413 |
+
forecast_fig.update_layout(
|
| 414 |
+
title=f"{pair_name} 30-Day Price Forecast", xaxis_title="Date", yaxis_title="Price",
|
| 415 |
+
template="plotly_white", height=500, hovermode="x unified"
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
# Create forecast table
|
| 419 |
+
future_dates = forecast[forecast['ds'] > prophet_df['ds'].max()].head(30)
|
| 420 |
+
|
| 421 |
+
# Calculate trend based on yhat difference
|
| 422 |
+
future_dates['Trend_Value'] = future_dates['yhat'].diff()
|
| 423 |
+
future_dates.iloc[0, future_dates.columns.get_loc('Trend_Value')] = future_dates.iloc[0]['yhat'] - prophet_df.iloc[-1]['y']
|
| 424 |
+
|
| 425 |
+
future_dates['Date'] = future_dates['ds'].dt.strftime('%Y-%m-%d')
|
| 426 |
+
future_dates['Predicted Price'] = future_dates['yhat'].apply(lambda x: f"{x:.5f}")
|
| 427 |
+
future_dates['Lower Bound'] = future_dates['yhat_lower'].apply(lambda x: f"{x:.5f}")
|
| 428 |
+
future_dates['Upper Bound'] = future_dates['yhat_upper'].apply(lambda x: f"{x:.5f}")
|
| 429 |
+
future_dates['Trend'] = future_dates['Trend_Value'].apply(
|
| 430 |
+
lambda x: "๐ Rising" if x > 0 else "๐ Falling" if x < 0 else "โก๏ธ Stable"
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
table_data = future_dates[['Date', 'Predicted Price', 'Lower Bound', 'Upper Bound', 'Trend']].values.tolist()
|
| 434 |
+
table_headers = ["Date", "Predicted Price", "Lower Bound", "Upper Bound", "Trend"]
|
| 435 |
+
|
| 436 |
+
forecast_table = gr.DataFrame(headers=table_headers, value=table_data)
|
| 437 |
+
|
| 438 |
+
last_forecast = forecast.iloc[-1]
|
| 439 |
+
forecast_result = (
|
| 440 |
+
f"๐ฎ 30-Day Forecast:\n"
|
| 441 |
+
f"Predicted price for {last_forecast['ds'].strftime('%Y-%m-%d')}: **{last_forecast['yhat']:.5f}**\n"
|
| 442 |
+
f"95% Confidence Range: {last_forecast['yhat_lower']:.5f} to {last_forecast['yhat_upper']:.5f}"
|
| 443 |
+
)
|
| 444 |
+
print("โ
Forecast generated successfully")
|
| 445 |
+
|
| 446 |
+
except Exception as model_error:
|
| 447 |
+
forecast_result = f"โ ๏ธ Forecasting failed. Error: {str(model_error)}"
|
| 448 |
+
print(f"โ Forecasting failed: {forecast_result}")
|
| 449 |
traceback.print_exc()
|
| 450 |
+
|
| 451 |
+
# --- 3. Technical Analysis Signal & Metrics ---
|
| 452 |
current_price = hist['Close'].iloc[-1]
|
| 453 |
signal = "๐ Analyzing market conditions..."
|
| 454 |
|
| 455 |
+
# Signal based on MAs
|
| 456 |
+
if 'MA50' in hist.columns and 'MA20' in hist.columns:
|
| 457 |
ma20 = hist['MA20'].iloc[-1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
ma50 = hist['MA50'].iloc[-1]
|
| 459 |
+
|
| 460 |
if current_price > ma20 and ma20 > ma50:
|
| 461 |
+
signal = "๐ **STRONG BULLISH**: Price above 20MA, and 20MA > 50MA (Golden Cross potential)"
|
| 462 |
elif current_price < ma20 and ma20 < ma50:
|
| 463 |
+
signal = "๐ฃ **STRONG BEARISH**: Price below 20MA, and 20MA < 50MA (Death Cross potential)"
|
| 464 |
+
elif current_price > ma20:
|
| 465 |
+
signal = "๐ **BULLISH**: Price above 20-period MA"
|
| 466 |
+
elif current_price < ma20:
|
| 467 |
+
signal = "๐ **BEARISH**: Price below 20-period MA"
|
| 468 |
|
| 469 |
# Calculate performance metrics
|
| 470 |
start_price = hist['Close'].iloc[0]
|
| 471 |
total_return = (current_price / start_price - 1) * 100
|
|
|
|
| 472 |
|
| 473 |
+
# Annualized volatility: assumes daily data, adjusts for time period if needed (simple)
|
| 474 |
+
# Use a more robust check for non-daily data, e.g., daily returns if frequency is higher than daily
|
| 475 |
+
volatility = hist['Close'].pct_change().std() * np.sqrt(252) * 100
|
| 476 |
+
|
| 477 |
+
# Create final result text
|
| 478 |
result_text = (
|
| 479 |
+
f"๐ **{pair_name} Analysis Report**\n"
|
| 480 |
+
f"{'=' * 50}\n"
|
| 481 |
+
f"๐ฐ **Current Price**: {current_price:.5f}\n"
|
| 482 |
+
f"๐ **Total Return (full period)**: {total_return:.2f}%\n"
|
| 483 |
+
f"โก **Annualized Volatility**: {volatility:.2f}%\n"
|
| 484 |
+
f"๐ฏ **Technical Signal**: {signal}\n"
|
| 485 |
+
f"{'=' * 50}\n"
|
| 486 |
f"{forecast_result}"
|
| 487 |
)
|
| 488 |
|
| 489 |
print(f"โ
Analysis completed for {pair_name}")
|
| 490 |
return result_text, fig, forecast_fig, forecast_table
|
| 491 |
+
|
| 492 |
except Exception as e:
|
| 493 |
+
error_msg = f"โ Major Analysis Error for {pair_name}: {str(e)}"
|
| 494 |
print(error_msg)
|
|
|
|
| 495 |
traceback.print_exc()
|
| 496 |
+
|
| 497 |
+
return (
|
| 498 |
+
error_msg,
|
| 499 |
+
default_error_fig,
|
| 500 |
+
default_error_fig,
|
| 501 |
+
default_error_df
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
# --- Application Initialization ---
|
| 505 |
+
|
| 506 |
+
print("๐ Initializing data processing system...")
|
| 507 |
+
available_data = load_available_data()
|
| 508 |
+
print(f"๐ Available trading pairs: {list(available_data.keys())}")
|
| 509 |
+
|
| 510 |
+
# --- Gradio Interface ---
|
| 511 |
|
|
|
|
| 512 |
with gr.Blocks(title="Trading Pair AI Analyzer") as demo:
|
| 513 |
gr.Markdown("# ๐ Trading Pair AI Analysis System")
|
| 514 |
+
gr.Markdown("### Analyze financial instruments with interactive charts and 30-day AI-powered forecasts (Prophet)")
|
| 515 |
|
| 516 |
with gr.Row():
|
| 517 |
with gr.Column(scale=2):
|
|
|
|
| 519 |
label="๐ Available Data",
|
| 520 |
value=get_available_pairs(),
|
| 521 |
interactive=False,
|
| 522 |
+
lines=5,
|
| 523 |
+
autoscroll=True
|
| 524 |
)
|
| 525 |
|
| 526 |
with gr.Column(scale=1):
|
| 527 |
gr.Markdown("### โน๏ธ System Information")
|
| 528 |
system_info = gr.Textbox(
|
| 529 |
+
value=(
|
| 530 |
+
f"๐ Trading Analysis System v2.3\n"
|
| 531 |
+
f"๐ Last updated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
|
| 532 |
+
f"๐งฎ Loaded pairs: {len(available_data)}"
|
| 533 |
+
),
|
| 534 |
+
interactive=False,
|
| 535 |
+
lines=3
|
| 536 |
)
|
| 537 |
|
| 538 |
with gr.Row():
|
| 539 |
+
with gr.Column(scale=2):
|
| 540 |
pair_input = gr.Textbox(
|
| 541 |
+
label="๐ Trading Pair to Analyze",
|
| 542 |
+
value=list(available_data.keys())[0] if available_data else "EURUSD", # Set default to first available pair
|
| 543 |
placeholder="Enter pair name (e.g., EURUSD, BTCUSD, AAPL)"
|
| 544 |
)
|
| 545 |
+
analyze_btn = gr.Button("๐ Analyze Pair", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
+
with gr.Column(scale=3):
|
| 548 |
+
result_output = gr.Textbox(label="๐ Analysis & Forecast Summary", lines=6, max_lines=6)
|
| 549 |
+
|
| 550 |
+
# Tabs for Visual Output
|
| 551 |
+
with gr.Tabs():
|
| 552 |
+
with gr.TabItem("๐ Price Chart & Indicators"):
|
| 553 |
+
price_chart = gr.Plot(label="Candlestick Chart with 20/50-period Moving Averages")
|
| 554 |
+
|
| 555 |
+
with gr.TabItem("๐ฎ Price Forecast Chart"):
|
| 556 |
+
forecast_chart = gr.Plot(label="30-Day Price Forecast (Prophet Model)")
|
| 557 |
+
|
| 558 |
+
with gr.TabItem("๐ Forecast Table"):
|
| 559 |
forecast_table = gr.DataFrame(
|
| 560 |
headers=["Date", "Predicted Price", "Lower Bound", "Upper Bound", "Trend"],
|
| 561 |
value=[],
|
| 562 |
datatype=["str", "str", "str", "str", "str"],
|
| 563 |
+
label="30-Day Predicted Prices Table",
|
| 564 |
+
interactive=False,
|
| 565 |
+
wrap=True
|
| 566 |
)
|
| 567 |
|
| 568 |
+
with gr.Accordion("๐ Data & Usage Instructions", open=False):
|
| 569 |
gr.Markdown("""
|
| 570 |
+
### How to Add Your Own Data (General Instrument Handling)
|
| 571 |
|
| 572 |
+
1. **Prepare your CSV file** with at least these columns:
|
| 573 |
+
- **Date/Time** column (any reasonable format)
|
| 574 |
+
- **Open, High, Low, Close** prices (case-insensitive column names are handled).
|
|
|
|
| 575 |
|
| 576 |
+
2. **Upload to Hugging Face Space**: Upload your CSV file(s) to the designated folder: `data/raw/`
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
|
| 578 |
+
3. **Restart the application**: Go to the Space Settings and select 'Restart Space'.
|
|
|
|
|
|
|
| 579 |
|
| 580 |
+
4. **Automatic Processing**: Your data will be automatically loaded, cleaned, and a new pair entry will appear in the 'Available Data' section, ready for analysis. The system is designed to generalize to *any* instrument/pair name you upload (e.g., `TSLA.csv`, `GBPCHF.csv`).
|
| 581 |
""")
|
| 582 |
|
| 583 |
# Examples for quick testing
|
| 584 |
+
examples_list = [pair for pair in available_data.keys() if pair in ["EURUSD", "BTCUSD", "AAPL"]]
|
| 585 |
+
if examples_list:
|
| 586 |
+
gr.Examples(
|
| 587 |
+
examples=[[pair] for pair in examples_list],
|
| 588 |
+
inputs=pair_input,
|
| 589 |
+
label="Try these examples:"
|
| 590 |
+
)
|
|
|
|
|
|
|
| 591 |
|
| 592 |
# Analysis function
|
| 593 |
analyze_btn.click(
|