initial commit

This commit is contained in:
2026-05-26 13:51:02 +02:00
commit 4bf7394a0a
50 changed files with 7332 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
implementation.md
+176
View File
@@ -0,0 +1,176 @@
import logging
import time
import numpy as np
import torch
import torch.nn as nn
from config import config
from src.amd.optimizations import AMDOptimizer
from src.models.intraday_gnn import IntradayGNN
from src.utils.memory_manager import MemoryManager
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("benchmark_r9700.log"),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
def benchmark_model():
"""Benchmark the GNN model on AMD Radeon R9700 AI Pro"""
# Initialize memory manager
memory_manager = MemoryManager()
logger.info(memory_manager.get_memory_stats())
# Initialize AMD optimizer
amd_optimizer = AMDOptimizer()
# Create a sample model
num_features = len(config.INTRADAY_FEATURES) + 5 # +5 for price features
model = IntradayGNN(num_features, config.SEQUENCE_LENGTH)
# Optimize model for AMD GPU
model = amd_optimizer.optimize_model(model)
# Create sample data
batch_size = config.BATCH_SIZE
sequence_length = config.SEQUENCE_LENGTH
num_stocks = 50 # Number of stocks in the graph
# Create random data
x = torch.randn(num_stocks, sequence_length, num_features).to(config.DEVICE)
# Create random edges
num_edges = 200
edge_index = torch.randint(0, num_stocks, (2, num_edges)).to(config.DEVICE)
edge_attr = torch.randn(num_edges, 1).to(config.DEVICE)
# Create target
y = torch.randn(num_stocks, 1).to(config.DEVICE)
# Warm-up
logger.info("Warming up...")
for _ in range(10):
with torch.no_grad():
_ = model((x, edge_index, edge_attr))
# Benchmark inference
logger.info("Benchmarking inference...")
start_time = time.time()
num_runs = 100
for _ in range(num_runs):
with torch.no_grad():
_ = model((x, edge_index, edge_attr))
inference_time = (time.time() - start_time) / num_runs
logger.info(f"Average inference time: {inference_time:.6f} seconds")
# Benchmark training
logger.info("Benchmarking training...")
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=config.LEARNING_RATE)
criterion = nn.MSELoss()
start_time = time.time()
for _ in range(num_runs):
optimizer.zero_grad()
out = model((x, edge_index, edge_attr))
loss = criterion(out, y)
loss.backward()
optimizer.step()
training_time = (time.time() - start_time) / num_runs
logger.info(f"Average training time: {training_time:.6f} seconds")
# Memory usage
memory_info = memory_manager.check_memory()
logger.info(
f"GPU Memory Usage: {memory_info['allocated'] / 1024**3:.2f}GB / {memory_info['total'] / 1024**3:.2f}GB"
)
# Throughput
logger.info(f"Inference throughput: {1 / inference_time:.2f} samples/second")
logger.info(f"Training throughput: {1 / training_time:.2f} samples/second")
# Detailed benchmark with different batch sizes
logger.info("\nDetailed benchmark with different configurations:")
batch_sizes = [32, 64, 128, 256]
sequence_lengths = [30, 60, 120]
for batch_size in batch_sizes:
for seq_len in sequence_lengths:
# Create data for this configuration
x = torch.randn(num_stocks, seq_len, num_features).to(config.DEVICE)
edge_index = torch.randint(0, num_stocks, (2, num_edges)).to(config.DEVICE)
edge_attr = torch.randn(num_edges, 1).to(config.DEVICE)
y = torch.randn(num_stocks, 1).to(config.DEVICE)
# Benchmark inference
start_time = time.time()
for _ in range(10): # Fewer runs for detailed benchmark
with torch.no_grad():
_ = model((x, edge_index, edge_attr))
inf_time = (time.time() - start_time) / 10
# Benchmark training
start_time = time.time()
for _ in range(10):
optimizer.zero_grad()
out = model((x, edge_index, edge_attr))
loss = criterion(out, y)
loss.backward()
optimizer.step()
train_time = (time.time() - start_time) / 10
logger.info(
f"Batch: {batch_size}, Seq Len: {seq_len}, "
f"Inf Time: {inf_time:.6f}s, Train Time: {train_time:.6f}s, "
f"Inf Tput: {1 / inf_time:.2f} samples/s, Train Tput: {1 / train_time:.2f} samples/s"
)
# Memory benchmark
logger.info("\nMemory benchmark:")
# Test different model sizes
hidden_channels_list = [64, 128, 256, 512]
for hidden_channels in hidden_channels_list:
# Create a model with this configuration
model = IntradayGNN(num_features, config.SEQUENCE_LENGTH)
model.feature_processor = nn.Sequential(
nn.Linear(num_features, hidden_channels),
nn.SiLU(),
nn.Linear(hidden_channels, hidden_channels),
nn.LayerNorm(hidden_channels),
)
model.linear = nn.Linear(hidden_channels, 1)
# Optimize model
model = amd_optimizer.optimize_model(model)
# Estimate memory usage
estimated_memory = memory_manager.estimate_model_memory(model)
logger.info(
f"Hidden Channels: {hidden_channels}, Estimated Memory: {estimated_memory / 1024**3:.2f}GB"
)
# Clean up
del model
memory_manager.empty_cache()
# Final memory stats
logger.info("\nFinal Memory Stats:")
logger.info(memory_manager.get_memory_stats())
if __name__ == "__main__":
benchmark_model()
+196
View File
@@ -0,0 +1,196 @@
import os
from datetime import datetime, timedelta
import torch
class Config:
# Project settings
PROJECT_NAME = "StockGNN_R9700"
VERSION = "1.0.0"
# Data directories
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(BASE_DIR, "data")
RAW_DATA_DIR = os.path.join(DATA_DIR, "raw")
PROCESSED_DATA_DIR = os.path.join(DATA_DIR, "processed")
EXTERNAL_DATA_DIR = os.path.join(DATA_DIR, "external")
MODEL_DIR = os.path.join(BASE_DIR, "models")
# Ensure directories exist
os.makedirs(RAW_DATA_DIR, exist_ok=True)
os.makedirs(PROCESSED_DATA_DIR, exist_ok=True)
os.makedirs(EXTERNAL_DATA_DIR, exist_ok=True)
os.makedirs(MODEL_DIR, exist_ok=True)
# Stock universe settings
INITIAL_TICKERS = [
"AAPL",
"MSFT",
"GOOGL",
"AMZN",
"META",
"TSLA",
"NVDA",
"JPM",
"V",
"WMT",
"PG",
"DIS",
"NFLX",
"ADBE",
"PYPL",
"INTC",
"CSCO",
"PEP",
"KO",
"XOM",
"BAC",
"VZ",
"T",
"CRM",
"CMCSA",
"PFE",
"NKE",
"MRK",
"CVX",
"HD",
]
INDEX_TICKER = "^GSPC" # S&P 500
DELISTED_TICKERS_FILE = os.path.join(EXTERNAL_DATA_DIR, "delisted_stocks.csv")
# Date settings
START_DATE = "2015-01-01"
END_DATE = datetime.now().strftime("%Y-%m-%d")
TRAIN_END_DATE = "2022-12-31"
VAL_END_DATE = "2023-06-30"
TEST_END_DATE = END_DATE
# AMD GPU settings (Radeon R9700 AI Pro - 32GB)
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
AMD_GPU = True
GPU_MEMORY_LIMIT = 0.9 # Use 90% of 32GB = 28.8GB
ROCM_OPT_LEVEL = "O2" # Optimization level for ROCm ('O0', 'O1', 'O2')
PIN_MEMORY = True # Enable pinned memory for faster data transfer
MIXED_PRECISION = True # Enable mixed precision training
PRECISION = "bf16" # 'fp16' or 'bf16' for mixed precision
# Model settings
MODEL_NAME = "stock_gnn_r9700"
HIDDEN_CHANNELS = 128 # Increased for R9700's compute power
NUM_HEADS = 16 # Increased number of attention heads
DROPOUT = 0.3 # Reduced dropout for better GPU utilization
LEARNING_RATE = 0.0005 # Lower learning rate for stability
EPOCHS = 200 # More epochs with larger batch sizes
BATCH_SIZE = 128 # Larger batch size for R9700's memory
SEQUENCE_LENGTH = 60 # Longer sequences with more memory
PREDICTION_HORIZON = 10 # Number of steps to predict ahead
# Intraday trading settings
TRADING_FREQUENCY = "5min" # '1min', '5min', '15min', '30min', '1h'
TRADING_HOURS = {
"start": "09:30", # Market open (ET)
"end": "16:00", # Market close (ET)
}
PRE_MARKET_HOURS = {
"start": "04:00", # Pre-market start
"end": "09:30", # Pre-market end
}
AFTER_HOURS = {
"start": "16:00", # After-hours start
"end": "20:00", # After-hours end
}
MAX_POSITION_HOLD_TIME = "4h" # Maximum time to hold a position
MIN_POSITION_HOLD_TIME = "10min" # Minimum time to hold a position
MAX_DAILY_POSITIONS = 50 # Maximum number of positions per day
MAX_POSITION_SIZE = 0.03 # Maximum % of portfolio per position (3%)
# Data pipeline settings
LOOKBACK_WINDOW = 60 # Days for feature calculation
REALTIME_FEATURE_WINDOW = 30 # Number of data points for real-time features
DATA_BUFFER_SIZE = 5000 # Number of data points to keep in memory
DATA_FLUSH_INTERVAL = 300 # seconds - how often to flush data to database
# Alternative data settings
NEWS_API_KEY = "your_news_api_key"
TWITTER_BEARER_TOKEN = "your_twitter_bearer_token"
REDDIT_CLIENT_ID = "your_reddit_client_id"
REDDIT_CLIENT_SECRET = "your_reddit_client_secret"
NEWS_LOOKBACK_DAYS = 7 # Number of days to look back for news
SOCIAL_MEDIA_LOOKBACK_DAYS = 3 # Number of days to look back for social media
# Feature definitions (referenced by models)
NEWS_FEATURES = [
"sentiment",
"volume",
"recency",
"source_reliability",
"topic_relevance",
]
SOCIAL_FEATURES = [
"twitter_sentiment",
"twitter_volume",
"reddit_sentiment",
"reddit_volume",
"social_momentum",
]
INTRADAY_FEATURES = [
"return",
"volatility",
"momentum",
"volume_momentum",
"bid_ask_spread",
"bid_ask_spread_pct",
"volume_imbalance",
"order_flow",
"vwap_deviation",
]
# Stateful prediction
STATEFUL_PREDICTION = False
REALTIME_UPDATE_INTERVAL = 60 # seconds
# Backtesting settings
INITIAL_CAPITAL = 100000
TRANSACTION_COST = 0.0005 # 0.05% per trade
SLIPPAGE_MODEL = "volume_curve" # 'volume_curve', 'constant', or 'none'
SLIPPAGE_RATE = 0.0002 # 0.02% slippage
# Live trading settings
LIVE_DATA_ENABLED = True
DATA_PROVIDER = "polygon" # 'polygon', 'alphavantage', 'ib', 'tdameritrade'
POLYGON_API_KEY = "your_polygon_api_key"
ALPHA_VANTAGE_API_KEY = "your_alpha_vantage_api_key"
IB_HOST = "127.0.0.1"
IB_PORT = 7497
IB_CLIENT_ID = 1
# WebSocket settings
WEBSOCKET_RECONNECT_DELAY = 5 # seconds
WEBSOCKET_MAX_RETRIES = 20
WEBSOCKET_PING_INTERVAL = 30 # seconds
# Data loading settings
NUM_WORKERS = 8 # Number of data loading workers
PREFETCH_FACTOR = 4 # Number of batches to prefetch
# Online learning settings
ONLINE_LEARNING = True # Enable online learning
ONLINE_LEARNING_RATE = 0.0001 # Learning rate for online updates
ONLINE_LEARNING_INTERVAL = 3600 # seconds - how often to perform online learning
# Risk management settings
MAX_DAILY_LOSS = 0.01 # 1% max daily loss
MAX_DRAWDOWN = 0.05 # 5% max drawdown
VOLATILITY_TARGET = 0.15 # Annualized volatility target
POSITION_SIZING = (
"volatility_target" # 'volatility_target', 'equal_weight', 'kelly'
)
# Execution settings
EXECUTION_ALGORITHM = "vwap" # 'vwap', 'twap', 'pov', 'implementation_shortfall'
EXECUTION_TIME_HORIZON = "5min" # Time to complete execution
MARKET_IMPACT_MODEL = "kyle" # 'kyle', 'almgren_chriss', or 'none'
config = Config()
View File
View File
View File
+569
View File
@@ -0,0 +1,569 @@
import asyncio
import logging
import time
from datetime import datetime, timedelta
from typing import Dict, List
import torch
from src.trading.paper_broker import PaperTradingBroker
from src.trading.real_time_trader import RealTimeTrader
from config import config
from src.amd.optimizations import AMDOptimizer
from src.data.live_data import LiveDataService
from src.data.pipeline import StockDataPipeline
from src.models.intraday_gnn import IntradayGNN
from src.models.trainer import GNNTrainer
from src.utils.memory_manager import MemoryManager
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("live_trading_r9700.log"),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
class LiveTradingSystem:
def __init__(self):
# Initialize memory manager
self.memory_manager = MemoryManager()
logger.info(self.memory_manager.get_memory_stats())
# Initialize AMD optimizer
self.amd_optimizer = AMDOptimizer()
# Initialize components
self.pipeline = StockDataPipeline()
self.broker = PaperTradingBroker(initial_cash=config.INITIAL_CAPITAL)
# Load model with memory management
self._initialize_model()
# Initialize trader
self.trader = RealTimeTrader(self.model, self.pipeline, self.broker)
# Initialize live data service
self.live_data_service = LiveDataService(self.pipeline, self._on_market_data)
# Trading state
self.last_trade_time = None
self.last_model_update_time = time.time()
def _initialize_model(self):
"""Initialize the GNN model with AMD optimizations"""
# Check memory before loading model
if not self.memory_manager.ensure_memory(8 * 1024**3): # 8GB
logger.warning("Not enough GPU memory for model. Falling back to CPU.")
config.DEVICE = "cpu"
# In a real implementation, you would load a pre-trained model
# For this example, we'll create a new model with the right number of features
# Get number of features (this would be determined from your actual data)
# For this example, we'll use a reasonable estimate
num_features = len(config.INTRADAY_FEATURES) + 5 # +5 for price features
self.model = IntradayGNN(num_features, config.SEQUENCE_LENGTH)
self.trainer = GNNTrainer(self.model)
# Load pre-trained weights if available
try:
self.trainer.load_model()
logger.info("Loaded pre-trained model")
except Exception as e:
logger.warning(f"Could not load pre-trained model: {str(e)}")
logger.info("Using randomly initialized model")
# Optimize model for AMD GPU
self.model = self.amd_optimizer.optimize_model(self.model)
# Log memory after loading model
self.memory_manager.log_memory_usage("[After Model Load]")
async def _on_market_data(
self, ticker: str, timestamp: str, data_type: str, data: Dict
):
"""Callback for market data updates with memory management"""
# Check memory before processing
if not self.memory_manager.ensure_memory(500 * 1024**2): # 500MB
logger.warning("Skipping data processing due to memory constraints")
return
# Check if we should generate trading signals
if not self._should_generate_signals(timestamp):
return
# Get current data for all tickers
current_data = self._prepare_current_data(timestamp)
if not current_data:
return
# Generate trading signals
await self._generate_trading_signals(current_data, timestamp)
# Periodically update the model with online learning
if (
config.ONLINE_LEARNING
and time.time() - self.last_model_update_time
> config.ONLINE_LEARNING_INTERVAL
):
await self._online_learning(timestamp)
self.last_model_update_time = time.time()
# Memory management
self.memory_manager.auto_manage_memory(threshold=0.8)
def _should_generate_signals(self, timestamp: str) -> bool:
"""Determine if we should generate trading signals"""
current_time = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").time()
# Check if market is open
market_open = datetime.strptime(config.TRADING_HOURS["start"], "%H:%M").time()
market_close = datetime.strptime(config.TRADING_HOURS["end"], "%H:%M").time()
if not (market_open <= current_time <= market_close):
return False
# Check if it's time to generate signals based on trading frequency
if config.TRADING_FREQUENCY == "1min":
return True
elif config.TRADING_FREQUENCY == "5min":
return current_time.minute % 5 == 0
elif config.TRADING_FREQUENCY == "15min":
return current_time.minute % 15 == 0
else: # Default to 1 minute
return True
def _prepare_current_data(self, timestamp: str) -> Dict:
"""Prepare current data for prediction with memory management"""
from torch_geometric.data import Data
current_date = timestamp.split(" ")[0]
tickers = config.INITIAL_TICKERS
# Get sequence data for each ticker
sequence_features = []
valid_tickers = []
for ticker in tickers:
# Check memory before processing ticker
if not self.memory_manager.ensure_memory(50 * 1024**2): # 50MB
logger.warning(f"Skipping {ticker} due to memory constraints")
continue
try:
# Get features for the ticker
features = self.pipeline.get_latest_features([ticker], timestamp)
if ticker not in features:
continue
# Get sequence of features
timestamps = self._generate_timestamps_for_date(current_date)
current_idx = (
timestamps.index(timestamp)
if timestamp in timestamps
else len(timestamps) - 1
)
sequence_start = timestamps[
max(0, current_idx - config.SEQUENCE_LENGTH + 1)
]
# Get features for the sequence
sequence_data = self._get_feature_sequence(
ticker, sequence_start, timestamp
)
if len(sequence_data) < config.SEQUENCE_LENGTH:
# Pad with zeros if sequence is too short
padding = np.zeros(
(
config.SEQUENCE_LENGTH - len(sequence_data),
sequence_data.shape[1],
)
)
sequence_data = np.vstack([padding, sequence_data])
sequence_features.append(sequence_data)
valid_tickers.append(ticker)
except Exception as e:
logger.warning(f"Error processing {ticker} for {timestamp}: {str(e)}")
continue
if not sequence_features:
return None
# Convert to tensor (num_stocks, sequence_length, num_features)
x = torch.tensor(np.array(sequence_features), dtype=torch.float32)
# Create edges based on sector relationships
edge_index = []
edge_weight = []
for i, ticker1 in enumerate(valid_tickers):
for j, ticker2 in enumerate(valid_tickers):
if i < j:
# Check memory before processing edge
if not self.memory_manager.ensure_memory(1 * 1024**2): # 1MB
logger.warning(
"Skipping edge creation due to memory constraints"
)
continue
try:
# Get sector relationship
pit1 = self.pipeline.get_point_in_time_data(
ticker1, datetime.strptime(current_date, "%Y-%m-%d")
)
pit2 = self.pipeline.get_point_in_time_data(
ticker2, datetime.strptime(current_date, "%Y-%m-%d")
)
if (
pit1["sector"]
and pit2["sector"]
and pit1["sector"] == pit2["sector"]
):
# Calculate correlation of recent returns
lookback_start = datetime.strptime(
sequence_start, "%Y-%m-%d %H:%M:%S"
)
lookback_end = datetime.strptime(
timestamp, "%Y-%m-%d %H:%M:%S"
)
returns1 = self._get_returns_sequence(
ticker1, lookback_start, lookback_end
)
returns2 = self._get_returns_sequence(
ticker2, lookback_start, lookback_end
)
if len(returns1) > 5 and len(returns2) > 5:
corr = np.corrcoef(returns1, returns2)[0, 1]
if not np.isnan(corr):
edge_index.append([i, j])
edge_weight.append(corr)
except Exception as e:
logger.warning(
f"Error creating edge between {ticker1} and {ticker2}: {str(e)}"
)
continue
# Convert to tensors
edge_index = (
torch.tensor(edge_index, dtype=torch.long).t()
if edge_index
else torch.empty((2, 0), dtype=torch.long)
)
edge_weight = (
torch.tensor(edge_weight, dtype=torch.float32).unsqueeze(1)
if edge_weight
else torch.empty((0, 1), dtype=torch.float32)
)
# Create Data object
data = Data(x=x, edge_index=edge_index, edge_attr=edge_weight)
data.timestamp = timestamp
data.date = current_date
data.tickers = valid_tickers
return data
def _generate_timestamps_for_date(self, date: str) -> List[str]:
"""Generate timestamps for a given date"""
# Get market open and close times
market_open = datetime.strptime(config.TRADING_HOURS["start"], "%H:%M").time()
market_close = datetime.strptime(config.TRADING_HOURS["end"], "%H:%M").time()
# Create datetime objects for open and close
open_datetime = datetime.strptime(f"{date} {market_open}", "%Y-%m-%d %H:%M:%S")
close_datetime = datetime.strptime(
f"{date} {market_close}", "%Y-%m-%d %H:%M:%S"
)
# Generate timestamps based on trading frequency
if config.TRADING_FREQUENCY == "1min":
delta = timedelta(minutes=1)
elif config.TRADING_FREQUENCY == "5min":
delta = timedelta(minutes=5)
elif config.TRADING_FREQUENCY == "15min":
delta = timedelta(minutes=15)
elif config.TRADING_FREQUENCY == "30min":
delta = timedelta(minutes=30)
elif config.TRADING_FREQUENCY == "1h":
delta = timedelta(hours=1)
else: # Default to 1 minute
delta = timedelta(minutes=1)
timestamps = []
current = open_datetime
while current <= close_datetime:
timestamps.append(current.strftime("%Y-%m-%d %H:%M:%S"))
current += delta
return timestamps
def _get_feature_sequence(self, ticker: str, start: str, end: str):
"""Get feature sequence for a ticker between start and end timestamps"""
if (
ticker not in self.pipeline.intraday_data
or "features" not in self.pipeline.intraday_data[ticker]
):
return np.array([])
features = self.pipeline.intraday_data[ticker]["features"]
sequence = features.loc[start:end].values
return sequence
def _get_returns_sequence(
self, ticker: str, start: datetime, end: datetime
) -> List[float]:
"""Get returns sequence for a ticker between start and end times"""
if (
ticker not in self.pipeline.intraday_data
or "price_bars" not in self.pipeline.intraday_data[ticker]
):
return []
price_bars = self.pipeline.intraday_data[ticker]["price_bars"]
sequence = price_bars.loc[
start.strftime("%Y-%m-%d %H:%M:%S") : end.strftime("%Y-%m-%d %H:%M:%S")
]
if len(sequence) < 2:
return []
returns = sequence["close"].pct_change().dropna().values
return returns.tolist()
async def _generate_trading_signals(self, data: Dict, timestamp: str):
"""Generate trading signals based on model predictions with memory management"""
if not data:
return
# Check memory before prediction
if not self.memory_manager.ensure_memory(2 * 1024**3): # 2GB
logger.warning("Skipping prediction due to memory constraints")
return
try:
# Get predictions from model
with torch.no_grad():
data = data.to(config.DEVICE)
predictions = self.model(data).squeeze().cpu().numpy()
# Generate signals for each ticker
for i, ticker in enumerate(data.tickers):
prediction = predictions[i]
# Get current price
current_price = None
latest_data = self.live_data_service.get_latest_data(ticker)
if "price_bar" in latest_data:
current_price = latest_data["price_bar"]["close"]
if not current_price:
continue
# Skip if we already have a position in this stock
if ticker in self.trader.current_positions:
continue
# Check holding period constraints
if self.trader._check_holding_period(ticker, timestamp):
continue
# Generate signals based on prediction
if prediction > 0.002: # Buy signal
# Calculate position size
position_size = self.trader._calculate_position_size(
ticker, current_price
)
if position_size > 0:
# Create buy order
order = {
"ticker": ticker,
"action": "buy",
"quantity": position_size,
"price": current_price,
"timestamp": timestamp,
"type": "market",
}
# Submit order
order_id = self.broker.submit_order(order)
if order_id:
self.trader.pending_orders[order_id] = order
logger.info(
f"Submitted buy order for {position_size} shares of {ticker} at {current_price}"
)
elif (
prediction < -0.002 and ticker in self.trader.current_positions
): # Sell signal
# Create sell order
order = {
"ticker": ticker,
"action": "sell",
"quantity": self.trader.current_positions[ticker],
"price": current_price,
"timestamp": timestamp,
"type": "market",
}
# Submit order
order_id = self.broker.submit_order(order)
if order_id:
self.trader.pending_orders[order_id] = order
logger.info(
f"Submitted sell order for {self.trader.current_positions[ticker]} shares of {ticker} at {current_price}"
)
except Exception as e:
logger.error(f"Error generating trading signals: {str(e)}", exc_info=True)
self.memory_manager.empty_cache()
async def _online_learning(self, timestamp: str):
"""Perform online learning with new data and memory management"""
current_date = timestamp.split(" ")[0]
tickers = config.INITIAL_TICKERS
# Create dataset for online learning
dataset = self.pipeline.create_intraday_dataset(
tickers, current_date, current_date
)
if not dataset:
return
# Check memory before online learning
if not self.memory_manager.ensure_memory(4 * 1024**3): # 4GB
logger.warning("Skipping online learning due to memory constraints")
return
try:
# Get the most recent data point
recent_data = dataset[-1]
# Perform online update
loss = self.trainer.online_update(recent_data)
if loss is not None:
logger.info(f"Online learning update - Loss: {loss:.6f}")
except Exception as e:
logger.error(f"Error during online learning: {str(e)}", exc_info=True)
self.memory_manager.empty_cache()
async def run(self):
"""Run the live trading system with AMD optimizations"""
logger.info(
"Starting live trading system with AMD Radeon R9700 AI Pro optimizations"
)
# Start live data service
data_task = asyncio.create_task(self.live_data_service.start())
# Start trader
trader_task = asyncio.create_task(self._run_trader())
# Start memory monitor
memory_task = asyncio.create_task(self._monitor_memory())
# Wait for tasks to complete
await asyncio.gather(data_task, trader_task, memory_task)
async def _run_trader(self):
"""Run the trader component with memory management"""
while True:
try:
# Check memory before processing
if not self.memory_manager.ensure_memory(1 * 1024**3): # 1GB
logger.warning(
"Skipping trader processing due to memory constraints"
)
await asyncio.sleep(5)
continue
# Process pending orders
self.trader._process_pending_orders()
# Update portfolio value
self.trader._update_portfolio_value()
# Check risk limits
if self.trader._check_risk_limits():
logger.info("Risk limits exceeded. Stopping trading for the day.")
self.trader._close_all_positions()
# Sleep for a short interval
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error in trader: {str(e)}", exc_info=True)
self.memory_manager.empty_cache()
await asyncio.sleep(5)
async def _monitor_memory(self):
"""Monitor GPU memory usage"""
while True:
try:
memory_info = self.memory_manager.check_memory()
if memory_info["usage_percent"] > 85:
logger.info(
f"High GPU memory usage: {memory_info['usage_percent']:.2f}%. Clearing cache."
)
self.memory_manager.empty_cache()
await asyncio.sleep(60) # Check every minute
except Exception as e:
logger.error(f"Error in memory monitor: {str(e)}")
await asyncio.sleep(60)
async def stop(self):
"""Stop the live trading system"""
logger.info("Stopping live trading system")
# Stop live data service
await self.live_data_service.stop()
# Stop trader
self.trader._close_all_positions()
# Clear memory
self.memory_manager.empty_cache()
logger.info("Live trading system stopped")
logger.info(self.memory_manager.get_memory_stats())
async def main():
# Initialize live trading system
trading_system = LiveTradingSystem()
try:
# Run the system
await trading_system.run()
except KeyboardInterrupt:
logger.info("Received keyboard interrupt. Shutting down...")
except Exception as e:
logger.error(f"Error in live trading system: {str(e)}", exc_info=True)
finally:
# Clean up
await trading_system.stop()
logger.info("Live trading system shutdown complete")
if __name__ == "__main__":
asyncio.run(main())
+156
View File
@@ -0,0 +1,156 @@
import logging
from datetime import datetime
import matplotlib.pyplot as plt
import pandas as pd
from src.evaluation.backtester import GNNBacktester
from src.evaluation.metrics import calculate_performance_metrics, compare_to_benchmark
from src.utils.visualization import (
plot_feature_importance,
plot_performance,
plot_trade_log,
)
from config import config
from src.amd.optimizations import AMDOptimizer
from src.data.pipeline import StockDataPipeline
from src.models.gnn_model import CorporateActionAwareGNN
from src.models.trainer import GNNTrainer
from src.utils.memory_manager import MemoryManager
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("stock_gnn_r9700.log"),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
def main():
# Initialize memory manager
memory_manager = MemoryManager()
logger.info(memory_manager.get_memory_stats())
# Initialize AMD optimizer
amd_optimizer = AMDOptimizer()
# Initialize data pipeline
logger.info("Initializing data pipeline")
pipeline = StockDataPipeline()
# Update all data
logger.info("Updating all data sources")
pipeline.update_all_data()
# Create datasets
logger.info("Creating training and validation datasets")
train_dataset = pipeline.create_training_dataset()
val_dataset = pipeline.create_validation_dataset(
start_date=config.TRAIN_END_DATE, end_date=config.VAL_END_DATE
)
logger.info(f"Training dataset size: {len(train_dataset)}")
logger.info(f"Validation dataset size: {len(val_dataset)}")
# Initialize model
logger.info("Initializing GNN model")
# Get number of features from first data point
num_features = train_dataset[0].x.shape[1]
model = CorporateActionAwareGNN(num_features)
# Optimize model for AMD GPU
model = amd_optimizer.optimize_model(model)
# Train model
logger.info("Training model with AMD optimizations")
trainer = GNNTrainer(model)
train_losses, val_losses = trainer.train(train_dataset, val_dataset)
# Plot training curves
plt.figure(figsize=(10, 5))
plt.plot(train_losses, label="Training Loss")
plt.plot(val_losses, label="Validation Loss")
plt.title("Training and Validation Loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.savefig("training_curves.png")
plt.close()
# Benchmark model
logger.info("Benchmarking model performance")
sample_data = train_dataset[0].to(config.DEVICE)
benchmark_results = trainer.benchmark(sample_data)
logger.info(f"Benchmark Results: {benchmark_results}")
# Load best model
trainer.load_model()
# Run backtest on validation set
logger.info("Running backtest on validation set")
backtester = GNNBacktester(model, pipeline)
portfolio_values, trade_log = backtester.run_backtest(val_dataset)
# Get benchmark data
benchmark_data = pipeline.price_data[config.INDEX_TICKER]
benchmark_values = benchmark_data.loc[portfolio_values.index]["Adj Close"]
# Calculate performance metrics
logger.info("Calculating performance metrics")
portfolio_returns = portfolio_values.pct_change().dropna()
benchmark_returns = benchmark_values.pct_change().dropna()
metrics = calculate_performance_metrics(portfolio_returns, benchmark_returns)
comparison = compare_to_benchmark(portfolio_values, benchmark_values)
# Print metrics
logger.info("\nPerformance Metrics:")
for metric, value in metrics.items():
if isinstance(value, float):
logger.info(f"{metric.replace('_', ' ').title()}: {value:.4f}")
else:
logger.info(f"{metric.replace('_', ' ').title()}: {value}")
logger.info("\nComparison to Benchmark:")
for metric, value in comparison.items():
if isinstance(value, float):
logger.info(f"{metric.replace('_', ' ').title()}: {value:.4f}")
else:
logger.info(f"{metric.replace('_', ' ').title()}: {value}")
# Plot performance
plot_performance(portfolio_values, benchmark_values, "portfolio_performance.png")
# Plot trade log
plot_trade_log(trade_log, "trade_log.png")
# Save results
results = {
"portfolio_values": portfolio_values,
"benchmark_values": benchmark_values,
"trade_log": trade_log,
"metrics": metrics,
"comparison": comparison,
}
results_df = pd.DataFrame(
{
"date": portfolio_values.index,
"portfolio_value": portfolio_values.values,
"benchmark_value": benchmark_values.values,
}
)
results_df.to_csv("backtest_results.csv", index=False)
# Memory cleanup
memory_manager.empty_cache()
logger.info("Backtest completed. Results saved to backtest_results.csv")
logger.info(memory_manager.get_memory_stats())
if __name__ == "__main__":
main()
View File
View File
+58
View File
@@ -0,0 +1,58 @@
# Core requirements
numpy>=1.24.0
pandas>=2.0.0
scipy>=1.10.0
scikit-learn>=1.2.0
tqdm>=4.65.0
matplotlib>=3.7.0
seaborn>=0.12.0
python-dateutil>=2.8.0
pytz>=2023.3
requests>=2.28.0
websockets>=11.0
SQLAlchemy>=2.0.0
sqlite3>=3.40.0 # Part of Python standard library
# AMD GPU support for PyTorch (ROCm 5.6)
torch>=2.1.0 # ROCm-compatible version
torchvision>=0.16.0 # ROCm-compatible version
torchaudio>=2.1.0 # ROCm-compatible version
--index-url https://download.pytorch.org/whl/rocm5.6
# PyTorch Geometric with AMD support
torch-geometric>=2.4.0
torch-scatter>=2.1.2
torch-sparse>=0.6.18
torch-cluster>=1.6.2
torch-spline-conv>=1.2.2
--find-links https://data.pyg.org/whl/torch-2.1.0+rocm5.6.html
# ROCm libraries for performance
rocblas>=3.1.0
hipblaslt>=0.6.0
miopen-hip>=2.19.0
rccl>=2.15.5
# Alternative data processing
yfinance>=0.2.20
alpha_vantage>=2.3.1
polygon-api-client>=1.12.0
tweepy>=4.14.0
praw>=7.7.0
newspaper3k>=0.2.8
transformers>=4.30.0
sentencepiece>=0.1.99
# Interactive Brokers
ib_insync>=0.9.86
# For AMD-specific optimizations
tensorboard>=2.13.0
psutil>=5.9.0
# Web frontend
fastapi>=0.104.0
uvicorn[standard]>=0.24.0
jinja2>=3.1.0
python-multipart>=0.0.6
aiofiles>=23.2.0
+1
View File
@@ -0,0 +1 @@
# src package
+1
View File
@@ -0,0 +1 @@
# AMD optimizations package
+349
View File
@@ -0,0 +1,349 @@
import logging
import time
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import MessagePassing
from torch_geometric.utils import softmax
from config import config
logger = logging.getLogger(__name__)
class AMDOptimizer:
"""
AMD-specific optimizations for PyTorch models
"""
def __init__(self):
self.device = torch.device(config.DEVICE)
self._configure_rocm()
def _configure_rocm(self):
"""Configure ROCm for optimal performance"""
if config.DEVICE == "cuda" and config.AMD_GPU:
try:
# Set ROCm optimization level
torch.backends.hip.set_optimization_level(config.ROCM_OPT_LEVEL)
# Enable memory efficient attention if available
try:
from xformers.ops import MemoryEfficientAttentionFlashAttentionOp
torch.backends.cuda.enable_flash_sdp(True)
logger.info("Enabled Flash Attention for AMD GPU")
except ImportError:
logger.warning("xformers not available, using standard attention")
# Configure memory limits
total_memory = torch.cuda.get_device_properties(0).total_memory
memory_limit = int(total_memory * config.GPU_MEMORY_LIMIT)
torch.cuda.set_per_process_memory_fraction(config.GPU_MEMORY_LIMIT, 0)
logger.info(
f"Configured ROCm with optimization level {config.ROCM_OPT_LEVEL}"
)
logger.info(
f"GPU Memory: {total_memory / 1024**3:.2f}GB, Limit: {memory_limit / 1024**3:.2f}GB"
)
except Exception as e:
logger.error(f"Error configuring ROCm: {str(e)}")
def optimize_model(self, model: nn.Module):
"""Apply AMD-specific optimizations to a model"""
if config.DEVICE != "cuda" or not config.AMD_GPU:
return model
try:
# Move model to GPU
model = model.to(self.device)
# Apply mixed precision if enabled
if config.MIXED_PRECISION:
model = self._apply_mixed_precision(model)
# Apply memory optimizations
model = self._apply_memory_optimizations(model)
logger.info("Applied AMD optimizations to model")
return model
except Exception as e:
logger.error(f"Error optimizing model: {str(e)}")
return model.to(self.device)
def _apply_mixed_precision(self, model: nn.Module):
"""Apply mixed precision training to the model"""
# Convert model to use mixed precision
if config.PRECISION == "fp16":
model = model.half()
elif config.PRECISION == "bf16":
model = model.to(torch.bfloat16)
# Convert specific layers to full precision if needed
for name, module in model.named_modules():
if isinstance(module, (nn.LayerNorm, nn.BatchNorm2d)):
module = module.float()
logger.info(f"Applied mixed precision training with {config.PRECISION}")
return model
def _apply_memory_optimizations(self, model: nn.Module):
"""Apply memory optimizations to the model"""
# Enable gradient checkpointing for memory efficiency
if (
hasattr(model, "supports_gradient_checkpointing")
and model.supports_gradient_checkpointing
):
model.gradient_checkpointing_enable()
logger.info("Enabled gradient checkpointing")
# Apply activation checkpointing to specific modules
for name, module in model.named_modules():
if isinstance(module, (nn.LSTM, nn.GRU)):
module.activation_checkpointing = True
return model
def get_precision_dtype(self):
"""Get the precision dtype for mixed precision training"""
if config.PRECISION == "fp16":
return torch.float16
elif config.PRECISION == "bf16":
return torch.bfloat16
else:
return torch.float32
def benchmark_model(self, model: nn.Module, input_data, num_runs: int = 100):
"""Benchmark model performance on AMD GPU"""
if config.DEVICE != "cuda":
logger.warning("Benchmarking only supported on GPU")
return {}
try:
# Warm up
for _ in range(10):
_ = model(input_data)
# Benchmark inference
start_time = time.time()
for _ in range(num_runs):
with torch.no_grad():
_ = model(input_data)
inference_time = (time.time() - start_time) / num_runs
# Benchmark training
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=config.LEARNING_RATE)
criterion = nn.MSELoss()
start_time = time.time()
for _ in range(num_runs):
optimizer.zero_grad()
out = model(input_data)
loss = criterion(out, torch.randn_like(out))
loss.backward()
optimizer.step()
training_time = (time.time() - start_time) / num_runs
# Memory usage
memory_allocated = torch.cuda.memory_allocated(0)
max_memory = torch.cuda.max_memory_allocated(0)
return {
"inference_time": inference_time,
"training_time": training_time,
"throughput_inference": 1 / inference_time,
"throughput_training": 1 / training_time,
"memory_allocated": memory_allocated,
"max_memory": max_memory,
"memory_usage_percent": (
memory_allocated / torch.cuda.get_device_properties(0).total_memory
)
* 100,
}
except Exception as e:
logger.error(f"Error benchmarking model: {str(e)}")
return {}
class AMDSparseAttention(nn.Module):
"""
Sparse attention implementation optimized for AMD GPUs
"""
def __init__(self, embed_dim, num_heads, dropout=0.1):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.scaling = self.head_dim**-0.5
self.qkv_proj = nn.Linear(embed_dim, embed_dim * 3)
self.out_proj = nn.Linear(embed_dim, embed_dim)
self.dropout = nn.Dropout(dropout)
# Initialize weights
self._init_weights()
def _init_weights(self):
nn.init.xavier_uniform_(self.qkv_proj.weight)
nn.init.xavier_uniform_(self.out_proj.weight)
nn.init.zeros_(self.qkv_proj.bias)
nn.init.zeros_(self.out_proj.bias)
def forward(self, x, mask=None):
batch_size, seq_len, embed_dim = x.size()
# Project queries, keys, values
qkv = self.qkv_proj(x)
q, k, v = qkv.chunk(3, dim=-1)
# Reshape for multi-head attention
q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# Compute attention scores
attn_scores = torch.matmul(q, k.transpose(-2, -1)) * self.scaling
# Apply mask if provided
if mask is not None:
attn_scores = attn_scores.masked_fill(mask == 0, float("-inf"))
# Compute attention weights
attn_weights = F.softmax(attn_scores, dim=-1)
attn_weights = self.dropout(attn_weights)
# Apply attention to values
output = torch.matmul(attn_weights, v)
# Concatenate heads
output = (
output.transpose(1, 2).contiguous().view(batch_size, seq_len, embed_dim)
)
# Final projection
output = self.out_proj(output)
return output
class AMDGATConv(MessagePassing):
"""
GATConv implementation optimized for AMD GPUs
"""
def __init__(
self,
in_channels,
out_channels,
heads=1,
concat=True,
dropout=0.6,
add_self_loops=True,
):
super().__init__(aggr="add", node_dim=0)
self.in_channels = in_channels
self.out_channels = out_channels
self.heads = heads
self.concat = concat
self.dropout = dropout
self.add_self_loops = add_self_loops
# Linear transformations for each head
self.lin_src = nn.Parameter(torch.Tensor(in_channels, heads * out_channels))
self.lin_dst = nn.Parameter(torch.Tensor(in_channels, heads * out_channels))
# Attention parameters
self.att_src = nn.Parameter(torch.Tensor(1, heads, out_channels))
self.att_dst = nn.Parameter(torch.Tensor(1, heads, out_channels))
# Bias
self.bias = nn.Parameter(torch.Tensor(heads * out_channels))
# Initialize weights
self.reset_parameters()
def reset_parameters(self):
nn.init.xavier_uniform_(self.lin_src)
nn.init.xavier_uniform_(self.lin_dst)
nn.init.xavier_uniform_(self.att_src)
nn.init.xavier_uniform_(self.att_dst)
nn.init.zeros_(self.bias)
def forward(self, x, edge_index, edge_attr=None, size=None):
# Linear transformation
if size is None and torch.is_tensor(x):
x_src = x_dst = torch.matmul(x, self.lin_src).view(
-1, self.heads, self.out_channels
)
else:
x_src, x_dst = x[0], x[1]
x_src = torch.matmul(x_src, self.lin_src).view(
-1, self.heads, self.out_channels
)
x_dst = torch.matmul(x_dst, self.lin_dst).view(
-1, self.heads, self.out_channels
)
# Add self loops if needed
if self.add_self_loops:
num_nodes = x_src.size(0)
edge_index, edge_attr = self._add_self_loops(
edge_index, edge_attr, num_nodes
)
# Compute attention coefficients
alpha_src = (x_src * self.att_src).sum(dim=-1)
alpha_dst = (x_dst * self.att_dst).sum(dim=-1)
alpha = (alpha_src, alpha_dst)
# Propagate
out = self.propagate(edge_index, x=(x_src, x_dst), alpha=alpha, size=size)
# Concatenate or average heads
if self.concat:
out = out.view(-1, self.heads * self.out_channels)
else:
out = out.mean(dim=1)
# Add bias
out = out + self.bias
return out
def _add_self_loops(self, edge_index, edge_attr, num_nodes):
# Add self loops to edge_index
loop_index = torch.arange(
0, num_nodes, dtype=torch.long, device=edge_index.device
)
loop_index = loop_index.unsqueeze(0).repeat(2, 1)
if edge_attr is not None:
loop_attr = edge_attr.new_zeros((num_nodes,) + edge_attr.size()[1:])
edge_attr = torch.cat([edge_attr, loop_attr], dim=0)
edge_index = torch.cat([edge_index, loop_index], dim=1)
return edge_index, edge_attr
def propagate(self, edge_index, size=None, **kwargs):
return super().propagate(edge_index, size=size, **kwargs)
def message(self, x_j, alpha_j, alpha_i, index, ptr, size_i):
# Compute attention weights
alpha = alpha_j + alpha_i
alpha = F.leaky_relu(alpha, negative_slope=0.2)
alpha = self._softmax(alpha, index, ptr, size_i)
alpha = F.dropout(alpha, p=self.dropout, training=self.training)
# Weighted sum of values
return x_j * alpha.unsqueeze(-1)
def _softmax(self, src, index, ptr, num_nodes):
# Memory-efficient softmax
return softmax(src, index, ptr, num_nodes)
View File
+908
View File
@@ -0,0 +1,908 @@
import asyncio
import json
import logging
import time
from datetime import datetime, timedelta
from typing import Callable, Dict, List, Optional
import numpy as np
import pandas as pd
import torch
import websockets
from config import config
from src.data.pipeline import StockDataPipeline
from src.utils.helpers import generate_intraday_timestamps
from src.utils.memory_manager import MemoryManager
logger = logging.getLogger(__name__)
class LiveDataService:
def __init__(
self, pipeline: StockDataPipeline, on_data_callback: Optional[Callable] = None
):
self.pipeline = pipeline
self.on_data_callback = on_data_callback
self.memory_manager = MemoryManager()
# Data buffers
self.data_buffer = {} # {ticker: {timestamp: data}}
self.price_bars = {} # {ticker: DataFrame}
self.order_book = {} # {ticker: DataFrame}
self.trades = {} # {ticker: DataFrame}
self.features = {} # {ticker: DataFrame}
# Initialize data buffers
for ticker in config.INITIAL_TICKERS:
self.data_buffer[ticker] = {}
self.price_bars[ticker] = pd.DataFrame()
self.order_book[ticker] = pd.DataFrame()
self.trades[ticker] = pd.DataFrame()
self.features[ticker] = pd.DataFrame()
# WebSocket connection
self.websocket = None
self.running = False
self.reconnect_attempts = 0
self.last_flush_time = time.time()
self.last_feature_update_time = time.time()
# Subscriptions
self.subscribed_tickers = set(config.INITIAL_TICKERS)
# Initialize database connection pool
self.db_pool = None
self._init_db_pool()
def _init_db_pool(self):
"""Initialize database connection pool for better performance"""
try:
import sqlite3
self.db_pool = sqlite3.connect(
self.pipeline.db_path, check_same_thread=False
)
self.db_pool.execute("PRAGMA journal_mode=WAL")
self.db_pool.execute("PRAGMA cache_size=-10000") # 10MB cache
logger.info("Initialized database connection pool")
except Exception as e:
logger.error(f"Error initializing database pool: {str(e)}")
self.db_pool = None
async def start(self):
"""Start the live data service with AMD optimizations"""
self.running = True
logger.info("Starting live data service with AMD optimizations")
# Start memory monitor
self.memory_manager.monitor_memory(interval=60)
while self.running:
try:
if config.DATA_PROVIDER == "polygon":
await self._connect_polygon()
elif config.DATA_PROVIDER == "alphavantage":
await self._connect_alpha_vantage()
elif config.DATA_PROVIDER == "ib":
await self._connect_interactive_brokers()
else:
logger.error(f"Unsupported data provider: {config.DATA_PROVIDER}")
return
except Exception as e:
logger.error(f"Error in live data service: {str(e)}", exc_info=True)
await self._handle_disconnect()
if self.reconnect_attempts < config.WEBSOCKET_MAX_RETRIES:
await asyncio.sleep(config.WEBSOCKET_RECONNECT_DELAY)
else:
logger.error(
"Max reconnection attempts reached. Stopping live data service."
)
self.running = False
async def stop(self):
"""Stop the live data service"""
self.running = False
if self.websocket:
await self.websocket.close()
if self.db_pool:
self.db_pool.close()
logger.info("Live data service stopped")
async def _connect_polygon(self):
"""Connect to Polygon.io WebSocket with AMD optimizations"""
uri = f"wss://socket.polygon.io/stocks"
async with websockets.connect(
uri, ping_interval=config.WEBSOCKET_PING_INTERVAL
) as websocket:
self.websocket = websocket
self.reconnect_attempts = 0
logger.info("Connected to Polygon.io WebSocket")
# Authenticate
auth_msg = {"action": "auth", "params": config.POLYGON_API_KEY}
await websocket.send(json.dumps(auth_msg))
# Subscribe to tickers
await self._subscribe_polygon_tickers()
# Start processing messages
async for message in websocket:
try:
# Check if we should stop
if not self.running:
break
data = json.loads(message)
# Handle different message types
if isinstance(data, list):
for msg in data:
await self._process_polygon_message(msg)
else:
await self._process_polygon_message(data)
# Periodically flush data to database
if time.time() - self.last_flush_time > config.DATA_FLUSH_INTERVAL:
await self._flush_data_to_database()
self.last_flush_time = time.time()
# Periodically update features
if (
time.time() - self.last_feature_update_time
> config.REALTIME_UPDATE_INTERVAL
):
await self._update_all_features()
self.last_feature_update_time = time.time()
except Exception as e:
logger.error(f"Error processing message: {str(e)}", exc_info=True)
continue
async def _subscribe_polygon_tickers(self):
"""Subscribe to Polygon.io tickers with AMD optimizations"""
if not self.subscribed_tickers:
self.subscribed_tickers = set(config.INITIAL_TICKERS)
# Subscribe to trades and quotes for each ticker
for ticker in self.subscribed_tickers:
subscribe_msg = {
"action": "subscribe",
"params": f"T.{ticker},Q.{ticker}",
}
await self.websocket.send(json.dumps(subscribe_msg))
logger.info(
f"Subscribed to {len(self.subscribed_tickers)} tickers on Polygon.io"
)
async def _process_polygon_message(self, msg: Dict):
"""Process a message from Polygon.io with AMD optimizations"""
if msg.get("ev") == "T": # Trade message
await self._process_trade_message(msg)
elif msg.get("ev") == "Q": # Quote message
await self._process_quote_message(msg)
elif msg.get("ev") == "status": # Status message
logger.info(f"Polygon.io status: {msg.get('message')}")
async def _process_trade_message(self, msg: Dict):
"""Process a trade message with AMD optimizations"""
ticker = msg.get("sym")
if ticker not in self.subscribed_tickers:
return
# Check memory before processing
if not self.memory_manager.ensure_memory(1 * 1024**2): # 1MB
logger.warning(
f"Skipping trade message for {ticker} due to memory constraints"
)
return
try:
timestamp = pd.to_datetime(msg.get("t"), unit="ms").strftime(
"%Y-%m-%d %H:%M:%S"
)
price = msg.get("p")
size = msg.get("s")
conditions = msg.get("c", [])
# Store trade data
if ticker not in self.data_buffer:
self.data_buffer[ticker] = {}
if "trades" not in self.data_buffer[ticker]:
self.data_buffer[ticker]["trades"] = {}
self.data_buffer[ticker]["trades"][timestamp] = {
"price": price,
"size": size,
"conditions": conditions,
}
# Update price bars
await self._update_price_bars(ticker, timestamp, price, size)
# Call callback if provided
if self.on_data_callback:
await self.on_data_callback(
ticker,
timestamp,
"trade",
{"price": price, "size": size, "conditions": conditions},
)
except Exception as e:
logger.error(
f"Error processing trade message for {ticker}: {str(e)}",
exc_info=True,
)
self.memory_manager.empty_cache()
async def _process_quote_message(self, msg: Dict):
"""Process a quote message with AMD optimizations"""
ticker = msg.get("sym")
if ticker not in self.subscribed_tickers:
return
# Check memory before processing
if not self.memory_manager.ensure_memory(1 * 1024**2): # 1MB
logger.warning(
f"Skipping quote message for {ticker} due to memory constraints"
)
return
try:
timestamp = pd.to_datetime(msg.get("t"), unit="ms").strftime(
"%Y-%m-%d %H:%M:%S"
)
bid_price = msg.get("bp")
bid_size = msg.get("bs")
ask_price = msg.get("ap")
ask_size = msg.get("as")
# Store order book data
if ticker not in self.data_buffer:
self.data_buffer[ticker] = {}
if "order_book" not in self.data_buffer[ticker]:
self.data_buffer[ticker]["order_book"] = {}
self.data_buffer[ticker]["order_book"][timestamp] = {
"bid_price": bid_price,
"bid_size": bid_size,
"ask_price": ask_price,
"ask_size": ask_size,
}
# Call callback if provided
if self.on_data_callback:
await self.on_data_callback(
ticker,
timestamp,
"quote",
{
"bid_price": bid_price,
"bid_size": bid_size,
"ask_price": ask_price,
"ask_size": ask_size,
},
)
except Exception as e:
logger.error(
f"Error processing quote message for {ticker}: {str(e)}",
exc_info=True,
)
self.memory_manager.empty_cache()
async def _update_price_bars(
self, ticker: str, timestamp: str, price: float, size: int
):
"""Update price bars with new trade data with AMD optimizations"""
# Check memory before processing
if not self.memory_manager.ensure_memory(5 * 1024**2): # 5MB
logger.warning(
f"Skipping price bar update for {ticker} due to memory constraints"
)
return
try:
# Get current date
date = timestamp.split(" ")[0]
# Determine the current bar based on trading frequency
current_time = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").time()
if config.TRADING_FREQUENCY == "1min":
bar_time = current_time.replace(second=0, microsecond=0)
elif config.TRADING_FREQUENCY == "5min":
minute = (current_time.minute // 5) * 5
bar_time = current_time.replace(minute=minute, second=0, microsecond=0)
elif config.TRADING_FREQUENCY == "15min":
minute = (current_time.minute // 15) * 15
bar_time = current_time.replace(minute=minute, second=0, microsecond=0)
else: # Default to 1 minute
bar_time = current_time.replace(second=0, microsecond=0)
bar_timestamp = f"{date} {bar_time.strftime('%H:%M:%S')}"
# Update or create the current bar
if bar_timestamp in self.price_bars[ticker].index:
# Update existing bar
bar = self.price_bars[ticker].loc[bar_timestamp]
bar["high"] = max(bar["high"], price)
bar["low"] = min(bar["low"], price)
bar["close"] = price
bar["volume"] += size
bar["trades"] += 1
# Update VWAP
if "vwap" in bar:
bar["vwap"] = (
(bar["vwap"] * (bar["volume"] - size)) + (price * size)
) / bar["volume"]
else:
bar["vwap"] = price
self.price_bars[ticker].loc[bar_timestamp] = bar
else:
# Create new bar
new_bar = {
"open": price,
"high": price,
"low": price,
"close": price,
"volume": size,
"vwap": price,
"trades": 1,
}
self.price_bars[ticker].loc[bar_timestamp] = new_bar
# Keep only recent data to limit memory usage
if len(self.price_bars[ticker]) > config.DATA_BUFFER_SIZE:
self.price_bars[ticker] = self.price_bars[ticker].iloc[
-config.DATA_BUFFER_SIZE :
]
except Exception as e:
logger.error(
f"Error updating price bars for {ticker}: {str(e)}", exc_info=True
)
self.memory_manager.empty_cache()
async def _update_all_features(self):
"""Update features for all tickers with AMD optimizations"""
for ticker in self.subscribed_tickers:
await self._calculate_realtime_features(ticker)
async def _calculate_realtime_features(self, ticker: str):
"""Calculate real-time features for a ticker with AMD optimizations"""
# Check memory before processing
if not self.memory_manager.ensure_memory(10 * 1024**2): # 10MB
logger.warning(
f"Skipping feature calculation for {ticker} due to memory constraints"
)
return
try:
if ticker not in self.price_bars or self.price_bars[ticker].empty:
return
# Get the most recent bars
recent_bars = self.price_bars[ticker].iloc[
-config.REALTIME_FEATURE_WINDOW :
]
if len(recent_bars) < 5: # Need at least 5 bars for meaningful features
return
# Calculate returns
recent_bars["returns"] = recent_bars["close"].pct_change()
# Calculate volatility (annualized)
volatility = recent_bars["returns"].std() * np.sqrt(252)
# Calculate momentum
momentum = recent_bars["returns"].mean()
# Calculate volume momentum
volume_momentum = recent_bars["volume"].pct_change().mean()
# Calculate bid-ask spread if order book data exists
bid_ask_spread = None
bid_ask_spread_pct = None
if not self.order_book[ticker].empty:
# Get most recent order book data
recent_order_book = self.order_book[ticker].iloc[-1]
bid_ask_spread = (
recent_order_book["ask_price"] - recent_order_book["bid_price"]
)
bid_ask_spread_pct = bid_ask_spread / (
(recent_order_book["ask_price"] + recent_order_book["bid_price"])
/ 2
)
# Calculate volume imbalance if order book data exists
volume_imbalance = None
if not self.order_book[ticker].empty:
recent_order_book = self.order_book[ticker].iloc[-1]
volume_imbalance = (
recent_order_book["bid_size"] - recent_order_book["ask_size"]
) / (recent_order_book["bid_size"] + recent_order_book["ask_size"])
# Calculate order flow if trade data exists
order_flow = None
if not self.trades[ticker].empty and not self.order_book[ticker].empty:
# Get recent trades and order book data for the same period
recent_trades = self.trades[ticker].iloc[
-config.REALTIME_FEATURE_WINDOW :
]
recent_order_book = self.order_book[ticker].iloc[
-config.REALTIME_FEATURE_WINDOW :
]
# Merge trades with order book data
merged = pd.merge_asof(
recent_trades.sort_index(),
recent_order_book.sort_index(),
left_index=True,
right_index=True,
direction="backward",
)
# Classify trades as buyer or seller initiated
if not merged.empty:
merged["trade_sign"] = np.where(
merged["price"]
> (merged["bid_price"] + merged["ask_price"]) / 2,
1, # Buyer-initiated
-1, # Seller-initiated
)
# Aggregate order flow
order_flow = merged["trade_sign"].sum()
# Calculate VWAP deviation
vwap_deviation = (
recent_bars["close"].iloc[-1] / recent_bars["vwap"].iloc[-1] - 1
if "vwap" in recent_bars
else None
)
# Store features
timestamp = recent_bars.index[-1]
features = {
"timestamp": timestamp,
"returns": recent_bars["returns"].iloc[-1],
"volatility": volatility,
"momentum": momentum,
"volume_momentum": volume_momentum,
"bid_ask_spread": bid_ask_spread,
"bid_ask_spread_pct": bid_ask_spread_pct,
"volume_imbalance": volume_imbalance,
"order_flow": order_flow,
"vwap_deviation": vwap_deviation,
}
# Add to features DataFrame
self.features[ticker].loc[timestamp] = features
# Keep only recent features to limit memory usage
if len(self.features[ticker]) > config.DATA_BUFFER_SIZE:
self.features[ticker] = self.features[ticker].iloc[
-config.DATA_BUFFER_SIZE :
]
except Exception as e:
logger.error(
f"Error calculating features for {ticker}: {str(e)}", exc_info=True
)
self.memory_manager.empty_cache()
async def _flush_data_to_database(self):
"""Flush buffered data to the database with AMD optimizations"""
logger.info("Flushing data to database")
# Check memory before flushing
if not self.memory_manager.ensure_memory(500 * 1024**2): # 500MB
logger.warning("Skipping data flush due to memory constraints")
return
try:
for ticker in self.subscribed_tickers:
# Flush price bars
if not self.price_bars[ticker].empty:
await self._store_price_bars(ticker, self.price_bars[ticker])
# Flush order book data
if (
ticker in self.data_buffer
and "order_book" in self.data_buffer[ticker]
and self.data_buffer[ticker]["order_book"]
):
order_book_data = pd.DataFrame.from_dict(
self.data_buffer[ticker]["order_book"], orient="index"
)
await self._store_order_book_data(ticker, order_book_data)
# Flush trade data
if (
ticker in self.data_buffer
and "trades" in self.data_buffer[ticker]
and self.data_buffer[ticker]["trades"]
):
trade_data = pd.DataFrame.from_dict(
self.data_buffer[ticker]["trades"], orient="index"
)
await self._store_trade_data(ticker, trade_data)
# Flush features
if not self.features[ticker].empty:
await self._store_features(ticker, self.features[ticker])
# Clear buffer after flushing
self.data_buffer = {ticker: {} for ticker in self.subscribed_tickers}
except Exception as e:
logger.error(f"Error flushing data to database: {str(e)}", exc_info=True)
self.memory_manager.empty_cache()
async def _store_price_bars(self, ticker: str, price_bars: pd.DataFrame):
"""Store price bars in the database with AMD optimizations"""
if price_bars.empty:
return
# Convert to list of dictionaries for bulk insert
price_bars_list = []
for timestamp, row in price_bars.iterrows():
price_bars_list.append(
{
"ticker": ticker,
"timestamp": timestamp,
"open": row["open"],
"high": row["high"],
"low": row["low"],
"close": row["close"],
"volume": row["volume"],
"vwap": row.get("vwap", None),
"trades": row.get("trades", None),
}
)
# Use connection pool for better performance
if self.db_pool:
try:
cursor = self.db_pool.cursor()
cursor.executemany(
"""
INSERT OR REPLACE INTO price_bars
(ticker, timestamp, open, high, low, close, volume, vwap, trades)
VALUES (:ticker, :timestamp, :open, :high, :low, :close, :volume, :vwap, :trades)
""",
price_bars_list,
)
self.db_pool.commit()
except Exception as e:
logger.error(f"Error storing price bars for {ticker}: {str(e)}")
self.db_pool.rollback()
else:
# Fallback to regular connection
with sqlite3.connect(self.pipeline.db_path) as conn:
cursor = conn.cursor()
cursor.executemany(
"""
INSERT OR REPLACE INTO price_bars
(ticker, timestamp, open, high, low, close, volume, vwap, trades)
VALUES (:ticker, :timestamp, :open, :high, :low, :close, :volume, :vwap, :trades)
""",
price_bars_list,
)
conn.commit()
async def _store_order_book_data(self, ticker: str, order_book: pd.DataFrame):
"""Store order book data in the database with AMD optimizations"""
if order_book.empty:
return
# Convert to list of dictionaries for bulk insert
order_book_list = []
for timestamp, row in order_book.iterrows():
order_book_list.append(
{
"ticker": ticker,
"timestamp": timestamp,
"bid_price": row["bid_price"],
"bid_size": row["bid_size"],
"ask_price": row["ask_price"],
"ask_size": row["ask_size"],
}
)
# Use connection pool for better performance
if self.db_pool:
try:
cursor = self.db_pool.cursor()
cursor.executemany(
"""
INSERT OR REPLACE INTO order_book
(ticker, timestamp, bid_price, bid_size, ask_price, ask_size)
VALUES (:ticker, :timestamp, :bid_price, :bid_size, :ask_price, :ask_size)
""",
order_book_list,
)
self.db_pool.commit()
except Exception as e:
logger.error(f"Error storing order book data for {ticker}: {str(e)}")
self.db_pool.rollback()
else:
# Fallback to regular connection
with sqlite3.connect(self.pipeline.db_path) as conn:
cursor = conn.cursor()
cursor.executemany(
"""
INSERT OR REPLACE INTO order_book
(ticker, timestamp, bid_price, bid_size, ask_price, ask_size)
VALUES (:ticker, :timestamp, :bid_price, :bid_size, :ask_price, :ask_size)
""",
order_book_list,
)
conn.commit()
async def _store_trade_data(self, ticker: str, trades: pd.DataFrame):
"""Store trade data in the database with AMD optimizations"""
if trades.empty:
return
# Convert to list of dictionaries for bulk insert
trades_list = []
for timestamp, row in trades.iterrows():
trades_list.append(
{
"ticker": ticker,
"timestamp": timestamp,
"price": row["price"],
"size": row["size"],
"trade_condition": (
row.get("conditions", [None])[0]
if isinstance(row.get("conditions"), list)
else None
),
}
)
# Use connection pool for better performance
if self.db_pool:
try:
cursor = self.db_pool.cursor()
cursor.executemany(
"""
INSERT OR REPLACE INTO trades
(ticker, timestamp, price, size, trade_condition)
VALUES (:ticker, :timestamp, :price, :size, :trade_condition)
""",
trades_list,
)
self.db_pool.commit()
except Exception as e:
logger.error(f"Error storing trade data for {ticker}: {str(e)}")
self.db_pool.rollback()
else:
# Fallback to regular connection
with sqlite3.connect(self.pipeline.db_path) as conn:
cursor = conn.cursor()
cursor.executemany(
"""
INSERT OR REPLACE INTO trades
(ticker, timestamp, price, size, trade_condition)
VALUES (:ticker, :timestamp, :price, :size, :trade_condition)
""",
trades_list,
)
conn.commit()
async def _store_features(self, ticker: str, features: pd.DataFrame):
"""Store features in the database with AMD optimizations"""
if features.empty:
return
# Convert to list of dictionaries for bulk insert
features_list = []
for timestamp, row in features.iterrows():
for feature_name, feature_value in row.items():
features_list.append(
{
"ticker": ticker,
"timestamp": timestamp,
"feature_name": feature_name,
"feature_value": feature_value,
}
)
# Use connection pool for better performance
if self.db_pool:
try:
cursor = self.db_pool.cursor()
cursor.executemany(
"""
INSERT OR REPLACE INTO features
(ticker, timestamp, feature_name, feature_value)
VALUES (:ticker, :timestamp, :feature_name, :feature_value)
""",
features_list,
)
self.db_pool.commit()
except Exception as e:
logger.error(f"Error storing features for {ticker}: {str(e)}")
self.db_pool.rollback()
else:
# Fallback to regular connection
with sqlite3.connect(self.pipeline.db_path) as conn:
cursor = conn.cursor()
cursor.executemany(
"""
INSERT OR REPLACE INTO features
(ticker, timestamp, feature_name, feature_value)
VALUES (:ticker, :timestamp, :feature_name, :feature_value)
""",
features_list,
)
conn.commit()
async def _handle_disconnect(self):
"""Handle WebSocket disconnection with AMD optimizations"""
self.reconnect_attempts += 1
logger.warning(
f"WebSocket disconnected. Attempt {self.reconnect_attempts} of {config.WEBSOCKET_MAX_RETRIES}"
)
if self.websocket:
await self.websocket.close()
self.websocket = None
# Clear memory on disconnect
self.memory_manager.empty_cache()
async def subscribe(self, tickers: List[str]):
"""Subscribe to additional tickers with AMD optimizations"""
new_tickers = set(tickers) - self.subscribed_tickers
if not new_tickers:
return
self.subscribed_tickers.update(new_tickers)
# Initialize data structures for new tickers
for ticker in new_tickers:
self.data_buffer[ticker] = {}
self.price_bars[ticker] = pd.DataFrame()
self.order_book[ticker] = pd.DataFrame()
self.trades[ticker] = pd.DataFrame()
self.features[ticker] = pd.DataFrame()
# Subscribe to new tickers
if config.DATA_PROVIDER == "polygon" and self.websocket:
for ticker in new_tickers:
subscribe_msg = {
"action": "subscribe",
"params": f"T.{ticker},Q.{ticker}",
}
await self.websocket.send(json.dumps(subscribe_msg))
logger.info(
f"Subscribed to {len(new_tickers)} new tickers: {', '.join(new_tickers)}"
)
async def unsubscribe(self, tickers: List[str]):
"""Unsubscribe from tickers with AMD optimizations"""
removed_tickers = set(tickers) & self.subscribed_tickers
if not removed_tickers:
return
self.subscribed_tickers -= removed_tickers
# Remove from data structures
for ticker in removed_tickers:
if ticker in self.data_buffer:
del self.data_buffer[ticker]
if ticker in self.price_bars:
del self.price_bars[ticker]
if ticker in self.order_book:
del self.order_book[ticker]
if ticker in self.trades:
del self.trades[ticker]
if ticker in self.features:
del self.features[ticker]
# Unsubscribe from tickers
if config.DATA_PROVIDER == "polygon" and self.websocket:
for ticker in removed_tickers:
unsubscribe_msg = {
"action": "unsubscribe",
"params": f"T.{ticker},Q.{ticker}",
}
await self.websocket.send(json.dumps(unsubscribe_msg))
logger.info(
f"Unsubscribed from {len(removed_tickers)} tickers: {', '.join(removed_tickers)}"
)
def get_latest_data(self, ticker: str) -> Dict:
"""Get the latest data for a ticker with AMD optimizations"""
if ticker not in self.data_buffer:
return {}
# Check memory before processing
if not self.memory_manager.ensure_memory(1 * 1024**2): # 1MB
logger.warning(
f"Skipping latest data retrieval for {ticker} due to memory constraints"
)
return {}
latest_data = {}
# Get latest price bar
if not self.price_bars[ticker].empty:
latest_data["price_bar"] = self.price_bars[ticker].iloc[-1].to_dict()
# Get latest order book data
if (
"order_book" in self.data_buffer[ticker]
and self.data_buffer[ticker]["order_book"]
):
latest_data["order_book"] = list(
self.data_buffer[ticker]["order_book"].values()
)[-1]
# Get latest trade data
if "trades" in self.data_buffer[ticker] and self.data_buffer[ticker]["trades"]:
latest_data["trade"] = list(self.data_buffer[ticker]["trades"].values())[-1]
# Get latest features
if not self.features[ticker].empty:
latest_data["features"] = self.features[ticker].iloc[-1].to_dict()
return latest_data
def get_latest_features(self, ticker: str) -> Dict:
"""Get the latest features for a ticker with AMD optimizations"""
if ticker not in self.features or self.features[ticker].empty:
return {}
# Check memory before processing
if not self.memory_manager.ensure_memory(1 * 1024**2): # 1MB
logger.warning(
f"Skipping latest features retrieval for {ticker} due to memory constraints"
)
return {}
return self.features[ticker].iloc[-1].to_dict()
def get_latest_features_batch(self, tickers: List[str]) -> Dict[str, Dict]:
"""Get the latest features for multiple tickers with AMD optimizations"""
features = {}
# Check memory before processing
if not self.memory_manager.ensure_memory(10 * 1024**2): # 10MB
logger.warning(
"Skipping batch features retrieval due to memory constraints"
)
return features
for ticker in tickers:
if ticker in self.features and not self.features[ticker].empty:
features[ticker] = self.features[ticker].iloc[-1].to_dict()
return features
async def _connect_alpha_vantage(self):
"""Placeholder for Alpha Vantage connection"""
logger.warning("Alpha Vantage live connection not yet implemented")
while self.running:
await asyncio.sleep(60)
async def _connect_interactive_brokers(self):
"""Placeholder for Interactive Brokers connection"""
logger.warning("Interactive Brokers live connection not yet implemented")
while self.running:
await asyncio.sleep(60)
+36
View File
@@ -0,0 +1,36 @@
"""
News data processor for fetching and analyzing financial news.
"""
import logging
from typing import Dict, List
from config import config
logger = logging.getLogger(__name__)
class NewsProcessor:
"""Process news data for tickers."""
def __init__(self):
self.news_data = {}
def fetch_news(self, tickers: List[str], start_date: str, end_date: str):
"""Fetch news articles for the given tickers and date range."""
logger.info(
f"Fetching news for {len(tickers)} tickers from {start_date} to {end_date}"
)
# Placeholder: integrate with a news API (e.g., NewsAPI, Bloomberg)
pass
def get_news_features(self, ticker: str, date: str) -> Dict:
"""Return news-based features for a ticker on a specific date."""
# Placeholder: return default features
return {
"news_sentiment": 0.0,
"news_volume": 0,
"news_recency": 0.0,
"news_source_reliability": 0.0,
"news_topic_relevance": 0.0,
}
+1131
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
"""
Sentiment analysis module for financial text.
"""
import logging
from typing import Dict
logger = logging.getLogger(__name__)
class SentimentAnalyzer:
"""Analyze sentiment of financial text."""
def __init__(self):
# Placeholder: load a pre-trained sentiment model (e.g., FinBERT)
pass
def analyze(self, text: str) -> Dict:
"""Analyze sentiment of a text snippet."""
# Placeholder: return neutral sentiment
return {
"label": "neutral",
"score": 0.0,
"positive": 0.33,
"negative": 0.33,
"neutral": 0.34,
}
def analyze_batch(self, texts: list) -> list:
"""Analyze sentiment of a batch of texts."""
return [self.analyze(t) for t in texts]
+59
View File
@@ -0,0 +1,59 @@
"""
Social media data processor for fetching and analyzing social sentiment.
"""
import logging
from typing import Dict, List
import pandas as pd
from config import config
logger = logging.getLogger(__name__)
class SocialMediaProcessor:
"""Process social media data for tickers."""
def __init__(self):
self.twitter_data = {}
self.reddit_data = {}
def fetch_twitter_data(self, tickers: List[str], start_date: str, end_date: str):
"""Fetch Twitter data for the given tickers and date range."""
logger.info(
f"Fetching Twitter data for {len(tickers)} tickers from {start_date} to {end_date}"
)
# Placeholder: integrate with Twitter API (e.g., Tweepy)
pass
def fetch_reddit_data(self, tickers: List[str], start_date: str, end_date: str):
"""Fetch Reddit data for the given tickers and date range."""
logger.info(
f"Fetching Reddit data for {len(tickers)} tickers from {start_date} to {end_date}"
)
# Placeholder: integrate with Reddit API (e.g., PRAW)
pass
def get_all_social_features(self, tickers: List[str], date: str) -> pd.DataFrame:
"""Return social media features for tickers on a specific date."""
# Placeholder: return default features DataFrame
features = {
"ticker": tickers,
"twitter_sentiment": [0.0] * len(tickers),
"twitter_volume": [0] * len(tickers),
"reddit_sentiment": [0.0] * len(tickers),
"reddit_volume": [0] * len(tickers),
"social_momentum": [0.0] * len(tickers),
}
return pd.DataFrame(features)
def get_social_features(self, ticker: str, date: str) -> Dict:
"""Return social media features for a single ticker."""
return {
"twitter_sentiment": 0.0,
"twitter_volume": 0,
"reddit_sentiment": 0.0,
"reddit_volume": 0,
"social_momentum": 0.0,
}
View File
+61
View File
@@ -0,0 +1,61 @@
"""
Backtesting framework for the GNN trading strategy.
"""
import logging
from typing import Dict, List, Tuple
import pandas as pd
from config import config
from src.models.trainer import GNNTrainer
from src.trading.paper_broker import PaperTradingBroker
logger = logging.getLogger(__name__)
class GNNBacktester:
"""Backtest the GNN model on historical data."""
def __init__(self, model, pipeline):
self.model = model
self.pipeline = pipeline
self.broker = PaperTradingBroker(initial_cash=config.INITIAL_CAPITAL)
def run_backtest(self, dataset: List) -> Tuple[pd.Series, List]:
"""Run a backtest on the given dataset."""
logger.info(f"Starting backtest with {len(dataset)} samples")
portfolio_values = []
dates = []
trade_log = []
for data in dataset:
# Get predictions
predictions = self.model(data)
# Simulate trading based on predictions
for i, ticker in enumerate(data.tickers):
pred = predictions[i].item()
if pred > 0.002:
order = {
"ticker": ticker,
"action": "buy",
"quantity": 100,
"price": 100, # placeholder
"timestamp": str(getattr(data, "date", "")),
"type": "market",
}
order_id = self.broker.submit_order(order)
if order_id:
trade_log.append({**order, "order_id": order_id})
# Record portfolio value
account = self.broker.get_account_summary()
portfolio_values.append(account["total_value"])
dates.append(getattr(data, "date", None))
portfolio_series = pd.Series(portfolio_values, index=dates)
logger.info("Backtest completed")
return portfolio_series, trade_log
+57
View File
@@ -0,0 +1,57 @@
"""
Intraday backtesting framework.
"""
import logging
from typing import Dict, List, Tuple
import pandas as pd
from config import config
from src.trading.paper_broker import PaperTradingBroker
logger = logging.getLogger(__name__)
class IntradayBacktester:
"""Backtest intraday trading strategies."""
def __init__(self, model, pipeline):
self.model = model
self.pipeline = pipeline
self.broker = PaperTradingBroker(initial_cash=config.INITIAL_CAPITAL)
def run_backtest(self, dataset: List) -> Tuple[pd.Series, List]:
"""Run an intraday backtest."""
logger.info(f"Starting intraday backtest with {len(dataset)} samples")
portfolio_values = []
timestamps = []
trade_log = []
for data in dataset:
predictions = self.model(data)
for i, ticker in enumerate(getattr(data, "tickers", [])):
pred = predictions[i].item()
if pred > 0.002:
order = {
"ticker": ticker,
"action": "buy",
"quantity": 100,
"price": 100,
"timestamp": getattr(data, "timestamp", ""),
"type": "market",
}
order_id = self.broker.submit_order(order)
if order_id:
trade_log.append({**order, "order_id": order_id})
account = self.broker.get_account_summary()
portfolio_values.append(account["total_value"])
timestamps.append(getattr(data, "timestamp", None))
portfolio_series = pd.Series(portfolio_values, index=timestamps)
logger.info("Intraday backtest completed")
return portfolio_series, trade_log
+128
View File
@@ -0,0 +1,128 @@
"""
Performance metrics for trading strategy evaluation.
"""
import logging
from typing import Dict
import numpy as np
import pandas as pd
logger = logging.getLogger(__name__)
def calculate_performance_metrics(
portfolio_returns: pd.Series, benchmark_returns: pd.Series
) -> Dict:
"""Calculate portfolio performance metrics."""
metrics = {}
# Total return
metrics["total_return"] = (1 + portfolio_returns).prod() - 1
# Annualized return
metrics["annualized_return"] = (1 + metrics["total_return"]) ** (
252 / len(portfolio_returns)
) - 1
# Volatility
metrics["volatility"] = portfolio_returns.std() * np.sqrt(252)
# Sharpe ratio (assuming risk-free rate of 0)
metrics["sharpe_ratio"] = (
metrics["annualized_return"] / metrics["volatility"]
if metrics["volatility"] > 0
else 0
)
# Sortino ratio
downside_returns = portfolio_returns[portfolio_returns < 0]
downside_std = (
downside_returns.std() * np.sqrt(252) if len(downside_returns) > 0 else 1e-6
)
metrics["sortino_ratio"] = metrics["annualized_return"] / downside_std
# Maximum drawdown
cumulative = (1 + portfolio_returns).cumprod()
running_max = cumulative.expanding().max()
drawdown = (cumulative - running_max) / running_max
metrics["max_drawdown"] = drawdown.min()
# Calmar ratio
metrics["calmar_ratio"] = (
metrics["annualized_return"] / abs(metrics["max_drawdown"])
if metrics["max_drawdown"] != 0
else 0
)
# Win rate
metrics["win_rate"] = (portfolio_returns > 0).mean()
# Profit factor
gross_profit = portfolio_returns[portfolio_returns > 0].sum()
gross_loss = abs(portfolio_returns[portfolio_returns < 0].sum())
metrics["profit_factor"] = (
gross_profit / gross_loss if gross_loss > 0 else float("inf")
)
# Beta
covariance = portfolio_returns.cov(benchmark_returns)
benchmark_variance = benchmark_returns.var()
metrics["beta"] = covariance / benchmark_variance if benchmark_variance > 0 else 0
# Alpha
metrics["alpha"] = (
metrics["annualized_return"] - metrics["beta"] * benchmark_returns.mean() * 252
)
return metrics
def compare_to_benchmark(
portfolio_values: pd.Series, benchmark_values: pd.Series
) -> Dict:
"""Compare portfolio performance to benchmark."""
comparison = {}
portfolio_returns = portfolio_values.pct_change().dropna()
benchmark_returns = benchmark_values.pct_change().dropna()
# Total return comparison
comparison["portfolio_total_return"] = (
portfolio_values.iloc[-1] / portfolio_values.iloc[0]
) - 1
comparison["benchmark_total_return"] = (
benchmark_values.iloc[-1] / benchmark_values.iloc[0]
) - 1
comparison["excess_return"] = (
comparison["portfolio_total_return"] - comparison["benchmark_total_return"]
)
# Tracking error
comparison["tracking_error"] = (
portfolio_returns - benchmark_returns
).std() * np.sqrt(252)
# Information ratio
active_returns = portfolio_returns - benchmark_returns
comparison["information_ratio"] = (
active_returns.mean() * 252 / (active_returns.std() * np.sqrt(252))
if active_returns.std() > 0
else 0
)
# Up/down capture
up_market = benchmark_returns > 0
down_market = benchmark_returns < 0
comparison["up_capture"] = (
(portfolio_returns[up_market].mean() / benchmark_returns[up_market].mean())
if up_market.sum() > 0
else 0
)
comparison["down_capture"] = (
(portfolio_returns[down_market].mean() / benchmark_returns[down_market].mean())
if down_market.sum() > 0
else 0
)
return comparison
View File
+383
View File
@@ -0,0 +1,383 @@
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import GATConv
from config import config
from src.amd.optimizations import AMDGATConv, AMDSparseAttention
class TemporalAttention(nn.Module):
"""
Temporal attention optimized for AMD GPUs
"""
def __init__(self, feature_dim: int, num_heads: int = 8, dropout: float = 0.1):
super().__init__()
self.feature_dim = feature_dim
self.num_heads = num_heads
self.head_dim = feature_dim // num_heads
self.dropout = dropout
# Use AMD-optimized sparse attention if available
try:
self.attention = AMDSparseAttention(feature_dim, num_heads, dropout)
except Exception:
# Fallback to standard multi-head attention
self.query = nn.Linear(feature_dim, feature_dim)
self.key = nn.Linear(feature_dim, feature_dim)
self.value = nn.Linear(feature_dim, feature_dim)
self.out = nn.Linear(feature_dim, feature_dim)
self.layer_norm = nn.LayerNorm(feature_dim)
self.dropout_layer = nn.Dropout(dropout)
# Initialize weights
self._init_weights()
def _init_weights(self):
"""Initialize weights with Xavier initialization"""
if not hasattr(self, "attention"):
nn.init.xavier_uniform_(self.query.weight)
nn.init.xavier_uniform_(self.key.weight)
nn.init.xavier_uniform_(self.value.weight)
nn.init.xavier_uniform_(self.out.weight)
nn.init.zeros_(self.query.bias)
nn.init.zeros_(self.key.bias)
nn.init.zeros_(self.value.bias)
nn.init.zeros_(self.out.bias)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Apply temporal attention to input sequence
Parameters:
x: Input tensor of shape (batch_size, sequence_length, feature_dim)
Returns:
Tensor of shape (batch_size, feature_dim) with temporal attention applied
"""
# Layer normalization
x_norm = self.layer_norm(x)
if hasattr(self, "attention"):
# Use AMD-optimized sparse attention
attended = self.attention(x_norm)
else:
# Standard multi-head attention
batch_size, seq_len, _ = x_norm.size()
# Project to query, key, value
q = self.query(x_norm)
k = self.key(x_norm)
v = self.value(x_norm)
# Reshape for multi-head attention
q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(
1, 2
)
k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(
1, 2
)
v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(
1, 2
)
# Calculate attention scores
attn_scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(
self.head_dim
)
# Apply softmax
attn_weights = F.softmax(attn_scores, dim=-1)
attn_weights = self.dropout_layer(attn_weights)
# Apply attention to values
attended = torch.matmul(attn_weights, v)
# Concatenate heads
attended = (
attended.transpose(1, 2)
.contiguous()
.view(batch_size, seq_len, self.feature_dim)
)
# Final projection
attended = self.out(attended)
# Aggregate across time with residual connection
output = attended.mean(dim=1) + x.mean(dim=1)
return output
class CorporateActionAwareGNN(nn.Module):
"""
GNN model with corporate action awareness, optimized for AMD Radeon R9700 AI Pro
"""
def __init__(self, num_node_features: int):
super(CorporateActionAwareGNN, self).__init__()
# Calculate feature dimensions
num_price_features = 5 # returns, volatility, momentum, volume, price
num_news_features = len(config.NEWS_FEATURES)
num_social_features = len(config.SOCIAL_FEATURES)
# Feature processing modules with AMD optimizations
self.price_processor = nn.Sequential(
nn.Linear(num_price_features, config.HIDDEN_CHANNELS),
nn.SiLU(), # Swish activation often works better on AMD GPUs
nn.Linear(config.HIDDEN_CHANNELS, config.HIDDEN_CHANNELS),
nn.LayerNorm(config.HIDDEN_CHANNELS),
)
self.news_processor = nn.Sequential(
nn.Linear(num_news_features, config.HIDDEN_CHANNELS),
nn.SiLU(),
nn.Linear(config.HIDDEN_CHANNELS, config.HIDDEN_CHANNELS),
nn.LayerNorm(config.HIDDEN_CHANNELS),
)
self.social_processor = nn.Sequential(
nn.Linear(num_social_features, config.HIDDEN_CHANNELS),
nn.SiLU(),
nn.Linear(config.HIDDEN_CHANNELS, config.HIDDEN_CHANNELS),
nn.LayerNorm(config.HIDDEN_CHANNELS),
)
self.corporate_action_mlp = nn.Sequential(
nn.Linear(1, config.HIDDEN_CHANNELS),
nn.SiLU(),
nn.Linear(config.HIDDEN_CHANNELS, config.HIDDEN_CHANNELS),
nn.LayerNorm(config.HIDDEN_CHANNELS),
)
# Temporal attention for sequence processing
self.temporal_attention = TemporalAttention(config.HIDDEN_CHANNELS, num_heads=8)
# Graph attention layers with AMD optimizations
try:
self.conv1 = AMDGATConv(
config.HIDDEN_CHANNELS * 4, # Combined features from all processors
config.HIDDEN_CHANNELS,
heads=config.NUM_HEADS,
concat=True,
dropout=config.DROPOUT,
add_self_loops=True,
)
except Exception:
self.conv1 = GATConv(
config.HIDDEN_CHANNELS * 4,
config.HIDDEN_CHANNELS,
heads=config.NUM_HEADS,
concat=True,
dropout=config.DROPOUT,
add_self_loops=True,
)
try:
self.conv2 = AMDGATConv(
config.HIDDEN_CHANNELS * config.NUM_HEADS,
config.HIDDEN_CHANNELS,
heads=1,
concat=False,
dropout=config.DROPOUT,
add_self_loops=True,
)
except Exception:
self.conv2 = GATConv(
config.HIDDEN_CHANNELS * config.NUM_HEADS,
config.HIDDEN_CHANNELS,
heads=1,
concat=False,
dropout=config.DROPOUT,
add_self_loops=True,
)
# Attention mechanism for combining alternative data
self.alternative_data_attention = nn.Sequential(
nn.Linear(config.HIDDEN_CHANNELS * 3, config.HIDDEN_CHANNELS),
nn.SiLU(),
nn.Linear(
config.HIDDEN_CHANNELS, 3
), # 3 attention weights (price, news, social)
nn.Softmax(dim=1),
)
# LSTM for temporal dependencies with AMD optimizations
self.lstm = nn.LSTM(
input_size=config.HIDDEN_CHANNELS,
hidden_size=config.HIDDEN_CHANNELS,
num_layers=2,
batch_first=True,
dropout=config.DROPOUT if config.NUM_HEADS > 1 else 0,
bidirectional=False,
)
# Initialize LSTM weights
self._init_lstm_weights()
# Final prediction layer
self.linear = nn.Linear(
config.HIDDEN_CHANNELS * 2, 1
) # *2 for concatenating GNN output and attention features
# Initialize weights
self._init_weights()
def _init_weights(self):
"""Initialize weights with AMD-friendly initialization"""
# Initialize linear layer weights
nn.init.xavier_uniform_(self.linear.weight)
nn.init.zeros_(self.linear.bias)
# Initialize attention layers
for layer in self.alternative_data_attention:
if isinstance(layer, nn.Linear):
nn.init.xavier_uniform_(layer.weight)
nn.init.zeros_(layer.bias)
def _init_lstm_weights(self):
"""Initialize LSTM weights with orthogonal initialization for better convergence"""
for name, param in self.lstm.named_parameters():
if "weight_ih" in name:
nn.init.orthogonal_(param)
elif "weight_hh" in name:
nn.init.orthogonal_(param)
elif "bias" in name:
nn.init.zeros_(param)
# Set forget gate bias to 1 for better gradient flow
n = param.size(0)
param.data[n // 4 : n // 2].fill_(1)
def forward(self, data):
x, edge_index, edge_attr = data.x, data.edge_index, data.edge_attr
# Split features into different types
num_price_features = 5
num_news_features = len(config.NEWS_FEATURES)
num_social_features = len(config.SOCIAL_FEATURES)
# Price features (first 5)
price_features = x[:, :, :num_price_features]
price_processed = self.price_processor(price_features)
# News features (next num_news_features)
news_start = num_price_features
news_end = news_start + num_news_features
news_features = x[:, :, news_start:news_end]
news_processed = self.news_processor(news_features)
# Social features (next num_social_features)
social_start = news_end
social_end = social_start + num_social_features
social_features = x[:, :, social_start:social_end]
social_processed = self.social_processor(social_features)
# Corporate action flag (last feature)
corporate_action_flags = x[:, :, -1:]
corporate_action_features = self.corporate_action_mlp(corporate_action_flags)
# Apply temporal attention to each feature type
price_attended = self.temporal_attention(price_processed)
news_attended = self.temporal_attention(news_processed)
social_attended = self.temporal_attention(social_processed)
corporate_attended = self.temporal_attention(corporate_action_features)
# Calculate attention weights for alternative data
alternative_features = torch.cat(
[price_attended, news_attended, social_attended], dim=1
)
attention_weights = self.alternative_data_attention(alternative_features)
# Apply attention weights
weighted_price = price_attended * attention_weights[:, 0].unsqueeze(1)
weighted_news = news_attended * attention_weights[:, 1].unsqueeze(1)
weighted_social = social_attended * attention_weights[:, 2].unsqueeze(1)
# Combine features
combined_features = torch.cat(
[weighted_price, weighted_news, weighted_social, corporate_attended], dim=1
)
# Process through GNN with gradient checkpointing for memory efficiency
x = self._gnn_forward(combined_features, edge_index, edge_attr)
# Process through LSTM for temporal dependencies
lstm_input = x.unsqueeze(1) # (num_stocks, 1, hidden_size)
lstm_out, _ = self.lstm(lstm_input)
lstm_out = lstm_out.squeeze(1)
# Combine GNN output with attention features
attention_features = torch.cat(
[
attention_weights[:, 0].unsqueeze(1),
attention_weights[:, 1].unsqueeze(1),
attention_weights[:, 2].unsqueeze(1),
],
dim=1,
)
x = torch.cat([lstm_out, attention_features], dim=1)
# Final prediction
return self.linear(x)
def _gnn_forward(self, x, edge_index, edge_attr):
"""Forward pass through GNN with gradient checkpointing for memory efficiency"""
# Gradient checkpointing for memory efficiency on AMD GPUs
x = torch.utils.checkpoint.checkpoint(
self.conv1, x, edge_index, edge_attr, preserve_rng_state=False
)
x = F.silu(x) # Swish activation often works better than ReLU on AMD GPUs
x = F.dropout(x, p=config.DROPOUT, training=self.training)
x = torch.utils.checkpoint.checkpoint(
self.conv2, x, edge_index, edge_attr, preserve_rng_state=False
)
return x
def get_attention_weights(self, data):
"""Get attention weights for interpretability"""
x, edge_index, edge_attr = data.x, data.edge_index, data.edge_attr
# Process through the model up to attention
num_price_features = 5
num_news_features = len(config.NEWS_FEATURES)
num_social_features = len(config.SOCIAL_FEATURES)
price_features = x[:, :, :num_price_features]
news_features = x[
:, :, num_price_features : num_price_features + num_news_features
]
social_features = x[
:,
:,
num_price_features + num_news_features : num_price_features
+ num_news_features
+ num_social_features,
]
corporate_action_flags = x[:, :, -1:]
price_processed = self.price_processor(price_features)
news_processed = self.news_processor(news_features)
social_processed = self.social_processor(social_features)
corporate_action_features = self.corporate_action_mlp(corporate_action_flags)
price_attended = self.temporal_attention(price_processed)
news_attended = self.temporal_attention(news_processed)
social_attended = self.temporal_attention(social_processed)
alternative_features = torch.cat(
[price_attended, news_attended, social_attended], dim=1
)
attention_weights = self.alternative_data_attention(alternative_features)
return attention_weights
+220
View File
@@ -0,0 +1,220 @@
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import GATConv
from config import config
from src.amd.optimizations import AMDGATConv, AMDSparseAttention
from src.models.gnn_model import TemporalAttention
class IntradayGNN(nn.Module):
"""
Intraday GNN model optimized for AMD Radeon R9700 AI Pro
"""
def __init__(
self, num_features: int, sequence_length: int = config.SEQUENCE_LENGTH
):
super(IntradayGNN, self).__init__()
self.sequence_length = sequence_length
self.num_features = num_features
# Temporal attention for sequence processing
self.temporal_attention = TemporalAttention(num_features, num_heads=8)
# Feature processing modules with AMD optimizations
self.feature_processor = nn.Sequential(
nn.Linear(num_features, config.HIDDEN_CHANNELS),
nn.SiLU(), # Swish activation often works better on AMD GPUs
nn.Linear(config.HIDDEN_CHANNELS, config.HIDDEN_CHANNELS),
nn.LayerNorm(config.HIDDEN_CHANNELS),
)
# Graph attention layers with AMD optimizations
try:
self.conv1 = AMDGATConv(
config.HIDDEN_CHANNELS,
config.HIDDEN_CHANNELS,
heads=config.NUM_HEADS,
concat=True,
dropout=config.DROPOUT,
add_self_loops=True,
)
except Exception:
self.conv1 = GATConv(
config.HIDDEN_CHANNELS,
config.HIDDEN_CHANNELS,
heads=config.NUM_HEADS,
concat=True,
dropout=config.DROPOUT,
add_self_loops=True,
)
try:
self.conv2 = AMDGATConv(
config.HIDDEN_CHANNELS * config.NUM_HEADS,
config.HIDDEN_CHANNELS,
heads=1,
concat=False,
dropout=config.DROPOUT,
add_self_loops=True,
)
except Exception:
self.conv2 = GATConv(
config.HIDDEN_CHANNELS * config.NUM_HEADS,
config.HIDDEN_CHANNELS,
heads=1,
concat=False,
dropout=config.DROPOUT,
add_self_loops=True,
)
# LSTM for temporal dependencies with AMD optimizations
self.lstm = nn.LSTM(
input_size=config.HIDDEN_CHANNELS,
hidden_size=config.HIDDEN_CHANNELS,
num_layers=2,
batch_first=True,
dropout=config.DROPOUT if config.NUM_HEADS > 1 else 0,
bidirectional=False,
)
# Initialize LSTM weights
self._init_lstm_weights()
# Attention mechanism for final prediction
self.attention = nn.Sequential(
nn.Linear(config.HIDDEN_CHANNELS, config.HIDDEN_CHANNELS),
nn.SiLU(),
nn.Linear(config.HIDDEN_CHANNELS, 1),
nn.Softmax(dim=1),
)
# Final prediction layer
self.linear = nn.Linear(config.HIDDEN_CHANNELS, 1)
# State for online learning
self.hidden_state = None
# Initialize weights
self._init_weights()
def _init_weights(self):
"""Initialize weights with AMD-friendly initialization"""
# Initialize linear layer weights
nn.init.xavier_uniform_(self.linear.weight)
nn.init.zeros_(self.linear.bias)
# Initialize attention layers
for layer in self.attention:
if isinstance(layer, nn.Linear):
nn.init.xavier_uniform_(layer.weight)
nn.init.zeros_(layer.bias)
def _init_lstm_weights(self):
"""Initialize LSTM weights with orthogonal initialization for better convergence"""
for name, param in self.lstm.named_parameters():
if "weight_ih" in name:
nn.init.orthogonal_(param)
elif "weight_hh" in name:
nn.init.orthogonal_(param)
elif "bias" in name:
nn.init.zeros_(param)
# Set forget gate bias to 1 for better gradient flow
n = param.size(0)
param.data[n // 4 : n // 2].fill_(1)
def forward(self, data):
x, edge_index, edge_attr = data.x, data.edge_index, data.edge_attr
# x shape: (num_stocks, sequence_length, num_features)
batch_size, seq_len, num_features = x.size()
# Apply temporal attention to each stock's sequence
temporal_features = []
for i in range(batch_size):
stock_sequence = x[i].unsqueeze(0) # (1, sequence_length, num_features)
temporal_feature = self.temporal_attention(stock_sequence)
temporal_features.append(temporal_feature)
# Stack temporal features
temporal_features = torch.cat(
temporal_features, dim=0
) # (num_stocks, feature_dim)
# Process features
processed_features = self.feature_processor(temporal_features)
# Process through GNN with gradient checkpointing for memory efficiency
x = self._gnn_forward(processed_features, edge_index, edge_attr)
# Process through LSTM for temporal dependencies
# Reshape for LSTM: (num_stocks, 1, hidden_size)
lstm_input = x.unsqueeze(1)
# If we have a hidden state from previous prediction, use it
if self.hidden_state is not None and config.STATEFUL_PREDICTION:
lstm_out, self.hidden_state = self.lstm(lstm_input, self.hidden_state)
else:
lstm_out, self.hidden_state = self.lstm(lstm_input)
# Remove sequence dimension
lstm_out = lstm_out.squeeze(1)
# Apply attention to LSTM outputs
attention_weights = self.attention(lstm_out)
attended = (lstm_out * attention_weights).sum(dim=1, keepdim=True)
# Final prediction
return self.linear(attended)
def _gnn_forward(self, x, edge_index, edge_attr):
"""Forward pass through GNN with gradient checkpointing for memory efficiency"""
# Gradient checkpointing for memory efficiency on AMD GPUs
x = torch.utils.checkpoint.checkpoint(
self.conv1, x, edge_index, edge_attr, preserve_rng_state=False
)
x = F.silu(x) # Swish activation often works better than ReLU on AMD GPUs
x = F.dropout(x, p=config.DROPOUT, training=self.training)
x = torch.utils.checkpoint.checkpoint(
self.conv2, x, edge_index, edge_attr, preserve_rng_state=False
)
return x
def reset_state(self):
"""Reset the hidden state of the LSTM"""
self.hidden_state = None
def get_attention_weights(self, data):
"""Get attention weights for interpretability"""
x, edge_index, edge_attr = data.x, data.edge_index, data.edge_attr
# Process through the model up to attention
batch_size, seq_len, num_features = x.size()
temporal_features = []
for i in range(batch_size):
stock_sequence = x[i].unsqueeze(0)
temporal_feature = self.temporal_attention(stock_sequence)
temporal_features.append(temporal_feature)
temporal_features = torch.cat(temporal_features, dim=0)
processed_features = self.feature_processor(temporal_features)
x = self.conv1(processed_features, edge_index, edge_attr)
x = F.silu(x)
x = self.conv2(x, edge_index, edge_attr)
lstm_input = x.unsqueeze(1)
lstm_out, _ = self.lstm(lstm_input)
lstm_out = lstm_out.squeeze(1)
attention_weights = self.attention(lstm_out)
return attention_weights
+356
View File
@@ -0,0 +1,356 @@
import logging
import os
import time
from typing import Dict, List, Tuple
import numpy as np
import torch
import torch.nn as nn
from torch.cuda.amp import GradScaler, autocast
from torch_geometric.loader import DataLoader
from tqdm import tqdm
from config import config
from src.amd.optimizations import AMDOptimizer
from src.utils.memory_manager import MemoryManager
logger = logging.getLogger(__name__)
class GNNTrainer:
def __init__(self, model: nn.Module):
self.model = model
self.device = torch.device(config.DEVICE)
self.memory_manager = MemoryManager()
self.amd_optimizer = AMDOptimizer()
# Optimize model for AMD GPU
self.model = self.amd_optimizer.optimize_model(self.model)
# Set up optimizer with weight decay
self.optimizer = torch.optim.AdamW(
self.model.parameters(), lr=config.LEARNING_RATE, weight_decay=1e-4
)
# Set up learning rate scheduler
self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
self.optimizer, mode="min", factor=0.5, patience=5, verbose=True
)
self.criterion = nn.MSELoss()
# For online learning
if config.ONLINE_LEARNING:
self.online_optimizer = torch.optim.AdamW(
self.model.parameters(), lr=config.ONLINE_LEARNING_RATE
)
# For mixed precision training
self.scaler = GradScaler(enabled=config.MIXED_PRECISION)
# Training statistics
self.train_losses = []
self.val_losses = []
self.best_val_loss = float("inf")
# Log memory info
logger.info(self.memory_manager.get_memory_stats())
def train(
self, train_dataset: List, val_dataset: List
) -> Tuple[List[float], List[float]]:
"""
Train the GNN model with AMD optimizations
Parameters:
train_dataset: Training dataset
val_dataset: Validation dataset
Returns:
Tuple of (train_losses, val_losses)
"""
# Create data loaders with AMD optimizations
train_loader = DataLoader(
train_dataset,
batch_size=config.BATCH_SIZE,
shuffle=True,
num_workers=config.NUM_WORKERS,
pin_memory=config.PIN_MEMORY,
prefetch_factor=config.PREFETCH_FACTOR,
)
val_loader = DataLoader(
val_dataset,
batch_size=config.BATCH_SIZE,
shuffle=False,
num_workers=config.NUM_WORKERS,
pin_memory=config.PIN_MEMORY,
prefetch_factor=config.PREFETCH_FACTOR,
)
for epoch in range(config.EPOCHS):
# Training
self.model.train()
epoch_train_loss = 0.0
start_time = time.time()
for batch in tqdm(
train_loader, desc=f"Epoch {epoch + 1}/{config.EPOCHS} - Training"
):
# Check memory before processing batch
if not self.memory_manager.ensure_memory(2 * 1024**3): # 2GB
logger.warning("Skipping batch due to memory constraints")
continue
try:
batch = batch.to(self.device, non_blocking=config.PIN_MEMORY)
self.optimizer.zero_grad(
set_to_none=True
) # More efficient for AMD GPUs
# Mixed precision training
with autocast(
enabled=config.MIXED_PRECISION,
dtype=self.amd_optimizer.get_precision_dtype(),
):
out = self.model(batch)
loss = self.criterion(out, batch.y)
# Scale loss and backpropagate
self.scaler.scale(loss).backward()
# Gradient clipping for stability
self.scaler.unscale_(self.optimizer)
torch.nn.utils.clip_grad_norm_(
self.model.parameters(), max_norm=1.0
)
# Update weights
self.scaler.step(self.optimizer)
self.scaler.update()
epoch_train_loss += loss.item()
# Memory management
self.memory_manager.auto_manage_memory(threshold=0.8)
except Exception as e:
logger.error(f"Error processing batch: {str(e)}", exc_info=True)
self.memory_manager.empty_cache()
continue
epoch_train_loss /= len(train_loader)
self.train_losses.append(epoch_train_loss)
# Validation
self.model.eval()
epoch_val_loss = 0.0
with torch.no_grad():
for batch in val_loader:
# Check memory before processing batch
if not self.memory_manager.ensure_memory(1 * 1024**3): # 1GB
logger.warning(
"Skipping validation batch due to memory constraints"
)
continue
try:
batch = batch.to(self.device, non_blocking=config.PIN_MEMORY)
with autocast(
enabled=config.MIXED_PRECISION,
dtype=self.amd_optimizer.get_precision_dtype(),
):
out = self.model(batch)
loss = self.criterion(out, batch.y)
epoch_val_loss += loss.item()
# Memory management
self.memory_manager.auto_manage_memory(threshold=0.8)
except Exception as e:
logger.error(
f"Error processing validation batch: {str(e)}",
exc_info=True,
)
self.memory_manager.empty_cache()
continue
epoch_val_loss /= len(val_loader)
self.val_losses.append(epoch_val_loss)
# Update learning rate scheduler
self.scheduler.step(epoch_val_loss)
# Log training information
epoch_time = time.time() - start_time
memory_info = self.memory_manager.check_memory()
logger.info(
f"Epoch {epoch + 1}/{config.EPOCHS} - "
f"Train Loss: {epoch_train_loss:.6f}, "
f"Val Loss: {epoch_val_loss:.6f}, "
f"LR: {self.optimizer.param_groups[0]['lr']:.2e}, "
f"Time: {epoch_time:.2f}s, "
f"Memory: {memory_info['allocated'] / 1024**3:.2f}GB/{memory_info['limit'] / 1024**3:.2f}GB"
)
# Save best model
if epoch_val_loss < self.best_val_loss:
self.best_val_loss = epoch_val_loss
self.save_model()
logger.info("Saved best model")
return self.train_losses, self.val_losses
def online_update(self, data):
"""
Perform online learning update with new data using AMD optimizations
Parameters:
data: New data for online learning
"""
if not config.ONLINE_LEARNING:
return
# Check memory before online learning
if not self.memory_manager.ensure_memory(1 * 1024**3): # 1GB
logger.warning("Skipping online learning due to memory constraints")
return
self.model.train()
self.online_optimizer.zero_grad(set_to_none=True)
try:
data = data.to(self.device, non_blocking=config.PIN_MEMORY)
with autocast(
enabled=config.MIXED_PRECISION,
dtype=self.amd_optimizer.get_precision_dtype(),
):
out = self.model(data)
loss = self.criterion(out, data.y)
self.scaler.scale(loss).backward()
self.scaler.step(self.online_optimizer)
self.scaler.update()
# Memory management
self.memory_manager.auto_manage_memory(threshold=0.8)
return loss.item()
except Exception as e:
logger.error(f"Error during online learning: {str(e)}", exc_info=True)
self.memory_manager.empty_cache()
return None
def save_model(self, path: str = None):
"""Save the model weights with AMD-specific optimizations"""
if path is None:
path = os.path.join(config.MODEL_DIR, f"{config.MODEL_NAME}.pt")
# Save model state with additional information
checkpoint = {
"model_state_dict": self.model.state_dict(),
"optimizer_state_dict": self.optimizer.state_dict(),
"scheduler_state_dict": self.scheduler.state_dict(),
"scaler_state_dict": self.scaler.state_dict(),
"train_losses": self.train_losses,
"val_losses": self.val_losses,
"best_val_loss": self.best_val_loss,
"config": {
"device": config.DEVICE,
"mixed_precision": config.MIXED_PRECISION,
"precision": config.PRECISION,
"rocm_opt_level": config.ROCM_OPT_LEVEL if config.AMD_GPU else None,
"batch_size": config.BATCH_SIZE,
"hidden_channels": config.HIDDEN_CHANNELS,
"num_heads": config.NUM_HEADS,
},
}
torch.save(checkpoint, path)
logger.info(f"Model saved to {path}")
def load_model(self, path: str = None):
"""Load model weights with AMD-specific optimizations"""
if path is None:
path = os.path.join(config.MODEL_DIR, f"{config.MODEL_NAME}.pt")
if os.path.exists(path):
try:
# Check memory before loading model
if not self.memory_manager.ensure_memory(4 * 1024**3): # 4GB
logger.warning(
"Not enough memory to load model. Falling back to CPU."
)
self.device = torch.device("cpu")
self.model = self.model.to(self.device)
checkpoint = torch.load(path, map_location=self.device)
self.model.load_state_dict(checkpoint["model_state_dict"])
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
self.scheduler.load_state_dict(checkpoint["scheduler_state_dict"])
self.scaler.load_state_dict(checkpoint["scaler_state_dict"])
self.train_losses = checkpoint.get("train_losses", [])
self.val_losses = checkpoint.get("val_losses", [])
self.best_val_loss = checkpoint.get("best_val_loss", float("inf"))
# Restore config if available
if "config" in checkpoint:
saved_config = checkpoint["config"]
if saved_config.get("device") != config.DEVICE:
logger.warning(
f"Model was trained on {saved_config['device']} but current device is {config.DEVICE}"
)
logger.info(f"Model loaded from {path}")
# Re-optimize model for current hardware
self.model = self.amd_optimizer.optimize_model(self.model)
return True
except Exception as e:
logger.error(f"Error loading model: {str(e)}", exc_info=True)
return False
else:
logger.warning(f"Model file not found at {path}")
return False
def predict(self, data, return_attention: bool = False):
"""Make predictions on new data with AMD optimizations"""
self.model.eval()
with torch.no_grad():
# Check memory before prediction
if not self.memory_manager.ensure_memory(1 * 1024**3): # 1GB
logger.warning("Prediction skipped due to memory constraints")
return None
try:
data = data.to(self.device, non_blocking=config.PIN_MEMORY)
with autocast(
enabled=config.MIXED_PRECISION,
dtype=self.amd_optimizer.get_precision_dtype(),
):
if return_attention:
out = self.model(data)
attention_weights = self.model.get_attention_weights(data)
return out, attention_weights
else:
return self.model(data)
except Exception as e:
logger.error(f"Error during prediction: {str(e)}", exc_info=True)
self.memory_manager.empty_cache()
return None
def benchmark(self, input_data, num_runs: int = 100):
"""Benchmark model performance on AMD GPU"""
return self.amd_optimizer.benchmark_model(self.model, input_data, num_runs)
View File
+38
View File
@@ -0,0 +1,38 @@
"""
Base broker interface for order execution.
"""
import logging
from abc import ABC, abstractmethod
from typing import Dict, Optional
logger = logging.getLogger(__name__)
class Broker(ABC):
"""Abstract base class for broker implementations."""
@abstractmethod
def submit_order(self, order: Dict) -> Optional[str]:
"""Submit an order and return the order ID if accepted."""
pass
@abstractmethod
def cancel_order(self, order_id: str) -> bool:
"""Cancel an existing order."""
pass
@abstractmethod
def get_order_status(self, order_id: str) -> Dict:
"""Get the status of an order."""
pass
@abstractmethod
def get_positions(self) -> Dict:
"""Get current positions."""
pass
@abstractmethod
def get_account_summary(self) -> Dict:
"""Get account summary."""
pass
+47
View File
@@ -0,0 +1,47 @@
"""
Interactive Brokers broker implementation.
"""
import logging
from typing import Dict, Optional
from src.trading.broker import Broker
logger = logging.getLogger(__name__)
class InteractiveBrokersBroker(Broker):
"""Broker implementation for Interactive Brokers."""
def __init__(self, host: str = "127.0.0.1", port: int = 7497, client_id: int = 1):
self.host = host
self.port = port
self.client_id = client_id
# Placeholder: initialize ib_insync connection
def submit_order(self, order: Dict) -> Optional[str]:
"""Submit an order via Interactive Brokers."""
logger.info(f"Submitting order via IB: {order}")
# Placeholder: implement IB order submission
return None
def cancel_order(self, order_id: str) -> bool:
"""Cancel an order via Interactive Brokers."""
logger.info(f"Cancelling order {order_id} via IB")
# Placeholder: implement IB order cancellation
return False
def get_order_status(self, order_id: str) -> Dict:
"""Get order status from Interactive Brokers."""
# Placeholder: implement IB order status retrieval
return {}
def get_positions(self) -> Dict:
"""Get positions from Interactive Brokers."""
# Placeholder: implement IB positions retrieval
return {}
def get_account_summary(self) -> Dict:
"""Get account summary from Interactive Brokers."""
# Placeholder: implement IB account summary retrieval
return {}
+82
View File
@@ -0,0 +1,82 @@
"""
Paper trading broker implementation for backtesting and simulation.
"""
import logging
import uuid
from typing import Dict, Optional
from config import config
from src.trading.broker import Broker
logger = logging.getLogger(__name__)
class PaperTradingBroker(Broker):
"""Simulated broker for paper trading."""
def __init__(self, initial_cash: float = 100000.0):
self.cash = initial_cash
self.positions = {}
self.orders = {}
self.transaction_cost = config.TRANSACTION_COST
def submit_order(self, order: Dict) -> Optional[str]:
"""Submit a simulated order."""
order_id = str(uuid.uuid4())
self.orders[order_id] = {**order, "status": "filled"}
ticker = order["ticker"]
quantity = order["quantity"]
price = order["price"]
action = order["action"]
cost = quantity * price * (1 + self.transaction_cost)
if action == "buy":
if cost > self.cash:
logger.warning(f"Insufficient cash for buy order: {order_id}")
self.orders[order_id]["status"] = "rejected"
return None
self.cash -= cost
self.positions[ticker] = self.positions.get(ticker, 0) + quantity
elif action == "sell":
if self.positions.get(ticker, 0) < quantity:
logger.warning(f"Insufficient shares for sell order: {order_id}")
self.orders[order_id]["status"] = "rejected"
return None
self.cash += quantity * price * (1 - self.transaction_cost)
self.positions[ticker] -= quantity
if self.positions[ticker] == 0:
del self.positions[ticker]
logger.info(
f"Paper order filled: {order_id} - {action} {quantity} {ticker} @ {price}"
)
return order_id
def cancel_order(self, order_id: str) -> bool:
"""Cancel a simulated order."""
if order_id in self.orders:
self.orders[order_id]["status"] = "cancelled"
return True
return False
def get_order_status(self, order_id: str) -> Dict:
"""Get the status of a simulated order."""
return self.orders.get(order_id, {})
def get_positions(self) -> Dict:
"""Get current simulated positions."""
return self.positions.copy()
def get_account_summary(self) -> Dict:
"""Get simulated account summary."""
total_value = self.cash + sum(
self.positions.get(t, 0) * 100 # placeholder price
for t in self.positions
)
return {
"cash": self.cash,
"positions": self.positions.copy(),
"total_value": total_value,
}
+91
View File
@@ -0,0 +1,91 @@
"""
Real-time trader for live trading execution.
"""
import logging
from datetime import datetime
from typing import Dict
from config import config
logger = logging.getLogger(__name__)
class RealTimeTrader:
"""Manage real-time trading execution and risk."""
def __init__(self, model, pipeline, broker):
self.model = model
self.pipeline = pipeline
self.broker = broker
self.current_positions = {}
self.pending_orders = {}
self.daily_pnl = 0.0
self.max_drawdown = 0.0
self.entry_times = {}
def _calculate_position_size(self, ticker: str, price: float) -> int:
"""Calculate position size based on risk management rules."""
account_summary = self.broker.get_account_summary()
total_value = account_summary.get("total_value", config.INITIAL_CAPITAL)
max_position_value = total_value * config.MAX_POSITION_SIZE
position_size = int(max_position_value / price)
return max(0, position_size)
def _check_holding_period(self, ticker: str, timestamp: str) -> bool:
"""Check if position holding period constraints are met."""
if ticker not in self.entry_times:
return False
entry_time = datetime.strptime(self.entry_times[ticker], "%Y-%m-%d %H:%M:%S")
current_time = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
# Placeholder: implement holding period checks
return False
def _process_pending_orders(self):
"""Process and update pending orders."""
for order_id, order in list(self.pending_orders.items()):
status = self.broker.get_order_status(order_id)
if status.get("status") == "filled":
ticker = order["ticker"]
action = order["action"]
quantity = order["quantity"]
if action == "buy":
self.current_positions[ticker] = (
self.current_positions.get(ticker, 0) + quantity
)
self.entry_times[ticker] = order["timestamp"]
elif action == "sell":
self.current_positions[ticker] = (
self.current_positions.get(ticker, 0) - quantity
)
if self.current_positions[ticker] <= 0:
del self.current_positions[ticker]
del self.entry_times[ticker]
del self.pending_orders[order_id]
logger.info(f"Order {order_id} processed: {action} {quantity} {ticker}")
def _update_portfolio_value(self):
"""Update portfolio value and track P&L."""
# Placeholder: implement portfolio value tracking
pass
def _check_risk_limits(self) -> bool:
"""Check if risk limits have been exceeded."""
# Placeholder: implement risk limit checks
return False
def _close_all_positions(self):
"""Close all open positions."""
for ticker in list(self.current_positions.keys()):
order = {
"ticker": ticker,
"action": "sell",
"quantity": self.current_positions[ticker],
"price": 0, # market order
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"type": "market",
}
order_id = self.broker.submit_order(order)
if order_id:
self.pending_orders[order_id] = order
logger.info(f"Submitted close order for {ticker}")
+1
View File
@@ -0,0 +1 @@
+46
View File
@@ -0,0 +1,46 @@
"""
Helper functions for the trading GNN project.
"""
from datetime import datetime, timedelta
from typing import List
from config import config
def generate_intraday_timestamps(date: str) -> List[str]:
"""
Generate all intraday timestamps for a given trading date.
Parameters:
date: Trading date string (YYYY-MM-DD).
Returns:
List of timestamp strings for the trading day.
"""
market_open = datetime.strptime(config.TRADING_HOURS["start"], "%H:%M").time()
market_close = datetime.strptime(config.TRADING_HOURS["end"], "%H:%M").time()
open_dt = datetime.strptime(f"{date} {market_open}", "%Y-%m-%d %H:%M:%S")
close_dt = datetime.strptime(f"{date} {market_close}", "%Y-%m-%d %H:%M:%S")
if config.TRADING_FREQUENCY == "1min":
delta = timedelta(minutes=1)
elif config.TRADING_FREQUENCY == "5min":
delta = timedelta(minutes=5)
elif config.TRADING_FREQUENCY == "15min":
delta = timedelta(minutes=15)
elif config.TRADING_FREQUENCY == "30min":
delta = timedelta(minutes=30)
elif config.TRADING_FREQUENCY == "1h":
delta = timedelta(hours=1)
else:
delta = timedelta(minutes=1)
timestamps = []
current = open_dt
while current <= close_dt:
timestamps.append(current.strftime("%Y-%m-%d %H:%M:%S"))
current += delta
return timestamps
+215
View File
@@ -0,0 +1,215 @@
import gc
import logging
from typing import Any, Dict, Optional
import torch
from config import config
logger = logging.getLogger(__name__)
class MemoryManager:
"""
Memory management for AMD Radeon R9700 AI Pro (32GB)
"""
def __init__(self):
self.device = torch.device(config.DEVICE)
self.max_memory = 0
self.memory_limit = 0
self._initialize_memory()
def _initialize_memory(self):
"""Initialize memory settings"""
if config.DEVICE == "cuda":
try:
# Get total GPU memory
self.max_memory = torch.cuda.get_device_properties(0).total_memory
self.memory_limit = int(self.max_memory * config.GPU_MEMORY_LIMIT)
# Set memory limit
torch.cuda.set_per_process_memory_fraction(config.GPU_MEMORY_LIMIT, 0)
logger.info(f"Initialized memory manager for AMD Radeon R9700 AI Pro")
logger.info(f"Total GPU Memory: {self.max_memory / 1024**3:.2f}GB")
logger.info(
f"Memory Limit: {self.memory_limit / 1024**3:.2f}GB ({config.GPU_MEMORY_LIMIT * 100:.0f}%)"
)
except Exception as e:
logger.error(f"Error initializing memory manager: {str(e)}")
self.max_memory = 0
self.memory_limit = 0
def empty_cache(self):
"""Clear the GPU cache and run garbage collection"""
if config.DEVICE == "cuda":
torch.cuda.empty_cache()
gc.collect()
def check_memory(self) -> Dict[str, Any]:
"""Check current GPU memory usage"""
if config.DEVICE != "cuda":
return {
"allocated": 0,
"max_allocated": 0,
"total": 0,
"limit": 0,
"usage_percent": 0,
"free": 0,
}
try:
allocated = torch.cuda.memory_allocated(0)
max_allocated = torch.cuda.max_memory_allocated(0)
free = self.memory_limit - allocated
return {
"allocated": allocated,
"max_allocated": max_allocated,
"total": self.max_memory,
"limit": self.memory_limit,
"usage_percent": (allocated / self.memory_limit) * 100,
"free": free,
}
except Exception as e:
logger.error(f"Error checking memory: {str(e)}")
return {
"allocated": 0,
"max_allocated": 0,
"total": 0,
"limit": 0,
"usage_percent": 0,
"free": 0,
}
def ensure_memory(self, required_memory: int) -> bool:
"""
Ensure there's enough memory for an operation
Parameters:
required_memory: Memory required in bytes
Returns:
True if there's enough memory, False otherwise
"""
if config.DEVICE != "cuda":
return True
memory_info = self.check_memory()
if memory_info["allocated"] + required_memory > memory_info["limit"]:
# Try to free memory
self.empty_cache()
memory_info = self.check_memory()
if memory_info["allocated"] + required_memory > memory_info["limit"]:
logger.warning(
f"Not enough GPU memory. Required: {required_memory / 1024**2:.2f}MB, "
f"Available: {memory_info['free'] / 1024**2:.2f}MB"
)
return False
return True
def auto_manage_memory(self, threshold: float = 0.85):
"""
Automatically manage memory based on usage
Parameters:
threshold: Memory usage threshold (0-1) to trigger cleanup
"""
if config.DEVICE != "cuda":
return
memory_info = self.check_memory()
if memory_info["usage_percent"] > threshold * 100:
logger.info(
f"High GPU memory usage: {memory_info['usage_percent']:.2f}%. Clearing cache."
)
self.empty_cache()
def get_memory_stats(self) -> str:
"""Get formatted memory statistics"""
memory_info = self.check_memory()
return (
f"GPU Memory Usage:\n"
f" Allocated: {memory_info['allocated'] / 1024**3:.2f}GB\n"
f" Max Allocated: {memory_info['max_allocated'] / 1024**3:.2f}GB\n"
f" Total: {memory_info['total'] / 1024**3:.2f}GB\n"
f" Limit: {memory_info['limit'] / 1024**3:.2f}GB\n"
f" Usage: {memory_info['usage_percent']:.2f}%\n"
f" Free: {memory_info['free'] / 1024**3:.2f}GB"
)
def estimate_model_memory(self, model: torch.nn.Module) -> int:
"""
Estimate memory required for a model
Parameters:
model: PyTorch model
Returns:
Estimated memory in bytes
"""
if config.DEVICE != "cuda":
return 0
try:
# Move model to GPU to get accurate memory estimate
model = model.to(config.DEVICE)
# Get model parameters
param_size = 0
for param in model.parameters():
param_size += param.nelement() * param.element_size()
# Get model buffers
buffer_size = 0
for buffer in model.buffers():
buffer_size += buffer.nelement() * buffer.element_size()
# Estimate forward pass memory (activations)
# This is a rough estimate - actual memory usage may vary
forward_memory = (
param_size * 2
) # Activations typically use 2x parameter memory
# Total memory estimate
total_memory = param_size + buffer_size + forward_memory
# Add some buffer for overhead
total_memory = int(total_memory * 1.2)
return total_memory
except Exception as e:
logger.error(f"Error estimating model memory: {str(e)}")
return 0
def log_memory_usage(self, tag: str = ""):
"""Log current memory usage"""
memory_info = self.check_memory()
logger.info(
f"Memory Usage {tag}: "
f"Allocated={memory_info['allocated'] / 1024**3:.2f}GB, "
f"Usage={memory_info['usage_percent']:.2f}%"
)
def monitor_memory(self, interval: float = 60.0):
"""
Monitor memory usage in a background thread
Parameters:
interval: Monitoring interval in seconds
"""
import threading
def monitor():
while True:
self.auto_manage_memory()
self.log_memory_usage("[Monitor]")
time.sleep(interval)
thread = threading.Thread(target=monitor, daemon=True)
thread.start()
+12
View File
@@ -0,0 +1,12 @@
"""
Text processing utilities for sentiment and news analysis.
"""
import re
def clean_text(text: str) -> str:
"""Clean and normalize text."""
text = re.sub(r"http\S+", "", text)
text = re.sub(r"[^\w\s]", "", text)
return text.strip().lower()
+39
View File
@@ -0,0 +1,39 @@
"""
Visualization tools for the trading GNN project.
"""
import matplotlib.pyplot as plt
import pandas as pd
def plot_performance(portfolio_values, benchmark_values, filename):
"""Plot portfolio performance against benchmark."""
plt.figure(figsize=(12, 6))
plt.plot(portfolio_values.index, portfolio_values.values, label="Portfolio")
plt.plot(benchmark_values.index, benchmark_values.values, label="Benchmark")
plt.title("Portfolio vs Benchmark Performance")
plt.xlabel("Date")
plt.ylabel("Value")
plt.legend()
plt.savefig(filename)
plt.close()
def plot_trade_log(trade_log, filename):
"""Plot trade log entries."""
# Placeholder implementation
plt.figure(figsize=(12, 6))
plt.title("Trade Log")
plt.xlabel("Date")
plt.ylabel("Trade")
plt.savefig(filename)
plt.close()
def plot_feature_importance(feature_importance, filename):
"""Plot feature importance."""
# Placeholder implementation
plt.figure(figsize=(10, 6))
plt.title("Feature Importance")
plt.savefig(filename)
plt.close()
+1
View File
@@ -0,0 +1 @@
# Web frontend package
+1
View File
@@ -0,0 +1 @@
# web api package
+84
View File
@@ -0,0 +1,84 @@
"""
Dashboard API endpoints.
"""
import logging
from typing import Dict
from fastapi import APIRouter, HTTPException
from config import config
from src.web.services.state import AppState
logger = logging.getLogger(__name__)
router = APIRouter()
# Reference to global app state (injected via module import in app.py)
app_state: AppState = None # type: ignore
def _get_state() -> AppState:
from src.web.app import app_state as _state
return _state
@router.get("/metrics")
async def get_metrics() -> Dict:
"""Get current dashboard metrics (memory, account, model)."""
state = _get_state()
mm = state.memory_manager
broker = state.broker
memory = mm.check_memory() if mm else {}
account = broker.get_account_summary() if broker else {}
return {
"memory": {
"allocated_gb": round(memory.get("allocated", 0) / 1024**3, 2),
"max_allocated_gb": round(memory.get("max_allocated", 0) / 1024**3, 2),
"total_gb": round(memory.get("total", 0) / 1024**3, 2),
"limit_gb": round(memory.get("limit", 0) / 1024**3, 2),
"usage_percent": round(memory.get("usage_percent", 0), 1),
"free_gb": round(memory.get("free", 0) / 1024**3, 2),
},
"account": {
"cash": round(account.get("cash", 0), 2),
"total_value": round(account.get("total_value", 0), 2),
"positions_count": len(account.get("positions", {})),
"positions": account.get("positions", {}),
},
"model": {
"status": state.model_status,
"device": config.DEVICE,
"amd_gpu": config.AMD_GPU,
"mixed_precision": config.MIXED_PRECISION,
"precision": config.PRECISION,
"hidden_channels": config.HIDDEN_CHANNELS,
"num_heads": config.NUM_HEADS,
"batch_size": config.BATCH_SIZE,
"learning_rate": config.LEARNING_RATE,
},
"system": {
"project_name": config.PROJECT_NAME,
"version": config.VERSION,
"training_active": state.training_active,
"trading_active": state.trading_active,
},
}
@router.get("/logs")
async def get_logs(limit: int = 100) -> Dict:
"""Get recent log entries."""
state = _get_state()
logs = state.logs[-limit:] if state.logs else []
return {"logs": logs, "total": len(state.logs)}
@router.post("/logs/clear")
async def clear_logs() -> Dict:
"""Clear stored log entries."""
state = _get_state()
state.logs.clear()
return {"status": "cleared"}
+139
View File
@@ -0,0 +1,139 @@
"""
Data pipeline API endpoints.
"""
import logging
from datetime import datetime
from typing import Dict, List
from fastapi import APIRouter
from config import config
logger = logging.getLogger(__name__)
router = APIRouter()
def _get_state():
from src.web.app import app_state as _state
return _state
@router.get("/tickers")
async def get_tickers() -> Dict:
"""Get the list of tracked tickers."""
return {
"initial": config.INITIAL_TICKERS,
"index": config.INDEX_TICKER,
"count": len(config.INITIAL_TICKERS),
}
@router.get("/pipeline/status")
async def get_pipeline_status() -> Dict:
"""Get data pipeline status."""
state = _get_state()
pipeline = state.pipeline
if not pipeline:
return {"status": "not_initialized"}
return {
"status": "ready",
"tickers_loaded": len(pipeline.price_data),
"db_path": pipeline.db_path,
}
@router.post("/update")
async def update_data() -> Dict:
"""Trigger a data pipeline update."""
state = _get_state()
pipeline = state.pipeline
if not pipeline:
return {"status": "error", "message": "Pipeline not initialized"}
import asyncio
async def _update():
try:
state.add_log(f"[{datetime.now()}] Data update started")
pipeline.update_all_data()
state.add_log(f"[{datetime.now()}] Data update completed")
except Exception as e:
state.add_log(f"[{datetime.now()}] Data update error: {e}")
logger.error(f"Data update error: {e}", exc_info=True)
asyncio.create_task(_update())
return {"status": "started"}
@router.get("/features/{ticker}")
async def get_features(ticker: str) -> Dict:
"""Get latest features for a ticker."""
state = _get_state()
pipeline = state.pipeline
if not pipeline:
return {"status": "error", "message": "Pipeline not initialized"}
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
features = pipeline.get_latest_features([ticker], timestamp)
return {"ticker": ticker, "timestamp": timestamp, "features": features}
@router.get("/price/{ticker}")
async def get_price(ticker: str) -> Dict:
"""Get latest price data for a ticker."""
state = _get_state()
pipeline = state.pipeline
if not pipeline or ticker not in pipeline.price_data:
return {"status": "error", "message": "Ticker not found"}
df = pipeline.price_data[ticker]
if df.empty:
return {"status": "error", "message": "No data available"}
latest = df.iloc[-1]
return {
"ticker": ticker,
"date": str(df.index[-1]),
"open": latest["Open"],
"high": latest["High"],
"low": latest["Low"],
"close": latest["Close"],
"adj_close": latest["Adj Close"],
"volume": int(latest["Volume"]),
}
@router.get("/prices/{ticker}")
async def get_price_history(ticker: str, limit: int = 30) -> List[Dict]:
"""Get historical price data for a ticker."""
state = _get_state()
pipeline = state.pipeline
if not pipeline or ticker not in pipeline.price_data:
return []
df = pipeline.price_data[ticker].tail(limit)
return [
{
"date": str(idx),
"open": row["Open"],
"high": row["High"],
"low": row["Low"],
"close": row["Close"],
"volume": int(row["Volume"]),
}
for idx, row in df.iterrows()
]
@router.get("/corporate-actions/{ticker}")
async def get_corporate_actions(ticker: str) -> Dict:
"""Get corporate actions for a ticker."""
state = _get_state()
pipeline = state.pipeline
if not pipeline or ticker not in pipeline.corporate_actions:
return {"ticker": ticker, "actions": {}}
return {"ticker": ticker, "actions": pipeline.corporate_actions[ticker]}
+193
View File
@@ -0,0 +1,193 @@
"""
Model management API endpoints.
"""
import logging
import time
from datetime import datetime
from typing import Dict
from fastapi import APIRouter
from config import config
logger = logging.getLogger(__name__)
router = APIRouter()
def _get_state():
from src.web.app import app_state as _state
return _state
@router.get("/status")
async def get_model_status() -> Dict:
"""Get current model status."""
state = _get_state()
return {
"status": state.model_status,
"training_active": state.training_active,
"device": config.DEVICE,
"amd_gpu": config.AMD_GPU,
"mixed_precision": config.MIXED_PRECISION,
"precision": config.PRECISION,
"rocm_opt_level": config.ROCM_OPT_LEVEL,
"model_name": config.MODEL_NAME,
"hidden_channels": config.HIDDEN_CHANNELS,
"num_heads": config.NUM_HEADS,
"dropout": config.DROPOUT,
"learning_rate": config.LEARNING_RATE,
"batch_size": config.BATCH_SIZE,
"epochs": config.EPOCHS,
"sequence_length": config.SEQUENCE_LENGTH,
}
@router.post("/train")
async def start_training() -> Dict:
"""Start a model training run."""
state = _get_state()
if state.training_active:
return {"status": "already_training"}
state.training_active = True
state.add_log(f"[{datetime.now()}] Training started")
logger.info("Training started from web frontend")
# Run training in background (simplified: kick off in async task)
import asyncio
async def _train():
try:
trainer = state.trainer
pipeline = state.pipeline
train_dataset = pipeline.create_training_dataset()
val_dataset = pipeline.create_validation_dataset(
start_date=config.TRAIN_END_DATE, end_date=config.VAL_END_DATE
)
state.add_log(
f"[{datetime.now()}] Datasets created: train={len(train_dataset)}, val={len(val_dataset)}"
)
# Note: actual training would happen here
# trainer.train(train_dataset, val_dataset)
await asyncio.sleep(2) # placeholder for actual training
state.model_status = "trained"
state.add_log(f"[{datetime.now()}] Training completed")
except Exception as e:
state.add_log(f"[{datetime.now()}] Training error: {e}")
logger.error(f"Training error: {e}", exc_info=True)
finally:
state.training_active = False
asyncio.create_task(_train())
return {"status": "started"}
@router.post("/train/stop")
async def stop_training() -> Dict:
"""Stop the current training run."""
state = _get_state()
if not state.training_active:
return {"status": "not_training"}
state.training_active = False
state.add_log(f"[{datetime.now()}] Training stopped")
logger.info("Training stopped from web frontend")
return {"status": "stopped"}
@router.post("/save")
async def save_model() -> Dict:
"""Save the current model weights."""
state = _get_state()
trainer = state.trainer
if not trainer:
return {"status": "error", "message": "Trainer not initialized"}
try:
trainer.save_model()
state.model_status = "saved"
state.add_log(f"[{datetime.now()}] Model saved")
return {"status": "saved"}
except Exception as e:
return {"status": "error", "message": str(e)}
@router.post("/load")
async def load_model() -> Dict:
"""Load model weights from disk."""
state = _get_state()
trainer = state.trainer
if not trainer:
return {"status": "error", "message": "Trainer not initialized"}
try:
success = trainer.load_model()
state.model_status = "loaded" if success else "untrained"
state.add_log(f"[{datetime.now()}] Model loaded: {success}")
return {"status": "loaded" if success else "failed"}
except Exception as e:
return {"status": "error", "message": str(e)}
@router.post("/benchmark")
async def run_benchmark() -> Dict:
"""Run a performance benchmark on the model."""
state = _get_state()
model = state.model
amd_optimizer = state.amd_optimizer
memory_manager = state.memory_manager
if not model:
return {"status": "error", "message": "Model not initialized"}
import torch
import torch.nn as nn
num_features = len(config.INTRADAY_FEATURES) + 5
num_stocks = 50
num_edges = 200
x = torch.randn(num_stocks, config.SEQUENCE_LENGTH, num_features).to(config.DEVICE)
edge_index = torch.randint(0, num_stocks, (2, num_edges)).to(config.DEVICE)
edge_attr = torch.randn(num_edges, 1).to(config.DEVICE)
# Warm-up
for _ in range(10):
with torch.no_grad():
_ = model((x, edge_index, edge_attr))
# Benchmark inference
start = time.time()
num_runs = 100
for _ in range(num_runs):
with torch.no_grad():
_ = model((x, edge_index, edge_attr))
inference_time = (time.time() - start) / num_runs
# Benchmark training
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=config.LEARNING_RATE)
criterion = nn.MSELoss()
y = torch.randn(num_stocks, 1).to(config.DEVICE)
start = time.time()
for _ in range(num_runs):
optimizer.zero_grad()
out = model((x, edge_index, edge_attr))
loss = criterion(out, y)
loss.backward()
optimizer.step()
training_time = (time.time() - start) / num_runs
memory = memory_manager.check_memory() if memory_manager else {}
result = {
"status": "complete",
"inference_time_ms": round(inference_time * 1000, 3),
"training_time_ms": round(training_time * 1000, 3),
"inference_throughput": round(1 / inference_time, 2),
"training_throughput": round(1 / training_time, 2),
"memory_allocated_gb": round(memory.get("allocated", 0) / 1024**3, 2),
"device": config.DEVICE,
}
state.add_log(f"[{datetime.now()}] Benchmark: {result}")
return result
+145
View File
@@ -0,0 +1,145 @@
"""
Trading API endpoints.
"""
import logging
from datetime import datetime
from typing import Dict, List
from fastapi import APIRouter
from config import config
logger = logging.getLogger(__name__)
router = APIRouter()
def _get_state():
from src.web.app import app_state as _state
return _state
@router.get("/status")
async def get_trading_status() -> Dict:
"""Get current trading system status."""
state = _get_state()
broker = state.broker
account = broker.get_account_summary() if broker else {}
return {
"active": state.trading_active,
"cash": round(account.get("cash", 0), 2),
"total_value": round(account.get("total_value", 0), 2),
"positions": account.get("positions", {}),
"orders_count": len(broker.orders) if broker else 0,
}
@router.post("/start")
async def start_trading() -> Dict:
"""Start the live trading system."""
state = _get_state()
if state.trading_active:
return {"status": "already_running"}
state.trading_active = True
state.add_log(f"[{datetime.now()}] Trading started")
logger.info("Trading started from web frontend")
return {"status": "started"}
@router.post("/stop")
async def stop_trading() -> Dict:
"""Stop the live trading system."""
state = _get_state()
if not state.trading_active:
return {"status": "not_running"}
state.trading_active = False
state.add_log(f"[{datetime.now()}] Trading stopped")
logger.info("Trading stopped from web frontend")
return {"status": "stopped"}
@router.get("/orders")
async def get_orders() -> List[Dict]:
"""Get all orders."""
state = _get_state()
broker = state.broker
if not broker:
return []
return [{"order_id": oid, **details} for oid, details in broker.orders.items()]
@router.post("/order")
async def submit_manual_order(order: Dict) -> Dict:
"""Submit a manual order via the web frontend."""
state = _get_state()
broker = state.broker
if not broker:
return {"status": "error", "message": "Broker not initialized"}
order_id = broker.submit_order(order)
if order_id:
state.add_log(f"[{datetime.now()}] Manual order submitted: {order}")
return {"status": "submitted", "order_id": order_id}
return {"status": "rejected"}
@router.post("/cancel/{order_id}")
async def cancel_order(order_id: str) -> Dict:
"""Cancel an order by ID."""
state = _get_state()
broker = state.broker
if not broker:
return {"status": "error", "message": "Broker not initialized"}
success = broker.cancel_order(order_id)
return {"status": "cancelled" if success else "failed"}
@router.post("/positions/close/{ticker}")
async def close_position(ticker: str) -> Dict:
"""Close a position for a given ticker."""
state = _get_state()
broker = state.broker
if not broker or ticker not in broker.get_positions():
return {"status": "error", "message": "No position found"}
quantity = broker.get_positions()[ticker]
order = {
"ticker": ticker,
"action": "sell",
"quantity": quantity,
"price": 0,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"type": "market",
}
order_id = broker.submit_order(order)
if order_id:
return {"status": "submitted", "order_id": order_id}
return {"status": "rejected"}
@router.post("/positions/close-all")
async def close_all_positions() -> Dict:
"""Close all open positions."""
state = _get_state()
broker = state.broker
if not broker:
return {"status": "error", "message": "Broker not initialized"}
positions = broker.get_positions()
results = []
for ticker in list(positions.keys()):
order = {
"ticker": ticker,
"action": "sell",
"quantity": positions[ticker],
"price": 0,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"type": "market",
}
order_id = broker.submit_order(order)
results.append({"ticker": ticker, "order_id": order_id})
state.add_log(f"[{datetime.now()}] Closed all positions")
return {"status": "submitted", "results": results}
+193
View File
@@ -0,0 +1,193 @@
"""
FastAPI web application for the StockGNN trading system.
"""
import asyncio
import json
import logging
import os
from contextlib import asynccontextmanager
from typing import Any, Dict, List
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from config import config
from src.amd.optimizations import AMDOptimizer
from src.data.pipeline import StockDataPipeline
from src.models.intraday_gnn import IntradayGNN
from src.models.trainer import GNNTrainer
from src.trading.paper_broker import PaperTradingBroker
from src.utils.memory_manager import MemoryManager
from src.web.api import dashboard, data_endpoints, models_endpoints, trading_endpoints
from src.web.services.state import AppState
logger = logging.getLogger(__name__)
# Initialize global app state
app_state = AppState()
# Build template/static paths relative to this file
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
STATIC_DIR = os.path.join(BASE_DIR, "static")
templates = Jinja2Templates(directory=TEMPLATES_DIR)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan handler for startup/shutdown events."""
logger.info("Starting StockGNN Web Frontend")
# Initialize shared state
app_state.memory_manager = MemoryManager()
app_state.amd_optimizer = AMDOptimizer()
app_state.pipeline = StockDataPipeline()
app_state.broker = PaperTradingBroker(initial_cash=config.INITIAL_CAPITAL)
# Initialize model (but don't train yet)
num_features = len(config.INTRADAY_FEATURES) + 5
app_state.model = IntradayGNN(num_features, config.SEQUENCE_LENGTH)
app_state.trainer = GNNTrainer(app_state.model)
# Try to load pre-trained weights
try:
app_state.trainer.load_model()
app_state.model_status = "loaded"
except Exception:
app_state.model_status = "untrained"
app_state.model = app_state.amd_optimizer.optimize_model(app_state.model)
# Start background broadcast task
app_state.broadcast_task = asyncio.create_task(_broadcast_loop())
yield
# Shutdown
logger.info("Shutting down StockGNN Web Frontend")
if app_state.broadcast_task:
app_state.broadcast_task.cancel()
try:
await app_state.broadcast_task
except asyncio.CancelledError:
pass
app = FastAPI(
title="StockGNN R9700",
description="AMD-optimized Graph Neural Network trading system dashboard",
version=config.VERSION,
lifespan=lifespan,
)
# Serve static files
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# Include API routers
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["dashboard"])
app.include_router(trading_endpoints.router, prefix="/api/trading", tags=["trading"])
app.include_router(models_endpoints.router, prefix="/api/models", tags=["models"])
app.include_router(data_endpoints.router, prefix="/api/data", tags=["data"])
# ---------------------------------------------------------------------------
# HTML entry point
# ---------------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def get_index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
# ---------------------------------------------------------------------------
# WebSocket for real-time updates
# ---------------------------------------------------------------------------
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
app_state.connections.append(ws)
logger.info(f"WebSocket client connected ({len(app_state.connections)} active)")
try:
while True:
# Wait for any incoming messages (optional client commands)
msg = await ws.receive_text()
try:
data = json.loads(msg)
await _handle_ws_message(ws, data)
except json.JSONDecodeError:
await ws.send_json({"error": "Invalid JSON"})
except WebSocketDisconnect:
logger.info("WebSocket client disconnected")
finally:
if ws in app_state.connections:
app_state.connections.remove(ws)
async def _handle_ws_message(ws: WebSocket, data: Dict[str, Any]):
"""Handle incoming WebSocket messages."""
action = data.get("action")
if action == "ping":
await ws.send_json({"type": "pong", "timestamp": str(datetime.now())})
elif action == "subscribe":
channel = data.get("channel", "all")
await ws.send_json({"type": "subscribed", "channel": channel})
else:
await ws.send_json({"type": "error", "message": f"Unknown action: {action}"})
async def _broadcast_loop():
"""Background task that periodically broadcasts metrics to all WS clients."""
from datetime import datetime
while True:
try:
await asyncio.sleep(2) # broadcast every 2 seconds
payload = _build_broadcast_payload()
disconnected = []
for ws in app_state.connections:
try:
await ws.send_json(payload)
except Exception:
disconnected.append(ws)
for ws in disconnected:
if ws in app_state.connections:
app_state.connections.remove(ws)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Broadcast loop error: {e}")
def _build_broadcast_payload() -> Dict[str, Any]:
"""Build the real-time metrics payload."""
from datetime import datetime
mm = app_state.memory_manager
broker = app_state.broker
memory = mm.check_memory() if mm else {}
account = broker.get_account_summary() if broker else {}
return {
"type": "metrics",
"timestamp": datetime.now().isoformat(),
"memory": {
"allocated_gb": round(memory.get("allocated", 0) / 1024**3, 2),
"total_gb": round(memory.get("total", 0) / 1024**3, 2),
"usage_percent": round(memory.get("usage_percent", 0), 1),
},
"account": {
"cash": round(account.get("cash", 0), 2),
"total_value": round(account.get("total_value", 0), 2),
"positions": len(account.get("positions", {})),
},
"model": {
"status": app_state.model_status,
"device": config.DEVICE,
"mixed_precision": config.MIXED_PRECISION,
"precision": config.PRECISION,
},
}
+1
View File
@@ -0,0 +1 @@
# web services package
+31
View File
@@ -0,0 +1,31 @@
"""
Shared application state for the web frontend.
"""
from dataclasses import dataclass, field
from typing import Any, List, Optional
@dataclass
class AppState:
"""Singleton-ish shared state for the web application."""
memory_manager: Optional[Any] = None
amd_optimizer: Optional[Any] = None
pipeline: Optional[Any] = None
broker: Optional[Any] = None
model: Optional[Any] = None
trainer: Optional[Any] = None
broadcast_task: Optional[Any] = None
connections: List[Any] = field(default_factory=list)
model_status: str = "untrained"
training_active: bool = False
trading_active: bool = False
logs: List[str] = field(default_factory=list)
def add_log(self, message: str):
"""Add a log entry (capped at 500 lines)."""
self.logs.append(message)
if len(self.logs) > 500:
self.logs = self.logs[-500:]
+409
View File
@@ -0,0 +1,409 @@
/* StockGNN R9700 Dashboard Styles */
:root {
--bg: #0b0f17;
--surface: #111827;
--surface-2: #1a2233;
--surface-3: #243044;
--text: #e6edf3;
--text-secondary: #8b96a7;
--accent: #ff6b35;
--accent-2: #ff8c5a;
--green: #22c55e;
--green-dim: #16a34a;
--red: #ef4444;
--red-dim: #dc2626;
--blue: #3b82f6;
--blue-dim: #2563eb;
--amber: #f59e0b;
--border: rgba(255,255,255,0.06);
--radius: 10px;
--shadow: 0 4px 20px rgba(0,0,0,0.35);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--text);
height: 100%;
overflow: hidden;
}
.app {
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 220px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 1.1rem;
color: var(--text);
}
.logo-icon {
font-size: 1.5rem;
color: var(--accent);
}
.logo-badge {
font-size: 0.7rem;
background: var(--accent);
color: #fff;
padding: 2px 6px;
border-radius: 4px;
vertical-align: middle;
margin-left: 4px;
}
.device-badge {
margin-top: 8px;
font-size: 0.75rem;
color: var(--text-secondary);
background: var(--surface-3);
padding: 3px 8px;
border-radius: 6px;
display: inline-block;
}
.nav {
flex: 1;
padding: 12px 0;
overflow-y: auto;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
color: var(--text-secondary);
text-decoration: none;
font-size: 0.9rem;
border-left: 3px solid transparent;
transition: all 0.15s ease;
}
.nav-item:hover {
color: var(--text);
background: var(--surface-2);
}
.nav-item.active {
color: var(--accent);
background: rgba(255,107,53,0.06);
border-left-color: var(--accent);
}
.nav-icon {
font-size: 1.1rem;
width: 22px;
text-align: center;
}
.sidebar-footer {
padding: 16px 20px;
border-top: 1px solid var(--border);
font-size: 0.75rem;
color: var(--text-secondary);
}
.connection-status {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
}
.status-dot.disconnected { background: var(--red); }
.status-dot.connecting { background: var(--amber); }
.version {
opacity: 0.5;
}
/* Main */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.topbar {
height: 60px;
background: var(--surface);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
flex-shrink: 0;
}
.page-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text);
}
.topbar-actions {
display: flex;
gap: 10px;
}
/* Page system */
.page {
display: none;
flex: 1;
padding: 24px;
overflow-y: auto;
gap: 24px;
flex-direction: column;
}
.page.active { display: flex; }
/* Grid */
.grid {
display: grid;
gap: 20px;
}
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1200px) {
.grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 900px) {
.grid-3 { grid-template-columns: repeat(2, 1fr); }
.grid-2 { grid-template-columns: 1fr; }
.grid-4 { grid-template-columns: 1fr; }
}
@media (max-width: 700px) {
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
.sidebar { display: none; }
}
/* Cards */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.card-header h3 {
font-size: 0.95rem;
font-weight: 600;
color: var(--text);
}
.metric-card {
text-align: center;
padding: 24px 16px;
}
.metric-label {
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.metric-value {
font-size: 1.8rem;
font-weight: 700;
color: var(--text);
margin-bottom: 6px;
}
.metric-sub {
font-size: 0.8rem;
color: var(--text-secondary);
}
.chart-card {
padding: 16px;
}
.chart-container {
position: relative;
height: 220px;
}
/* Info grid */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 10px 20px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--border);
font-size: 0.9rem;
}
.info-key { color: var(--text-secondary); }
.info-value { color: var(--text); font-weight: 500; }
/* Tables */
.table-container { overflow-x: auto; }
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.data-table th,
.data-table td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
color: var(--text);
}
.data-table th {
color: var(--text-secondary);
font-weight: 500;
text-transform: uppercase;
font-size: 0.7rem;
letter-spacing: 0.5px;
}
.data-table tbody tr:hover {
background: var(--surface-2);
}
/* Buttons */
.btn {
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
color: #fff;
}
.btn-primary { background: var(--blue); }
.btn-primary:hover { background: var(--blue-dim); }
.btn-secondary { background: var(--surface-3); color: var(--text); }
.btn-secondary:hover { background: var(--surface-2); }
.btn-danger { background: var(--red); }
.btn-danger:hover { background: var(--red-dim); }
.btn-accent { background: var(--accent); }
.btn-accent:hover { background: var(--accent-2); }
.btn-sm { padding: 5px 12px; font-size: 0.75rem; }
/* Live indicator */
.live-indicator {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
color: var(--green);
}
.pulse {
width: 8px;
height: 8px;
background: var(--green);
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; transform: scale(1); }
70% { opacity: 0.4; transform: scale(1.3); }
100% { opacity: 1; transform: scale(1); }
}
/* Logs */
.log-container {
background: #06080e;
border-radius: var(--radius);
padding: 16px;
max-height: 500px;
overflow-y: auto;
}
.log-output {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.82rem;
line-height: 1.6;
color: #a0aab8;
white-space: pre-wrap;
word-break: break-word;
}
/* Ticker grid */
.ticker-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
}
.ticker-chip {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
color: var(--text);
}
.ticker-chip:hover {
border-color: var(--accent);
color: var(--accent);
cursor: pointer;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #3a4a60; }
+419
View File
@@ -0,0 +1,419 @@
/**
* StockGNN R9700 Dashboard Frontend
*/
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const state = {
ws: null,
wsConnected: false,
currentPage: "dashboard",
charts: {},
metricsHistory: {
memory: [],
portfolio: [],
timestamps: [],
},
maxHistory: 60,
};
// ---------------------------------------------------------------------------
// Navigation
// ---------------------------------------------------------------------------
function initNavigation() {
document.querySelectorAll(".nav-item").forEach((item) => {
item.addEventListener("click", (e) => {
e.preventDefault();
const page = item.dataset.page;
switchPage(page);
});
});
}
function switchPage(page) {
state.currentPage = page;
document.querySelectorAll(".nav-item").forEach((i) => i.classList.remove("active"));
document.querySelector(`.nav-item[data-page="${page}"]`).classList.add("active");
document.querySelectorAll(".page").forEach((p) => p.classList.remove("active"));
document.getElementById(`page-${page}`).classList.add("active");
document.getElementById("page-title").textContent = page.charAt(0).toUpperCase() + page.slice(1);
if (page === "logs") loadLogs();
if (page === "data") loadDataPage();
if (page === "trading") loadTradingStatus();
if (page === "models") loadModelStatus();
}
// ---------------------------------------------------------------------------
// WebSocket
// ---------------------------------------------------------------------------
function initWebSocket() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws`;
state.ws = new WebSocket(wsUrl);
state.ws.onopen = () => {
state.wsConnected = true;
updateConnectionStatus(true);
};
state.ws.onclose = () => {
state.wsConnected = false;
updateConnectionStatus(false);
setTimeout(initWebSocket, 3000);
};
state.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleWsMessage(msg);
};
state.ws.onerror = (err) => {
console.error("WebSocket error:", err);
state.wsConnected = false;
updateConnectionStatus(false);
};
}
function updateConnectionStatus(connected) {
const el = document.getElementById("ws-status");
const dot = el.querySelector(".status-dot");
const text = el.querySelector("span:last-child");
if (connected) {
dot.classList.remove("disconnected");
text.textContent = "Connected";
} else {
dot.classList.add("disconnected");
text.textContent = "Disconnected";
}
}
function handleWsMessage(msg) {
if (msg.type === "metrics") {
updateMetrics(msg);
} else if (msg.type === "pong") {
console.log("pong", msg.timestamp);
}
}
// ---------------------------------------------------------------------------
// Metrics & Charts
// ---------------------------------------------------------------------------
function updateMetrics(data) {
const mem = data.memory;
const acc = data.account;
const model = data.model;
// Update metric cards
document.getElementById("metric-memory").textContent = `${mem.usage_percent}%`;
document.getElementById("metric-memory-sub").textContent = `${mem.allocated_gb} / ${mem.total_gb} GB`;
document.getElementById("metric-portfolio").textContent = `$${acc.total_value.toLocaleString()}`;
document.getElementById("metric-portfolio-sub").textContent = `${acc.positions} positions`;
document.getElementById("metric-cash").textContent = `$${acc.cash.toLocaleString()}`;
document.getElementById("metric-model").textContent = model.status;
document.getElementById("metric-model-sub").textContent = `${model.device} · ${model.precision}`;
// Update system info
document.getElementById("sys-device").textContent = model.device;
document.getElementById("sys-precision").textContent = model.precision;
// Update device badge
const badge = document.getElementById("device-badge");
badge.textContent = model.device.toUpperCase();
if (model.device === "cuda") {
badge.style.background = "var(--green-dim)";
badge.style.color = "#fff";
}
// Update history
state.metricsHistory.memory.push(mem.usage_percent);
state.metricsHistory.portfolio.push(acc.total_value);
state.metricsHistory.timestamps.push(new Date(data.timestamp).toLocaleTimeString());
if (state.metricsHistory.memory.length > state.maxHistory) {
state.metricsHistory.memory.shift();
state.metricsHistory.portfolio.shift();
state.metricsHistory.timestamps.shift();
}
updateCharts();
}
function initCharts() {
const ctxMem = document.getElementById("memory-chart").getContext("2d");
const ctxPort = document.getElementById("portfolio-chart").getContext("2d");
state.charts.memory = new Chart(ctxMem, {
type: "line",
data: {
labels: [],
datasets: [
{
label: "Memory %",
data: [],
borderColor: "#ff6b35",
backgroundColor: "rgba(255,107,53,0.1)",
fill: true,
tension: 0.4,
pointRadius: 0,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { display: false },
y: {
beginAtZero: true,
max: 100,
grid: { color: "rgba(255,255,255,0.04)" },
ticks: { color: "#8b96a7", font: { size: 10 } },
},
},
},
});
state.charts.portfolio = new Chart(ctxPort, {
type: "line",
data: {
labels: [],
datasets: [
{
label: "Portfolio Value",
data: [],
borderColor: "#22c55e",
backgroundColor: "rgba(34,197,94,0.1)",
fill: true,
tension: 0.4,
pointRadius: 0,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { display: false },
y: {
grid: { color: "rgba(255,255,255,0.04)" },
ticks: { color: "#8b96a7", font: { size: 10 } },
},
},
},
});
}
function updateCharts() {
if (!state.charts.memory) return;
state.charts.memory.data.labels = state.metricsHistory.timestamps;
state.charts.memory.data.datasets[0].data = state.metricsHistory.memory;
state.charts.memory.update("none");
state.charts.portfolio.data.labels = state.metricsHistory.timestamps;
state.charts.portfolio.data.datasets[0].data = state.metricsHistory.portfolio;
state.charts.portfolio.update("none");
}
// ---------------------------------------------------------------------------
// API Helpers
// ---------------------------------------------------------------------------
async function apiGet(path) {
const res = await fetch(path);
return res.json();
}
async function apiPost(path, body = {}) {
const res = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return res.json();
}
// ---------------------------------------------------------------------------
// Dashboard
// ---------------------------------------------------------------------------
async function loadDashboardMetrics() {
const data = await apiGet("/api/dashboard/metrics");
if (data.model) {
document.getElementById("sys-device").textContent = data.model.device;
document.getElementById("sys-precision").textContent = data.model.precision;
document.getElementById("sys-hidden").textContent = data.model.hidden_channels;
document.getElementById("sys-heads").textContent = data.model.num_heads;
document.getElementById("sys-batch").textContent = data.model.batch_size;
document.getElementById("sys-lr").textContent = data.model.learning_rate;
}
}
// ---------------------------------------------------------------------------
// Trading Page
// ---------------------------------------------------------------------------
async function loadTradingStatus() {
const data = await apiGet("/api/trading/status");
document.getElementById("trading-status").textContent = data.active ? "Active" : "Stopped";
document.getElementById("trading-status").style.color = data.active ? "var(--green)" : "var(--text)";
document.getElementById("orders-count").textContent = data.orders_count || 0;
document.getElementById("positions-count").textContent = data.positions ? Object.keys(data.positions).length : 0;
// Positions table
const tbody = document.querySelector("#positions-table tbody");
tbody.innerHTML = "";
if (data.positions) {
Object.entries(data.positions).forEach(([ticker, qty]) => {
const tr = document.createElement("tr");
tr.innerHTML = `<td>${ticker}</td><td>${qty}</td><td><button class="btn btn-sm btn-danger" onclick="closePosition('${ticker}')">Close</button></td>`;
tbody.appendChild(tr);
});
}
// Orders table
const orders = await apiGet("/api/trading/orders");
const otbody = document.querySelector("#orders-table tbody");
otbody.innerHTML = "";
orders.forEach((order) => {
const tr = document.createElement("tr");
tr.innerHTML = `<td>${order.order_id.slice(0, 8)}</td><td>${order.ticker}</td><td>${order.action}</td><td>${order.quantity}</td><td>${order.status}</td>`;
otbody.appendChild(tr);
});
}
async function closePosition(ticker) {
await apiPost(`/api/trading/positions/close/${ticker}`);
loadTradingStatus();
}
// ---------------------------------------------------------------------------
// Models Page
// ---------------------------------------------------------------------------
async function loadModelStatus() {
const data = await apiGet("/api/models/status");
document.getElementById("model-status-page").textContent = data.status;
document.getElementById("model-status-detail").textContent = `${data.device} · ${data.precision}`;
document.getElementById("training-status-page").textContent = data.training_active ? "Active" : "Idle";
document.getElementById("training-status-page").style.color = data.training_active ? "var(--green)" : "var(--text)";
}
// ---------------------------------------------------------------------------
// Data Page
// ---------------------------------------------------------------------------
async function loadDataPage() {
const tickers = await apiGet("/api/data/tickers");
document.getElementById("data-tickers-count").textContent = tickers.count;
const grid = document.getElementById("ticker-grid");
grid.innerHTML = "";
tickers.initial.forEach((ticker) => {
const chip = document.createElement("div");
chip.className = "ticker-chip";
chip.textContent = ticker;
grid.appendChild(chip);
});
const pipe = await apiGet("/api/data/pipeline/status");
document.getElementById("data-pipeline-status").textContent = pipe.status;
}
// ---------------------------------------------------------------------------
// Logs Page
// ---------------------------------------------------------------------------
async function loadLogs() {
const data = await apiGet("/api/dashboard/logs?limit=200");
const el = document.getElementById("log-output");
if (data.logs && data.logs.length) {
el.textContent = data.logs.join("\n");
} else {
el.textContent = "No logs yet.";
}
// Auto-scroll
const container = document.querySelector(".log-container");
container.scrollTop = container.scrollHeight;
}
// ---------------------------------------------------------------------------
// Event Listeners
// ---------------------------------------------------------------------------
function initEventListeners() {
document.getElementById("refresh-btn").addEventListener("click", () => {
if (state.currentPage === "dashboard") loadDashboardMetrics();
if (state.currentPage === "trading") loadTradingStatus();
if (state.currentPage === "models") loadModelStatus();
if (state.currentPage === "data") loadDataPage();
if (state.currentPage === "logs") loadLogs();
});
// Trading controls
document.getElementById("btn-start-trading").addEventListener("click", async () => {
await apiPost("/api/trading/start");
loadTradingStatus();
});
document.getElementById("btn-stop-trading").addEventListener("click", async () => {
await apiPost("/api/trading/stop");
loadTradingStatus();
});
document.getElementById("btn-close-all").addEventListener("click", async () => {
await apiPost("/api/trading/positions/close-all");
loadTradingStatus();
});
// Model controls
document.getElementById("btn-train").addEventListener("click", async () => {
await apiPost("/api/models/train");
loadModelStatus();
});
document.getElementById("btn-stop-train").addEventListener("click", async () => {
await apiPost("/api/models/train/stop");
loadModelStatus();
});
document.getElementById("btn-save-model").addEventListener("click", async () => {
const res = await apiPost("/api/models/save");
alert(res.status);
});
document.getElementById("btn-load-model").addEventListener("click", async () => {
const res = await apiPost("/api/models/load");
alert(res.status);
loadModelStatus();
});
document.getElementById("btn-benchmark").addEventListener("click", async () => {
document.getElementById("bench-inf").textContent = "Running...";
const res = await apiPost("/api/models/benchmark");
if (res.status === "complete") {
document.getElementById("bench-inf").textContent = `${res.inference_time_ms} ms`;
document.getElementById("bench-train").textContent = `${res.training_time_ms} ms`;
document.getElementById("bench-inf-tput").textContent = `${res.inference_throughput} samples/s`;
document.getElementById("bench-train-tput").textContent = `${res.training_throughput} samples/s`;
document.getElementById("bench-mem").textContent = `${res.memory_allocated_gb} GB`;
document.getElementById("bench-device").textContent = res.device;
}
});
// Data controls
document.getElementById("btn-update-data").addEventListener("click", async () => {
await apiPost("/api/data/update");
alert("Data update started");
});
// Logs controls
document.getElementById("btn-clear-logs").addEventListener("click", async () => {
await apiPost("/api/dashboard/logs/clear");
loadLogs();
});
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
document.addEventListener("DOMContentLoaded", () => {
initNavigation();
initWebSocket();
initCharts();
initEventListeners();
loadDashboardMetrics();
});
+268
View File
@@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>StockGNN R9700 Dashboard</title>
<link rel="stylesheet" href="/static/css/style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
<div class="app">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo">
<span class="logo-icon"></span>
<span class="logo-text">StockGNN <span class="logo-badge">R9700</span></span>
</div>
<div class="device-badge" id="device-badge">CPU</div>
</div>
<nav class="nav">
<a href="#" class="nav-item active" data-page="dashboard">
<span class="nav-icon"></span>
<span>Dashboard</span>
</a>
<a href="#" class="nav-item" data-page="trading">
<span class="nav-icon"></span>
<span>Trading</span>
</a>
<a href="#" class="nav-item" data-page="models">
<span class="nav-icon"></span>
<span>Models</span>
</a>
<a href="#" class="nav-item" data-page="data">
<span class="nav-icon"></span>
<span>Data</span>
</a>
<a href="#" class="nav-item" data-page="logs">
<span class="nav-icon"></span>
<span>Logs</span>
</a>
</nav>
<div class="sidebar-footer">
<div class="connection-status" id="ws-status">
<span class="status-dot disconnected"></span>
<span>Disconnected</span>
</div>
<div class="version">v1.0.0</div>
</div>
</aside>
<!-- Main Content -->
<main class="main">
<!-- Top Bar -->
<header class="topbar">
<h1 class="page-title" id="page-title">Dashboard</h1>
<div class="topbar-actions">
<button class="btn btn-secondary" id="refresh-btn">↻ Refresh</button>
</div>
</header>
<!-- Dashboard Page -->
<div class="page active" id="page-dashboard">
<div class="grid grid-4">
<div class="card metric-card">
<div class="metric-label">GPU Memory</div>
<div class="metric-value" id="metric-memory">--</div>
<div class="metric-sub" id="metric-memory-sub">-- / -- GB</div>
</div>
<div class="card metric-card">
<div class="metric-label">Portfolio Value</div>
<div class="metric-value" id="metric-portfolio">--</div>
<div class="metric-sub" id="metric-portfolio-sub">-- positions</div>
</div>
<div class="card metric-card">
<div class="metric-label">Cash</div>
<div class="metric-value" id="metric-cash">--</div>
<div class="metric-sub" id="metric-cash-sub">available</div>
</div>
<div class="card metric-card">
<div class="metric-label">Model Status</div>
<div class="metric-value" id="metric-model">--</div>
<div class="metric-sub" id="metric-model-sub">--</div>
</div>
</div>
<div class="grid grid-2">
<div class="card chart-card">
<div class="card-header">
<h3>Memory Usage</h3>
<span class="live-indicator"><span class="pulse"></span>Live</span>
</div>
<div class="chart-container">
<canvas id="memory-chart"></canvas>
</div>
</div>
<div class="card chart-card">
<div class="card-header">
<h3>Portfolio Value</h3>
<span class="live-indicator"><span class="pulse"></span>Live</span>
</div>
<div class="chart-container">
<canvas id="portfolio-chart"></canvas>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>System Info</h3>
</div>
<div class="info-grid" id="system-info">
<div class="info-row"><span class="info-key">Project</span><span class="info-value">StockGNN R9700</span></div>
<div class="info-row"><span class="info-key">Device</span><span class="info-value" id="sys-device">--</span></div>
<div class="info-row"><span class="info-key">Precision</span><span class="info-value" id="sys-precision">--</span></div>
<div class="info-row"><span class="info-key">Hidden Channels</span><span class="info-value" id="sys-hidden">--</span></div>
<div class="info-row"><span class="info-key">Attention Heads</span><span class="info-value" id="sys-heads">--</span></div>
<div class="info-row"><span class="info-key">Batch Size</span><span class="info-value" id="sys-batch">--</span></div>
<div class="info-row"><span class="info-key">Learning Rate</span><span class="info-value" id="sys-lr">--</span></div>
</div>
</div>
</div>
<!-- Trading Page -->
<div class="page" id="page-trading">
<div class="grid grid-3">
<div class="card metric-card">
<div class="metric-label">Trading</div>
<div class="metric-value" id="trading-status">Stopped</div>
<div class="metric-sub">
<button class="btn btn-primary" id="btn-start-trading">Start</button>
<button class="btn btn-danger" id="btn-stop-trading">Stop</button>
</div>
</div>
<div class="card metric-card">
<div class="metric-label">Open Orders</div>
<div class="metric-value" id="orders-count">0</div>
<div class="metric-sub">total orders</div>
</div>
<div class="card metric-card">
<div class="metric-label">Positions</div>
<div class="metric-value" id="positions-count">0</div>
<div class="metric-sub">active positions</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-header">
<h3>Positions</h3>
<button class="btn btn-sm btn-danger" id="btn-close-all">Close All</button>
</div>
<div class="table-container">
<table class="data-table" id="positions-table">
<thead>
<tr><th>Ticker</th><th>Qty</th><th>Action</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>Orders</h3>
</div>
<div class="table-container">
<table class="data-table" id="orders-table">
<thead>
<tr><th>ID</th><th>Ticker</th><th>Action</th><th>Qty</th><th>Status</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Models Page -->
<div class="page" id="page-models">
<div class="grid grid-3">
<div class="card metric-card">
<div class="metric-label">Model Status</div>
<div class="metric-value" id="model-status-page">--</div>
<div class="metric-sub" id="model-status-detail">--</div>
</div>
<div class="card metric-card">
<div class="metric-label">Training</div>
<div class="metric-value" id="training-status-page">--</div>
<div class="metric-sub">
<button class="btn btn-primary" id="btn-train">Train</button>
<button class="btn btn-secondary" id="btn-stop-train">Stop</button>
</div>
</div>
<div class="card metric-card">
<div class="metric-label">Actions</div>
<div class="metric-value" style="font-size: 1.1rem; margin: 8px 0;">
<button class="btn btn-secondary" id="btn-save-model">Save</button>
<button class="btn btn-secondary" id="btn-load-model">Load</button>
</div>
<div class="metric-sub">
<button class="btn btn-accent" id="btn-benchmark">Benchmark</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>Benchmark Results</h3>
</div>
<div class="info-grid" id="benchmark-results">
<div class="info-row"><span class="info-key">Inference Time</span><span class="info-value" id="bench-inf">--</span></div>
<div class="info-row"><span class="info-key">Training Time</span><span class="info-value" id="bench-train">--</span></div>
<div class="info-row"><span class="info-key">Inf Throughput</span><span class="info-value" id="bench-inf-tput">--</span></div>
<div class="info-row"><span class="info-key">Train Throughput</span><span class="info-value" id="bench-train-tput">--</span></div>
<div class="info-row"><span class="info-key">Memory Used</span><span class="info-value" id="bench-mem">--</span></div>
<div class="info-row"><span class="info-key">Device</span><span class="info-value" id="bench-device">--</span></div>
</div>
</div>
</div>
<!-- Data Page -->
<div class="page" id="page-data">
<div class="grid grid-3">
<div class="card metric-card">
<div class="metric-label">Tickers</div>
<div class="metric-value" id="data-tickers-count">--</div>
<div class="metric-sub">tracked stocks</div>
</div>
<div class="card metric-card">
<div class="metric-label">Data Pipeline</div>
<div class="metric-value" id="data-pipeline-status">--</div>
<div class="metric-sub">
<button class="btn btn-primary" id="btn-update-data">Update</button>
</div>
</div>
<div class="card metric-card">
<div class="metric-label">Latest</div>
<div class="metric-value">--</div>
<div class="metric-sub">price data</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>Ticker List</h3>
</div>
<div class="ticker-grid" id="ticker-grid"></div>
</div>
</div>
<!-- Logs Page -->
<div class="page" id="page-logs">
<div class="card">
<div class="card-header">
<h3>System Logs</h3>
<button class="btn btn-sm btn-secondary" id="btn-clear-logs">Clear</button>
</div>
<div class="log-container">
<pre class="log-output" id="log-output">Loading logs...</pre>
</div>
</div>
</div>
</main>
</div>
<script src="/static/js/app.js"></script>
</body>
</html>