initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
implementation.md
|
||||
+176
@@ -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()
|
||||
@@ -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()
|
||||
Vendored
+569
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
# src package
|
||||
@@ -0,0 +1 @@
|
||||
# AMD optimizations package
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 {}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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}")
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
# Web frontend package
|
||||
@@ -0,0 +1 @@
|
||||
# web api package
|
||||
@@ -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"}
|
||||
@@ -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]}
|
||||
@@ -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
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# web services package
|
||||
@@ -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:]
|
||||
@@ -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; }
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user