Badumetsibb commited on
Commit
ae3edc2
·
verified ·
1 Parent(s): 06c0616

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +332 -0
app.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ import requests
8
+ import os
9
+ import time
10
+ import plotly.graph_objects as go
11
+ from plotly.subplots import make_subplots
12
+ from sklearn.preprocessing import StandardScaler
13
+
14
+ # --- 1. CONFIG & SECRETS ---
15
+ API_KEY = os.getenv("TWELVEDATA_KEY")
16
+ NTFY_TOPIC = os.getenv("NTFY_TOPIC")
17
+
18
+ # The Constellation Basket
19
+ TARGET_PAIR = "EUR/USD"
20
+ SYMBOLS = ["EUR/USD", "GBP/USD", "USD/JPY", "XAU/USD"]
21
+ TIMEFRAME = "15min"
22
+ LOOKBACK = 30
23
+
24
+ # Global State
25
+ GLOBAL_STATE = {
26
+ "model": None,
27
+ "last_trade": None,
28
+ "scaler": None,
29
+ "is_trained": False
30
+ }
31
+
32
+ # --- 2. THE MODEL: CONSTELLATION TRANSFORMER ---
33
+ class PositionalEncoding(nn.Module):
34
+ def __init__(self, d_model, max_len=5000):
35
+ super(PositionalEncoding, self).__init__()
36
+ # Simple learnable positional encoding
37
+ self.encoding = nn.Parameter(torch.zeros(1, max_len, d_model))
38
+
39
+ def forward(self, x):
40
+ # x: [Batch, Seq, Dim]
41
+ seq_len = x.size(1)
42
+ return x + self.encoding[:, :seq_len, :]
43
+
44
+ class ConstellationTransformer(nn.Module):
45
+ def __init__(self, input_dim, d_model=64, nhead=4, num_layers=2, num_gaussians=3):
46
+ super(ConstellationTransformer, self).__init__()
47
+
48
+ # 1. Embedding: Project 6 features (4 Pairs + 2 News) -> 64 Dim
49
+ self.embedding = nn.Linear(input_dim, d_model)
50
+ self.pos_encoder = PositionalEncoding(d_model)
51
+
52
+ # 2. Transformer Encoder: The "Graph" Brain
53
+ encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, batch_first=True, dropout=0.1)
54
+ self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
55
+
56
+ # 3. MDN Head
57
+ self.z_pi = nn.Linear(d_model, num_gaussians)
58
+ self.z_sigma = nn.Linear(d_model, num_gaussians)
59
+ self.z_mu = nn.Linear(d_model, num_gaussians)
60
+
61
+ # Init Sigma to be tight/confident
62
+ nn.init.constant_(self.z_sigma.bias, -2.0)
63
+
64
+ def forward(self, x):
65
+ # x: [Batch, Seq_Len, Features]
66
+ x = self.embedding(x)
67
+ x = self.pos_encoder(x)
68
+
69
+ # Attention Magic
70
+ x = self.transformer(x)
71
+
72
+ # Take the context from the last time step
73
+ context = x[:, -1, :]
74
+
75
+ pi = F.softmax(self.z_pi(context), dim=1)
76
+ sigma = F.softplus(self.z_sigma(context)) + 1e-6
77
+ mu = self.z_mu(context)
78
+ return pi, sigma, mu
79
+
80
+ def mdn_loss(pi, sigma, mu, y):
81
+ if y.dim() == 1: y = y.unsqueeze(1)
82
+ dist = torch.distributions.Normal(loc=mu, scale=sigma)
83
+ log_prob = dist.log_prob(y)
84
+ loss = -torch.logsumexp(torch.log(pi + 1e-8) + log_prob, dim=1)
85
+ return torch.mean(loss)
86
+
87
+ # --- 3. DATA PIPELINE (MULTI-PAIR) ---
88
+ def get_constellation_data():
89
+ if not API_KEY: return None, "❌ Error: TWELVEDATA_KEY missing."
90
+
91
+ dfs = []
92
+ # Fetch all pairs
93
+ for sym in SYMBOLS:
94
+ url = f"https://api.twelvedata.com/time_series?symbol={sym}&interval={TIMEFRAME}&outputsize=500&apikey={API_KEY}"
95
+ try:
96
+ r = requests.get(url).json()
97
+ if 'values' not in r: continue
98
+
99
+ df = pd.DataFrame(r['values'])
100
+ df['datetime'] = pd.to_datetime(df['datetime'])
101
+ df = df.sort_values('datetime').set_index('datetime')
102
+ df = df[['close']].astype(float)
103
+ df.rename(columns={'close': sym}, inplace=True) # Rename 'close' to 'EUR/USD'
104
+ dfs.append(df)
105
+ time.sleep(0.2) # Avoid rate limits
106
+ except: pass
107
+
108
+ if not dfs: return None, "❌ Failed to fetch data."
109
+
110
+ # Merge and Fill
111
+ master_df = pd.concat(dfs, axis=1).fillna(method='ffill').dropna()
112
+ return master_df, "✅ Constellation Aligned"
113
+
114
+ def get_events_data():
115
+ try:
116
+ url = "https://nfs.faireconomy.media/ff_calendar_thisweek.json"
117
+ r = requests.get(url, headers={"User-Agent": "V23/1.0"}, timeout=5)
118
+ data = r.json()
119
+ parsed = []
120
+ impact_map = {'Low': 1, 'Medium': 2, 'High': 3}
121
+ for i in data:
122
+ if i.get('country') in ['EUR', 'USD']:
123
+ dt = pd.to_datetime(i.get('date'), utc=True).tz_localize(None)
124
+ imp = impact_map.get(i.get('impact'), 0)
125
+ parsed.append({'DateTime': dt, 'Impact_Score': imp})
126
+ df = pd.DataFrame(parsed)
127
+ if not df.empty: df = df.sort_values('DateTime').set_index('DateTime')
128
+ return df
129
+ except: return pd.DataFrame()
130
+
131
+ def prepare_tensors(master_df, event_df):
132
+ # 1. Merge Events
133
+ if not event_df.empty:
134
+ merged = pd.merge_asof(master_df, event_df, left_index=True, right_index=True, direction='backward', tolerance=pd.Timedelta('4 hours')).fillna(0)
135
+ else:
136
+ merged = master_df.copy()
137
+ merged['Impact_Score'] = 0
138
+ merged['Surprise'] = 0.0
139
+
140
+ # 2. Calculate Returns for ALL Pairs
141
+ feature_cols = []
142
+ for sym in SYMBOLS:
143
+ col_name = f"{sym}_ret"
144
+ merged[col_name] = merged[sym].pct_change().fillna(0)
145
+ feature_cols.append(col_name)
146
+
147
+ # Add News
148
+ feature_cols.extend(['Surprise', 'Impact_Score'])
149
+
150
+ # 3. Scale
151
+ scaler = StandardScaler()
152
+ data_scaled = scaler.fit_transform(merged[feature_cols].values)
153
+
154
+ # 4. Windows
155
+ X_data = []
156
+ for i in range(LOOKBACK, len(data_scaled)):
157
+ X_data.append(data_scaled[i-LOOKBACK:i])
158
+
159
+ X_tensor = torch.FloatTensor(np.array(X_data))
160
+
161
+ # Metadata for reconstruction
162
+ # Target is EUR/USD (Index 0 in SYMBOLS list, so Index 0 in feature_cols)
163
+ # We need stats to unscale the Target Return
164
+ target_idx = 0
165
+ ret_mean = scaler.mean_[target_idx]
166
+ ret_scale = scaler.scale_[target_idx]
167
+
168
+ # Reference Prices for EUR/USD
169
+ ref_prices = merged[TARGET_PAIR].values[LOOKBACK:]
170
+
171
+ return X_tensor, merged.index[LOOKBACK:], ref_prices, ret_mean, ret_scale
172
+
173
+ # --- 4. CORE LOGIC ---
174
+ def send_ntfy(message):
175
+ if not NTFY_TOPIC: return
176
+ try:
177
+ requests.post(f"https://ntfy.sh/{NTFY_TOPIC}", data=message.encode('utf-8'), headers={"Title": "Holographic V4", "Priority": "high"})
178
+ except: pass
179
+
180
+ def hard_reset():
181
+ GLOBAL_STATE["model"] = None
182
+ GLOBAL_STATE["is_trained"] = False
183
+ return None, "<div>♻️ MEMORY WIPED. Click Refresh.</div>", "Reset."
184
+
185
+ def run_analysis():
186
+ log_buffer = []
187
+
188
+ # Init V4 Model
189
+ # Input Dim = 4 Pairs + 2 News = 6
190
+ if GLOBAL_STATE["model"] is None:
191
+ GLOBAL_STATE["model"] = ConstellationTransformer(input_dim=6, d_model=64, num_layers=2)
192
+ log_buffer.append("🧠 Constellation Transformer Initialized")
193
+
194
+ model = GLOBAL_STATE["model"]
195
+
196
+ # Get Data
197
+ master_df, msg = get_constellation_data()
198
+ if master_df is None: return None, msg, msg
199
+
200
+ event_df = get_events_data()
201
+
202
+ X_tensor, dates, ref_prices, ret_mean, ret_std = prepare_tensors(master_df, event_df)
203
+
204
+ # --- CALIBRATION ---
205
+ if not GLOBAL_STATE["is_trained"]:
206
+ log_buffer.append("⚙️ Learning Correlations...")
207
+ optimizer = torch.optim.Adam(model.parameters(), lr=0.005)
208
+ model.train()
209
+
210
+ train_X = X_tensor[:-1]
211
+
212
+ # Target: Return of EUR/USD (Target Pair)
213
+ # Calculate actual returns from reference prices
214
+ actual_returns = np.diff(ref_prices) / ref_prices[:-1]
215
+ actual_returns_scaled = (actual_returns - ret_mean) / ret_std
216
+ train_y = torch.FloatTensor(actual_returns_scaled).unsqueeze(1)
217
+
218
+ train_X = train_X[:len(train_y)]
219
+
220
+ dataset = torch.utils.data.TensorDataset(train_X, train_y)
221
+ loader = torch.utils.data.DataLoader(dataset, batch_size=32, shuffle=True)
222
+
223
+ for epoch in range(50):
224
+ for batch_X, batch_y in loader:
225
+ optimizer.zero_grad()
226
+ pi, sigma, mu = model(batch_X)
227
+ loss = mdn_loss(pi, sigma, mu, batch_y)
228
+ loss.backward()
229
+ optimizer.step()
230
+
231
+ GLOBAL_STATE["is_trained"] = True
232
+ log_buffer.append("✅ V4 Calibration Complete.")
233
+
234
+ # --- INFERENCE ---
235
+ model.eval()
236
+ with torch.no_grad():
237
+ pi, sigma, mu = model(X_tensor)
238
+
239
+ max_idx = torch.argmax(pi, dim=1)
240
+ pred_mu = mu[torch.arange(len(mu)), max_idx].numpy()
241
+ pred_sigma = sigma[torch.arange(len(sigma)), max_idx].numpy()
242
+
243
+ # Reconstruction
244
+ pred_ret = (pred_mu * ret_std) + ret_mean
245
+ pred_ret_sigma = pred_sigma * ret_std
246
+
247
+ prev_prices = ref_prices
248
+ pred_prices = prev_prices * (1 + pred_ret)
249
+ upper_band = prev_prices * (1 + pred_ret + (2 * pred_ret_sigma))
250
+ lower_band = prev_prices * (1 + pred_ret - (2 * pred_ret_sigma))
251
+
252
+ # Prepare Plot Data
253
+ dates = dates[1:]
254
+ plot_actual = ref_prices[1:]
255
+ plot_pred = pred_prices[:-1]
256
+ plot_upper = upper_band[:-1]
257
+ plot_lower = lower_band[:-1]
258
+ plot_sigma = pred_ret_sigma[:-1]
259
+
260
+ df = pd.DataFrame({
261
+ 'Close': plot_actual,
262
+ 'Pred': plot_pred,
263
+ 'Upper': plot_upper,
264
+ 'Lower': plot_lower,
265
+ 'Sigma': plot_sigma
266
+ }, index=dates)
267
+
268
+ # Z-Score
269
+ df['Gap'] = df['Pred'] - df['Close']
270
+ df['Price_Sigma'] = df['Close'] * df['Sigma']
271
+ df['Raw_Z'] = df['Gap'] / (df['Price_Sigma'] + 1e-9)
272
+ df['Rolling_Z'] = df['Raw_Z'] - df['Raw_Z'].rolling(window=50, min_periods=1).mean()
273
+
274
+ if len(df) > 0:
275
+ last_z = df['Rolling_Z'].iloc[-1]
276
+ last_price = df['Close'].iloc[-1]
277
+
278
+ status = "WAIT"
279
+ color = "gray"
280
+ if last_z > 1.8:
281
+ status = "BUY SIGNAL"
282
+ color = "green"
283
+ if GLOBAL_STATE["last_trade"] != "BUY":
284
+ send_ntfy(f"BUY EURUSD | Z: {last_z:.2f} | Price: {last_price}")
285
+ GLOBAL_STATE["last_trade"] = "BUY"
286
+ elif last_z < -1.8:
287
+ status = "SELL SIGNAL"
288
+ color = "red"
289
+ if GLOBAL_STATE["last_trade"] != "SELL":
290
+ send_ntfy(f"SELL EURUSD | Z: {last_z:.2f} | Price: {last_price}")
291
+ GLOBAL_STATE["last_trade"] = "SELL"
292
+
293
+ # PLOTTING (Dual Subplot)
294
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
295
+ vertical_spacing=0.1,
296
+ row_heights=[0.7, 0.3],
297
+ subplot_titles=("Holographic Price Cloud", "Market Divergence (Z-Score)"))
298
+
299
+ # Chart 1: Price
300
+ fig.add_trace(go.Scatter(x=df.index, y=df['Close'], mode='lines', name='Price', line=dict(color='rgba(255, 255, 255, 0.5)')), row=1, col=1)
301
+ fig.add_trace(go.Scatter(x=df.index, y=df['Upper'], mode='lines', line=dict(width=0), showlegend=False), row=1, col=1)
302
+ fig.add_trace(go.Scatter(x=df.index, y=df['Lower'], mode='lines', line=dict(width=0), fill='tonexty', fillcolor='rgba(0, 255, 255, 0.1)', name='Cloud'), row=1, col=1)
303
+ fig.add_trace(go.Scatter(x=df.index, y=df['Pred'], mode='lines', name='Transformer Path', line=dict(color='#00ffff', width=2)), row=1, col=1)
304
+
305
+ # Chart 2: Divergence
306
+ fig.add_trace(go.Bar(x=df.index, y=df['Rolling_Z'], name='Divergence', marker_color=df['Rolling_Z'].apply(lambda x: 'green' if x>0 else 'red')), row=2, col=1)
307
+ fig.add_hline(y=1.8, line_dash="dot", line_color="green", row=2, col=1)
308
+ fig.add_hline(y=-1.8, line_dash="dot", line_color="red", row=2, col=1)
309
+
310
+ fig.update_layout(template="plotly_dark", height=800, title=f"V4 Constellation: {status}")
311
+
312
+ info_html = f"""<div style="text-align: center; padding: 10px; background-color: {color}; color: white;"><h3>{status}</h3><p>Z: {last_z:.3f} | Price: {last_price}</p></div>"""
313
+ return fig, info_html, "\n".join(log_buffer)
314
+ else:
315
+ return None, "No Data", "Wait..."
316
+
317
+ # --- 5. UI ---
318
+ with gr.Blocks(title="Holographic FX V4", theme=gr.themes.Monochrome()) as app:
319
+ gr.Markdown("# 👁️ V4 Constellation Transformer (Multi-Pair)")
320
+ with gr.Row():
321
+ refresh_btn = gr.Button("🔄 Refresh / Scan Basket", variant="primary")
322
+ reset_btn = gr.Button("⚠️ HARD RESET", variant="stop")
323
+ with gr.Row(): status_box = gr.HTML()
324
+ plot = gr.Plot()
325
+ logs = gr.Textbox(label="Logs")
326
+
327
+ refresh_btn.click(fn=run_analysis, outputs=[plot, status_box, logs])
328
+ reset_btn.click(fn=hard_reset, outputs=[plot, status_box, logs])
329
+ app.load(fn=run_analysis, outputs=[plot, status_box, logs])
330
+
331
+ if __name__ == "__main__":
332
+ app.launch()