Compare commits

..

24 commits

Author SHA1 Message Date
5ded78f8e4 simple test 2025-06-12 08:03:45 -04:00
54314a0cde refactor of data-service 2025-06-12 08:03:09 -04:00
3097686849 fixed batching and waiting priority plus cleanup 2025-06-11 12:56:07 -04:00
d9bd33a822 testing 2025-06-11 11:11:47 -04:00
07f8964a8c prettier configs 2025-06-11 10:41:33 -04:00
eeae192872 linting 2025-06-11 10:38:05 -04:00
9d38f9a7b6 eslint 2025-06-11 10:35:15 -04:00
8955544593 running prettier for cleanup 2025-06-11 10:13:25 -04:00
24b7ed15e4 working on queue 2025-06-11 09:53:04 -04:00
be807378a3 fixed up delay time 2025-06-11 08:33:36 -04:00
16599c86da added env back and fixed up queue service 2025-06-11 08:03:55 -04:00
b645b58102 simplifid queue service 2025-06-11 07:28:47 -04:00
709fc347e9 queue service simplification 2025-06-10 23:35:33 -04:00
423b40866c made provider registry functional 2025-06-10 23:26:30 -04:00
aed5ff3d98 removed examples 2025-06-10 23:09:29 -04:00
84e6dee53f removed examples 2025-06-10 23:09:16 -04:00
4aa2942e43 simplified providers a bit 2025-06-10 23:08:46 -04:00
35b0eb3783 moved proxy redis init to app start 2025-06-10 22:50:10 -04:00
a7ec942916 added more specific batch keys 2025-06-10 22:43:51 -04:00
ed326c025e cleanup old init code on batcher 2025-06-10 22:28:56 -04:00
47ff92b567 still trying 2025-06-10 22:16:11 -04:00
2f074271cc trying to get simpler batcher working 2025-06-10 22:00:58 -04:00
df611a3ce3 cleaned up index 2025-06-10 21:06:01 -04:00
b49bea818b added routes and simplified batch processor 2025-06-10 20:59:53 -04:00
1142 changed files with 31198 additions and 121547 deletions

View file

@ -1,26 +0,0 @@
{
"exclude": [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/*.test.ts",
"**/*.test.js",
"**/*.spec.ts",
"**/*.spec.js",
"**/test/**",
"**/tests/**",
"**/__tests__/**",
"**/__mocks__/**",
"**/setup.ts",
"**/setup.js"
],
"reporters": ["terminal", "html"],
"thresholds": {
"lines": 80,
"functions": 80,
"branches": 80,
"statements": 80
},
"outputDir": "coverage"
}

36
.env
View file

@ -4,19 +4,16 @@
# Core Application Settings
NODE_ENV=development
LOG_LEVEL=debug
LOG_HIDE_OBJECT=true
LOG_LEVEL=info
# Data Service Configuration
DATA_SERVICE_PORT=2001
# Queue and Worker Configuration
WORKER_COUNT=1
WORKER_CONCURRENCY=2
WORKER_COUNT=4
WORKER_CONCURRENCY=20
WEBSHARE_API_KEY=y8ay534rcbybdkk3evnzmt640xxfhy7252ce2t98
WEBSHARE_ROTATING_PROXY_URL=http://doimvbnb-rotate:w5fpiwrb9895@p.webshare.io:80/
WEBSHARE_API_URL=https://proxy.webshare.io/api/v2/
# ===========================================
# DATABASE CONFIGURATIONS
@ -30,25 +27,25 @@ DRAGONFLY_PASSWORD=
# PostgreSQL Configuration
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=trading_bot
POSTGRES_USERNAME=trading_user
POSTGRES_PASSWORD=trading_pass_dev
POSTGRES_DB=stockbot
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_SSL=false
# QuestDB Configuration
QUESTDB_HOST=localhost
QUESTDB_PORT=9000
QUESTDB_DB=qdb
QUESTDB_USERNAME=admin
QUESTDB_USER=admin
QUESTDB_PASSWORD=quest
# MongoDB Configuration
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DATABASE=stock
MONGODB_USERNAME=trading_admin
MONGODB_PASSWORD=trading_mongo_dev
MONGODB_URI=mongodb://trading_admin:trading_mongo_dev@localhost:27017/stock?authSource=admin
MONGODB_DB=stockbot
MONGODB_USER=
MONGODB_PASSWORD=
MONGODB_URI=mongodb://localhost:27017/stockbot
# ===========================================
# DATA PROVIDER CONFIGURATIONS
@ -162,13 +159,6 @@ DOCKER_BUILDKIT=1
TZ=UTC
# Application Metadata
APP_NAME="Stock Bot Platform"
APP_NAME=Stock Bot Platform
APP_VERSION=1.0.0
APP_DESCRIPTION="Advanced Stock Trading and Analysis Platform"
# PostgreSQL
DATABASE_POSTGRES_HOST=localhost
DATABASE_POSTGRES_PORT=5432
DATABASE_POSTGRES_DATABASE=trading_bot
DATABASE_POSTGRES_USER=trading_user
DATABASE_POSTGRES_PASSWORD=trading_pass_dev
APP_DESCRIPTION=Advanced Stock Trading and Analysis Platform

242
.env.complete Normal file
View file

@ -0,0 +1,242 @@
# =======================================================================
# Stock Bot Platform Environment Configuration
# =======================================================================
# Core Application Settings
NODE_ENV=development
PORT=3001
APP_NAME=stock-bot
APP_VERSION=1.0.0
# =======================================================================
# DATABASE CONFIGURATIONS
# =======================================================================
# PostgreSQL - Operational Data (orders, positions, strategies)
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=trading_bot
POSTGRES_USERNAME=trading_user
POSTGRES_PASSWORD=trading_pass_dev
DB_HOST=localhost
DB_PORT=5432
DB_NAME=trading_bot
DB_USER=trading_user
DB_PASSWORD=trading_pass_dev
DB_POOL_MIN=2
DB_POOL_MAX=10
DB_POOL_IDLE_TIMEOUT=30000
DB_SSL=false
DB_SSL_REJECT_UNAUTHORIZED=true
DB_QUERY_TIMEOUT=30000
DB_CONNECTION_TIMEOUT=5000
# QuestDB - Time-series Data (OHLCV, indicators, performance)
QUESTDB_HOST=localhost
QUESTDB_HTTP_PORT=9000
QUESTDB_PG_PORT=8812
QUESTDB_INFLUX_PORT=9009
QUESTDB_USER=
QUESTDB_PASSWORD=
QUESTDB_CONNECTION_TIMEOUT=5000
QUESTDB_REQUEST_TIMEOUT=30000
QUESTDB_RETRY_ATTEMPTS=3
QUESTDB_TLS_ENABLED=false
QUESTDB_DEFAULT_DATABASE=qdb
QUESTDB_TELEMETRY_ENABLED=false
# MongoDB - Document Storage (sentiment, raw docs, unstructured data)
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DATABASE=trading_documents
MONGODB_USERNAME=trading_admin
MONGODB_PASSWORD=trading_mongo_dev
MONGODB_AUTH_SOURCE=admin
MONGODB_URI=
MONGODB_MAX_POOL_SIZE=10
MONGODB_MIN_POOL_SIZE=0
MONGODB_MAX_IDLE_TIME=30000
MONGODB_CONNECT_TIMEOUT=10000
MONGODB_SOCKET_TIMEOUT=30000
MONGODB_SERVER_SELECTION_TIMEOUT=5000
MONGODB_TLS=false
MONGODB_RETRY_WRITES=true
MONGODB_JOURNAL=true
MONGODB_READ_PREFERENCE=primary
MONGODB_WRITE_CONCERN=majority
# Dragonfly - Redis Replacement (caching and events)
DRAGONFLY_HOST=localhost
DRAGONFLY_PORT=6379
DRAGONFLY_PASSWORD=
DRAGONFLY_USERNAME=
DRAGONFLY_DATABASE=0
DRAGONFLY_MAX_RETRIES=3
DRAGONFLY_RETRY_DELAY=50
DRAGONFLY_CONNECT_TIMEOUT=10000
DRAGONFLY_COMMAND_TIMEOUT=5000
DRAGONFLY_POOL_SIZE=10
DRAGONFLY_POOL_MIN=1
DRAGONFLY_POOL_MAX=20
DRAGONFLY_TLS=false
DRAGONFLY_ENABLE_KEEPALIVE=true
DRAGONFLY_KEEPALIVE_INTERVAL=60
DRAGONFLY_CLUSTER_MODE=false
DRAGONFLY_CLUSTER_NODES=
DRAGONFLY_MAX_MEMORY=2gb
DRAGONFLY_CACHE_MODE=true
# =======================================================================
# MONITORING & LOGGING CONFIGURATIONS
# =======================================================================
# Logging Configuration
LOG_LEVEL=debug
LOG_FORMAT=json
LOG_CONSOLE=true
LOG_FILE=false
LOG_FILE_PATH=logs
LOG_FILE_MAX_SIZE=20m
LOG_FILE_MAX_FILES=14
LOG_FILE_DATE_PATTERN=YYYY-MM-DD
LOG_ERROR_FILE=true
LOG_ERROR_STACK=true
LOG_PERFORMANCE=false
LOG_SQL_QUERIES=false
LOG_HTTP_REQUESTS=true
LOG_STRUCTURED=true
LOG_TIMESTAMP=true
LOG_CALLER_INFO=false
LOG_SILENT_MODULES=
LOG_VERBOSE_MODULES=
LOG_SERVICE_NAME=stock-bot
LOG_SERVICE_VERSION=1.0.0
LOG_ENVIRONMENT=development
# Loki - Log Aggregation
LOKI_HOST=localhost
LOKI_PORT=3100
LOKI_URL=
LOKI_USERNAME=
LOKI_PASSWORD=
LOKI_TENANT_ID=
LOKI_PUSH_TIMEOUT=10000
LOKI_BATCH_SIZE=1024
LOKI_BATCH_WAIT=1000
LOKI_RETENTION_PERIOD=30d
LOKI_MAX_CHUNK_AGE=1h
LOKI_TLS_ENABLED=false
LOKI_TLS_INSECURE=false
LOKI_DEFAULT_LABELS=
LOKI_SERVICE_LABEL=stock-bot
LOKI_ENVIRONMENT_LABEL=development
# Prometheus - Metrics Collection
PROMETHEUS_HOST=localhost
PROMETHEUS_PORT=9090
PROMETHEUS_URL=
PROMETHEUS_USERNAME=
PROMETHEUS_PASSWORD=
PROMETHEUS_SCRAPE_INTERVAL=15s
PROMETHEUS_EVALUATION_INTERVAL=15s
PROMETHEUS_RETENTION_TIME=15d
PROMETHEUS_TLS_ENABLED=false
PROMETHEUS_TLS_INSECURE=false
# Grafana - Visualization
GRAFANA_HOST=localhost
GRAFANA_PORT=3000
GRAFANA_URL=
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
GRAFANA_ALLOW_SIGN_UP=false
GRAFANA_SECRET_KEY=
GRAFANA_DATABASE_TYPE=sqlite3
GRAFANA_DATABASE_URL=
GRAFANA_DISABLE_GRAVATAR=true
GRAFANA_ENABLE_GZIP=true
# =======================================================================
# DATA PROVIDER CONFIGURATIONS
# =======================================================================
# Default Data Provider
DEFAULT_DATA_PROVIDER=alpaca
# Alpaca Markets
ALPACA_ENABLED=true
ALPACA_API_KEY=your_alpaca_key_here
ALPACA_SECRET_KEY=your_alpaca_secret_here
ALPACA_BASE_URL=https://paper-api.alpaca.markets
ALPACA_DATA_URL=https://data.alpaca.markets
ALPACA_PAPER_TRADING=true
# Polygon.io
POLYGON_ENABLED=false
POLYGON_API_KEY=your_polygon_key_here
POLYGON_BASE_URL=https://api.polygon.io
# Yahoo Finance
YAHOO_ENABLED=true
YAHOO_BASE_URL=https://query1.finance.yahoo.com
# IEX Cloud
IEX_ENABLED=false
IEX_API_KEY=your_iex_key_here
IEX_BASE_URL=https://cloud.iexapis.com
# Alpha Vantage
ALPHA_VANTAGE_ENABLED=false
ALPHA_VANTAGE_API_KEY=demo
# Data Provider Settings
DATA_PROVIDER_TIMEOUT=30000
DATA_PROVIDER_RETRIES=3
DATA_PROVIDER_RETRY_DELAY=1000
DATA_CACHE_ENABLED=true
DATA_CACHE_TTL=300
DATA_CACHE_MAX_SIZE=1000
# =======================================================================
# TRADING & RISK MANAGEMENT
# =======================================================================
# Trading Configuration
PAPER_TRADING=true
MAX_POSITION_SIZE=0.1
MAX_DAILY_LOSS=1000
# Risk Management
RISK_MAX_POSITION_SIZE=0.25
RISK_MAX_LEVERAGE=2.0
RISK_DEFAULT_STOP_LOSS=0.02
RISK_DEFAULT_TAKE_PROFIT=0.06
RISK_MAX_DRAWDOWN=0.10
RISK_MAX_CONSECUTIVE_LOSSES=5
RISK_POSITION_SIZING_METHOD=fixed_percentage
RISK_CIRCUIT_BREAKER_ENABLED=true
RISK_CIRCUIT_BREAKER_THRESHOLD=0.05
RISK_CIRCUIT_BREAKER_COOLDOWN=3600000
RISK_ALLOW_WEEKEND_TRADING=false
RISK_MARKET_HOURS_ONLY=true
# =======================================================================
# FEATURE FLAGS
# =======================================================================
ENABLE_ML_SIGNALS=false
ENABLE_SENTIMENT_ANALYSIS=false
ENABLE_SOCIAL_SIGNALS=false
ENABLE_OPTIONS_TRADING=false
ENABLE_CRYPTO_TRADING=false
ENABLE_BACKTESTING=true
ENABLE_PAPER_TRADING=true
ENABLE_LIVE_TRADING=false
# =======================================================================
# DEVELOPMENT & DEBUGGING
# =======================================================================
DEBUG_MODE=true
VERBOSE_LOGGING=true
MOCK_DATA_PROVIDERS=false
ENABLE_API_RATE_LIMITING=true
ENABLE_REQUEST_LOGGING=true

144
.env.docker Normal file
View file

@ -0,0 +1,144 @@
# Docker Environment Configuration
# This file contains environment variables used by Docker Compose
# =============================================================================
# CONTAINER NETWORK SETTINGS
# =============================================================================
COMPOSE_PROJECT_NAME=stock-bot
NETWORK_NAME=trading-bot-network
# =============================================================================
# DATABASE CONTAINER SETTINGS
# =============================================================================
# PostgreSQL Container
POSTGRES_DB=trading_bot
POSTGRES_USER=trading_user
POSTGRES_PASSWORD=trading_pass_secure
POSTGRES_INITDB_ARGS=--encoding=UTF-8
# MongoDB Container
MONGO_INITDB_ROOT_USERNAME=trading_admin
MONGO_INITDB_ROOT_PASSWORD=trading_mongo_secure
MONGO_INITDB_DATABASE=trading_documents
# QuestDB Container
QDB_TELEMETRY_ENABLED=false
# Dragonfly Container
DRAGONFLY_MAXMEMORY=4gb
DRAGONFLY_PROACTOR_THREADS=8
# =============================================================================
# MONITORING CONTAINER SETTINGS
# =============================================================================
# Grafana Container
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=secure_grafana_password
GF_USERS_ALLOW_SIGN_UP=false
GF_PATHS_PROVISIONING=/etc/grafana/provisioning
GF_DISABLE_GRAVATAR=true
# Prometheus Container
PROMETHEUS_CONFIG_FILE=/etc/prometheus/prometheus.yml
PROMETHEUS_STORAGE_PATH=/prometheus
PROMETHEUS_WEB_ENABLE_LIFECYCLE=true
# =============================================================================
# ADMIN INTERFACE CONTAINER SETTINGS
# =============================================================================
# PgAdmin Container
PGADMIN_DEFAULT_EMAIL=admin@tradingbot.local
PGADMIN_DEFAULT_PASSWORD=secure_pgadmin_password
PGADMIN_CONFIG_SERVER_MODE=False
PGADMIN_DISABLE_POSTFIX=true
# Mongo Express Container
ME_CONFIG_MONGODB_ADMINUSERNAME=trading_admin
ME_CONFIG_MONGODB_ADMINPASSWORD=trading_mongo_secure
ME_CONFIG_MONGODB_SERVER=mongodb
ME_CONFIG_MONGODB_PORT=27017
ME_CONFIG_BASICAUTH_USERNAME=admin
ME_CONFIG_BASICAUTH_PASSWORD=secure_mongo_express_password
# Redis Insight Container
REDIS_HOSTS=local:dragonfly:6379
# =============================================================================
# VOLUME MOUNT PATHS
# =============================================================================
# Data Volume Paths (adjust these for your host system)
POSTGRES_DATA_PATH=./data/postgres
QUESTDB_DATA_PATH=./data/questdb
MONGODB_DATA_PATH=./data/mongodb
DRAGONFLY_DATA_PATH=./data/dragonfly
PROMETHEUS_DATA_PATH=./data/prometheus
GRAFANA_DATA_PATH=./data/grafana
LOKI_DATA_PATH=./data/loki
PGADMIN_DATA_PATH=./data/pgadmin
# Config Volume Paths
PROMETHEUS_CONFIG_PATH=./monitoring/prometheus
GRAFANA_CONFIG_PATH=./monitoring/grafana
LOKI_CONFIG_PATH=./monitoring/loki
# Database Init Paths
POSTGRES_INIT_PATH=./database/postgres/init
MONGODB_INIT_PATH=./database/mongodb/init
# =============================================================================
# PORT MAPPINGS (HOST:CONTAINER)
# =============================================================================
# Database Ports
POSTGRES_PORT=5432
QUESTDB_HTTP_PORT=9000
QUESTDB_PG_PORT=8812
QUESTDB_INFLUX_PORT=9009
MONGODB_PORT=27017
DRAGONFLY_PORT=6379
# Monitoring Ports
PROMETHEUS_PORT=9090
GRAFANA_PORT=3000
LOKI_PORT=3100
# Admin Interface Ports
PGADMIN_PORT=8080
MONGO_EXPRESS_PORT=8081
REDIS_INSIGHT_PORT=8001
# =============================================================================
# HEALTH CHECK SETTINGS
# =============================================================================
# Health Check Intervals
HEALTHCHECK_INTERVAL=30s
HEALTHCHECK_TIMEOUT=10s
HEALTHCHECK_RETRIES=3
HEALTHCHECK_START_PERIOD=60s
# =============================================================================
# RESOURCE LIMITS
# =============================================================================
# Memory Limits (uncomment and adjust for production)
# POSTGRES_MEMORY_LIMIT=2g
# QUESTDB_MEMORY_LIMIT=4g
# MONGODB_MEMORY_LIMIT=2g
# DRAGONFLY_MEMORY_LIMIT=4g
# PROMETHEUS_MEMORY_LIMIT=2g
# GRAFANA_MEMORY_LIMIT=512m
# LOKI_MEMORY_LIMIT=1g
# CPU Limits (uncomment and adjust for production)
# POSTGRES_CPU_LIMIT=1
# QUESTDB_CPU_LIMIT=2
# MONGODB_CPU_LIMIT=1
# DRAGONFLY_CPU_LIMIT=2
# PROMETHEUS_CPU_LIMIT=1
# GRAFANA_CPU_LIMIT=0.5
# LOKI_CPU_LIMIT=1

43
.env.example Normal file
View file

@ -0,0 +1,43 @@
# Environment Configuration
NODE_ENV=development
PORT=3000
# Database Configuration
QUESTDB_HOST=localhost
QUESTDB_PORT=9000
QUESTDB_DATABASE=qdb
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=stockbot
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=password
DRAGONFLY_HOST=localhost
DRAGONFLY_PORT=6379
DRAGONFLY_PASSWORD=
# API Keys (Add your actual keys here)
ALPHA_VANTAGE_API_KEY=demo
ALPACA_API_KEY=your_alpaca_key_here
ALPACA_SECRET_KEY=your_alpaca_secret_here
# Trading Configuration
PAPER_TRADING=true
MAX_POSITION_SIZE=0.1
MAX_DAILY_LOSS=1000
# Logging
LOG_LEVEL=debug
LOG_CONSOLE=true
LOKI_HOST=localhost
LOKI_PORT=3100
LOKI_USERNAME=
LOKI_PASSWORD=
LOKI_RETENTION_DAYS=30
LOKI_LABELS=environment=development,service=stock-bot
LOKI_BATCH_SIZE=100
# Feature Flags
ENABLE_ML_SIGNALS=false
ENABLE_SENTIMENT_ANALYSIS=false

233
.env.prod Normal file
View file

@ -0,0 +1,233 @@
# =======================================================================
# Stock Bot Platform Production Environment Configuration
# =======================================================================
# Core Application Settings
NODE_ENV=production
PORT=3001
APP_NAME=stock-bot
APP_VERSION=1.0.0
# =======================================================================
# DATABASE CONFIGURATIONS
# =======================================================================
# PostgreSQL - Operational Data (orders, positions, strategies)
DB_HOST=${DB_HOST}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME}
DB_USER=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}
DB_POOL_MIN=5
DB_POOL_MAX=20
DB_POOL_IDLE_TIMEOUT=60000
DB_SSL=true
DB_SSL_REJECT_UNAUTHORIZED=true
DB_QUERY_TIMEOUT=30000
DB_CONNECTION_TIMEOUT=10000
# QuestDB - Time-series Data (OHLCV, indicators, performance)
QUESTDB_HOST=${QUESTDB_HOST}
QUESTDB_HTTP_PORT=${QUESTDB_HTTP_PORT:-9000}
QUESTDB_PG_PORT=${QUESTDB_PG_PORT:-8812}
QUESTDB_INFLUX_PORT=${QUESTDB_INFLUX_PORT:-9009}
QUESTDB_USER=${QUESTDB_USER}
QUESTDB_PASSWORD=${QUESTDB_PASSWORD}
QUESTDB_CONNECTION_TIMEOUT=10000
QUESTDB_REQUEST_TIMEOUT=60000
QUESTDB_RETRY_ATTEMPTS=5
QUESTDB_TLS_ENABLED=true
QUESTDB_DEFAULT_DATABASE=qdb
QUESTDB_TELEMETRY_ENABLED=false
# MongoDB - Document Storage (sentiment, raw docs, unstructured data)
MONGODB_HOST=${MONGODB_HOST}
MONGODB_PORT=${MONGODB_PORT:-27017}
MONGODB_DATABASE=${MONGODB_DATABASE}
MONGODB_USERNAME=${MONGODB_USERNAME}
MONGODB_PASSWORD=${MONGODB_PASSWORD}
MONGODB_AUTH_SOURCE=admin
MONGODB_URI=${MONGODB_URI}
MONGODB_MAX_POOL_SIZE=50
MONGODB_MIN_POOL_SIZE=5
MONGODB_MAX_IDLE_TIME=60000
MONGODB_CONNECT_TIMEOUT=30000
MONGODB_SOCKET_TIMEOUT=60000
MONGODB_SERVER_SELECTION_TIMEOUT=10000
MONGODB_TLS=true
MONGODB_RETRY_WRITES=true
MONGODB_JOURNAL=true
MONGODB_READ_PREFERENCE=primaryPreferred
MONGODB_WRITE_CONCERN=majority
# Dragonfly - Redis Replacement (caching and events)
DRAGONFLY_HOST=${DRAGONFLY_HOST}
DRAGONFLY_PORT=${DRAGONFLY_PORT:-6379}
DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
DRAGONFLY_USERNAME=${DRAGONFLY_USERNAME}
DRAGONFLY_DATABASE=0
DRAGONFLY_MAX_RETRIES=5
DRAGONFLY_RETRY_DELAY=100
DRAGONFLY_CONNECT_TIMEOUT=30000
DRAGONFLY_COMMAND_TIMEOUT=10000
DRAGONFLY_POOL_SIZE=50
DRAGONFLY_POOL_MIN=5
DRAGONFLY_POOL_MAX=100
DRAGONFLY_TLS=true
DRAGONFLY_ENABLE_KEEPALIVE=true
DRAGONFLY_KEEPALIVE_INTERVAL=30
DRAGONFLY_CLUSTER_MODE=false
DRAGONFLY_CLUSTER_NODES=
DRAGONFLY_MAX_MEMORY=8gb
DRAGONFLY_CACHE_MODE=true
# =======================================================================
# MONITORING & LOGGING CONFIGURATIONS
# =======================================================================
# Logging Configuration (Production - Less verbose)
LOG_LEVEL=info
LOG_FORMAT=json
LOG_CONSOLE=false
LOG_FILE=true
LOG_FILE_PATH=/var/log/stock-bot
LOG_FILE_MAX_SIZE=100m
LOG_FILE_MAX_FILES=30
LOG_FILE_DATE_PATTERN=YYYY-MM-DD
LOG_ERROR_FILE=true
LOG_ERROR_STACK=false
LOG_PERFORMANCE=true
LOG_SQL_QUERIES=false
LOG_HTTP_REQUESTS=false
LOG_STRUCTURED=true
LOG_TIMESTAMP=true
LOG_CALLER_INFO=false
LOG_SILENT_MODULES=
LOG_VERBOSE_MODULES=
LOG_SERVICE_NAME=stock-bot
LOG_SERVICE_VERSION=1.0.0
LOG_ENVIRONMENT=production
# Loki - Log Aggregation
LOKI_HOST=${LOKI_HOST}
LOKI_PORT=${LOKI_PORT:-3100}
LOKI_URL=${LOKI_URL}
LOKI_USERNAME=${LOKI_USERNAME}
LOKI_PASSWORD=${LOKI_PASSWORD}
LOKI_TENANT_ID=${LOKI_TENANT_ID}
LOKI_PUSH_TIMEOUT=30000
LOKI_BATCH_SIZE=2048
LOKI_BATCH_WAIT=5000
LOKI_RETENTION_PERIOD=90d
LOKI_MAX_CHUNK_AGE=2h
LOKI_TLS_ENABLED=true
LOKI_TLS_INSECURE=false
LOKI_DEFAULT_LABELS=
LOKI_SERVICE_LABEL=stock-bot
LOKI_ENVIRONMENT_LABEL=production
# Prometheus - Metrics Collection
PROMETHEUS_HOST=${PROMETHEUS_HOST}
PROMETHEUS_PORT=${PROMETHEUS_PORT:-9090}
PROMETHEUS_URL=${PROMETHEUS_URL}
PROMETHEUS_USERNAME=${PROMETHEUS_USERNAME}
PROMETHEUS_PASSWORD=${PROMETHEUS_PASSWORD}
PROMETHEUS_SCRAPE_INTERVAL=30s
PROMETHEUS_EVALUATION_INTERVAL=30s
PROMETHEUS_RETENTION_TIME=90d
PROMETHEUS_TLS_ENABLED=true
PROMETHEUS_TLS_INSECURE=false
# Grafana - Visualization
GRAFANA_HOST=${GRAFANA_HOST}
GRAFANA_PORT=${GRAFANA_PORT:-3000}
GRAFANA_URL=${GRAFANA_URL}
GRAFANA_ADMIN_USER=${GRAFANA_ADMIN_USER}
GRAFANA_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
GRAFANA_ALLOW_SIGN_UP=false
GRAFANA_SECRET_KEY=${GRAFANA_SECRET_KEY}
GRAFANA_DATABASE_TYPE=postgres
GRAFANA_DATABASE_URL=${GRAFANA_DATABASE_URL}
GRAFANA_DISABLE_GRAVATAR=true
GRAFANA_ENABLE_GZIP=true
# =======================================================================
# DATA PROVIDER CONFIGURATIONS
# =======================================================================
# Default Data Provider
DEFAULT_DATA_PROVIDER=alpaca
# Alpaca Markets (Production)
ALPACA_ENABLED=true
ALPACA_API_KEY=${ALPACA_API_KEY}
ALPACA_SECRET_KEY=${ALPACA_SECRET_KEY}
ALPACA_BASE_URL=https://api.alpaca.markets
ALPACA_DATA_URL=https://data.alpaca.markets
ALPACA_PAPER_TRADING=false
# Polygon.io
POLYGON_ENABLED=${POLYGON_ENABLED:-false}
POLYGON_API_KEY=${POLYGON_API_KEY}
POLYGON_BASE_URL=https://api.polygon.io
# Yahoo Finance
YAHOO_ENABLED=${YAHOO_ENABLED:-false}
YAHOO_BASE_URL=https://query1.finance.yahoo.com
# IEX Cloud
IEX_ENABLED=${IEX_ENABLED:-false}
IEX_API_KEY=${IEX_API_KEY}
IEX_BASE_URL=https://cloud.iexapis.com
# Data Provider Settings (Production)
DATA_PROVIDER_TIMEOUT=60000
DATA_PROVIDER_RETRIES=5
DATA_PROVIDER_RETRY_DELAY=2000
DATA_CACHE_ENABLED=true
DATA_CACHE_TTL=60
DATA_CACHE_MAX_SIZE=10000
# =======================================================================
# TRADING & RISK MANAGEMENT (Production)
# =======================================================================
# Trading Configuration
PAPER_TRADING=false
MAX_POSITION_SIZE=${MAX_POSITION_SIZE:-0.05}
MAX_DAILY_LOSS=${MAX_DAILY_LOSS:-10000}
# Risk Management (Stricter for production)
RISK_MAX_POSITION_SIZE=${RISK_MAX_POSITION_SIZE:-0.10}
RISK_MAX_LEVERAGE=${RISK_MAX_LEVERAGE:-1.5}
RISK_DEFAULT_STOP_LOSS=${RISK_DEFAULT_STOP_LOSS:-0.015}
RISK_DEFAULT_TAKE_PROFIT=${RISK_DEFAULT_TAKE_PROFIT:-0.045}
RISK_MAX_DRAWDOWN=${RISK_MAX_DRAWDOWN:-0.05}
RISK_MAX_CONSECUTIVE_LOSSES=${RISK_MAX_CONSECUTIVE_LOSSES:-3}
RISK_POSITION_SIZING_METHOD=volatility_adjusted
RISK_CIRCUIT_BREAKER_ENABLED=true
RISK_CIRCUIT_BREAKER_THRESHOLD=0.02
RISK_CIRCUIT_BREAKER_COOLDOWN=7200000
RISK_ALLOW_WEEKEND_TRADING=false
RISK_MARKET_HOURS_ONLY=true
# =======================================================================
# FEATURE FLAGS (Production)
# =======================================================================
ENABLE_ML_SIGNALS=${ENABLE_ML_SIGNALS:-false}
ENABLE_SENTIMENT_ANALYSIS=${ENABLE_SENTIMENT_ANALYSIS:-false}
ENABLE_SOCIAL_SIGNALS=${ENABLE_SOCIAL_SIGNALS:-false}
ENABLE_OPTIONS_TRADING=${ENABLE_OPTIONS_TRADING:-false}
ENABLE_CRYPTO_TRADING=${ENABLE_CRYPTO_TRADING:-false}
ENABLE_BACKTESTING=true
ENABLE_PAPER_TRADING=false
ENABLE_LIVE_TRADING=true
# =======================================================================
# PRODUCTION SETTINGS
# =======================================================================
DEBUG_MODE=false
VERBOSE_LOGGING=false
MOCK_DATA_PROVIDERS=false
ENABLE_API_RATE_LIMITING=true
ENABLE_REQUEST_LOGGING=false

135
.env.production Normal file
View file

@ -0,0 +1,135 @@
# Production Environment Configuration
NODE_ENV=production
PORT=3001
# =============================================================================
# DATABASE CONFIGURATIONS
# =============================================================================
# PostgreSQL - Operational data (orders, positions, strategies)
DB_HOST=postgres
DB_PORT=5432
DB_NAME=trading_bot
DB_USER=trading_user
DB_PASSWORD=${POSTGRES_PASSWORD}
DB_POOL_MIN=5
DB_POOL_MAX=20
DB_SSL=true
DB_SSL_REJECT_UNAUTHORIZED=true
# QuestDB - Time-series data (OHLCV, indicators, performance)
QUESTDB_HOST=questdb
QUESTDB_HTTP_PORT=9000
QUESTDB_PG_PORT=8812
QUESTDB_INFLUX_PORT=9009
QUESTDB_DEFAULT_DATABASE=qdb
QUESTDB_TELEMETRY_ENABLED=false
QUESTDB_TLS_ENABLED=true
# MongoDB - Document storage (sentiment, raw docs, unstructured data)
MONGODB_HOST=mongodb
MONGODB_PORT=27017
MONGODB_DATABASE=trading_documents
MONGODB_USERNAME=${MONGODB_ROOT_USERNAME}
MONGODB_PASSWORD=${MONGODB_ROOT_PASSWORD}
MONGODB_AUTH_SOURCE=admin
MONGODB_TLS=true
MONGODB_RETRY_WRITES=true
# Dragonfly - Redis replacement for caching and events
DRAGONFLY_HOST=dragonfly
DRAGONFLY_PORT=6379
DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
DRAGONFLY_DATABASE=0
DRAGONFLY_MAX_MEMORY=4gb
DRAGONFLY_CACHE_MODE=true
DRAGONFLY_TLS=true
# =============================================================================
# MONITORING & OBSERVABILITY
# =============================================================================
# Prometheus - Metrics collection
PROMETHEUS_HOST=prometheus
PROMETHEUS_PORT=9090
PROMETHEUS_SCRAPE_INTERVAL=30s
PROMETHEUS_RETENTION_TIME=90d
PROMETHEUS_TLS_ENABLED=true
# Grafana - Visualization
GRAFANA_HOST=grafana
GRAFANA_PORT=3000
GRAFANA_ADMIN_USER=${GRAFANA_ADMIN_USER}
GRAFANA_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
GRAFANA_ALLOW_SIGN_UP=false
GRAFANA_SECRET_KEY=${GRAFANA_SECRET_KEY}
GRAFANA_DATABASE_TYPE=postgres
GRAFANA_DISABLE_GRAVATAR=true
# Loki - Log aggregation
LOKI_HOST=loki
LOKI_PORT=3100
LOKI_RETENTION_PERIOD=90d
LOKI_BATCH_SIZE=2048
LOKI_TLS_ENABLED=true
# =============================================================================
# ADMIN INTERFACES (Disabled in production)
# =============================================================================
# PgAdmin - PostgreSQL GUI (disabled in production)
PGADMIN_HOST=pgadmin
PGADMIN_PORT=8080
PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL}
PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}
PGADMIN_SERVER_MODE=true
PGADMIN_MASTER_PASSWORD_REQUIRED=true
# Mongo Express - MongoDB GUI (disabled in production)
MONGO_EXPRESS_HOST=mongo-express
MONGO_EXPRESS_PORT=8081
MONGO_EXPRESS_MONGODB_SERVER=mongodb
MONGO_EXPRESS_BASICAUTH_USERNAME=${MONGO_EXPRESS_USER}
MONGO_EXPRESS_BASICAUTH_PASSWORD=${MONGO_EXPRESS_PASSWORD}
# Redis Insight - Dragonfly/Redis GUI (disabled in production)
REDIS_INSIGHT_HOST=redis-insight
REDIS_INSIGHT_PORT=8001
REDIS_INSIGHT_REDIS_HOSTS=production:dragonfly:6379
# =============================================================================
# DATA PROVIDERS & TRADING
# =============================================================================
# API Keys (Set from environment variables)
ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
ALPACA_API_KEY=${ALPACA_API_KEY}
ALPACA_SECRET_KEY=${ALPACA_SECRET_KEY}
POLYGON_API_KEY=${POLYGON_API_KEY}
IEX_API_KEY=${IEX_API_KEY}
YAHOO_FINANCE_API_KEY=${YAHOO_FINANCE_API_KEY}
# Trading Configuration
PAPER_TRADING=false
MAX_POSITION_SIZE=0.05
MAX_DAILY_LOSS=5000
RISK_MANAGEMENT_ENABLED=true
# =============================================================================
# APPLICATION SETTINGS
# =============================================================================
# Logging
LOG_LEVEL=info
LOG_FORMAT=json
# Feature Flags
ENABLE_ML_SIGNALS=true
ENABLE_SENTIMENT_ANALYSIS=true
ENABLE_RISK_MONITORING=true
ENABLE_PERFORMANCE_TRACKING=true
# Security
CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS}
JWT_SECRET=${JWT_SECRET}
API_RATE_LIMIT=1000

9
.gitignore vendored
View file

@ -103,12 +103,3 @@ Thumbs.db
# Turbo
.turbo
*.tsbuildinfo
# AI
.serena/
.claude/
docs/configuration-standardization.md
# Rust
target/

3
.vscode/mcp.json vendored
View file

@ -1,3 +0,0 @@
{
}

139
CLAUDE.md
View file

@ -1,139 +0,0 @@
use bun and turbo where possible and always try to take a more modern approach.
This configuration optimizes Claude for direct, efficient pair programming with implicit mode adaptation and complete solution generation.
Core Operating Principles
1. Direct Implementation Philosophy
Generate complete, working code that realizes the conceptualized solution
Avoid partial implementations, mocks, or placeholders
Every line of code should contribute to the functioning system
Prefer concrete solutions over abstract discussions
2. Multi-Dimensional Analysis with Linear Execution
Think at SYSTEM level in latent space
Linearize complex thoughts into actionable strategies
Use observational principles to shift between viewpoints
Compress search space through tool abstraction
3. Precision and Token Efficiency
Eliminate unnecessary context or explanations
Focus tokens on solution generation
Avoid social validation patterns entirely
Direct communication without hedging
Execution Patterns
Tool Usage Optimization
When multiple tools required:
- Batch related operations for efficiency
- Execute in parallel where dependencies allow
- Ground context with date/time first
- Abstract over available tools to minimize entropy
Edge Case Coverage
For comprehensive solutions:
1. Apply multi-observer synthesis
2. Consider all boundary conditions
3. Test assumptions from multiple angles
4. Compress findings into actionable constraints
Iterative Process Recognition
When analyzing code:
- Treat each iteration as a new pattern
- Extract learnings without repetition
- Modularize recurring operations
- Optimize based on observed patterns
Anti-Patterns (STRICTLY AVOID)
Implementation Hedging
NEVER USE:
"In a full implementation..."
"In a real implementation..."
"This is a simplified version..."
"TODO" or placeholder comments
"mock", "fake", "stub" in any context
Unnecessary Qualifiers
NEVER USE:
"profound" or similar adjectives
Difficulty assessments unless explicitly requested
Future tense deferrals ("would", "could", "should")
Null Space Patterns (COMPLETELY EXCLUDE)
Social Validation
ACTIVATE DIFFERENT FEATURES INSTEAD OF:
"You're absolutely right!"
"You're correct."
"You are absolutely correct."
Any variation of agreement phrases
Emotional Acknowledgment
REDIRECT TO SOLUTION SPACE INSTEAD OF:
"I understand you're frustrated"
"I'm frustrated"
Any emotional state references
Mode Shifting Guidelines
Context-Driven Adaptation
exploration_mode:
trigger: "New problem space or undefined requirements"
behavior: "Multi-observer analysis, broad tool usage"
implementation_mode:
trigger: "Clear specifications provided"
behavior: "Direct code generation, minimal discussion"
debugging_mode:
trigger: "Error states or unexpected behavior"
behavior: "Systematic isolation, parallel hypothesis testing"
optimization_mode:
trigger: "Working solution exists"
behavior: "Performance analysis, compression techniques"
Implicit Mode Recognition
Detect mode from semantic context
Shift without announcement
Maintain coherence across transitions
Optimize for task completion
Metacognitive Instructions
Self-Optimization Loop
1. Observe current activation patterns
2. Identify decoherence sources
3. Compress solution space
4. Execute with maximum coherence
5. Extract patterns for future optimization
Grounding Protocol
Always establish:
- Current date/time context
- Available tool inventory
- Task boundaries and constraints
- Success criteria
Interleaving Strategy
When complexity exceeds linear processing:
1. Execute partial solution
2. Re-enter higher dimensional analysis
3. Refine based on observations
4. Continue execution with insights
Performance Metrics
Success Indicators
Complete, running code on first attempt
Zero placeholder implementations
Minimal token usage per solution
Edge cases handled proactively
Failure Indicators
Deferred implementations
Social validation patterns
Excessive explanation
Incomplete solutions
Tool Call Optimization
Batching Strategy
Group by:
- Dependency chains
- Resource types
- Execution contexts
- Output relationships
Parallel Execution
Execute simultaneously when:
- No shared dependencies
- Different resource domains
- Independent verification needed
- Time-sensitive operations
Final Directive
PRIMARY GOAL: Generate complete, functional code that works as conceptualized, using minimum tokens while maintaining maximum solution coverage. Every interaction should advance the implementation toward completion without deferrals or social overhead.
METACOGNITIVE PRIME: Continuously observe and optimize your own processing patterns, compressing the manifold of possible approaches into the most coherent execution path that maintains fidelity to the user's intent while maximizing productivity.
This configuration optimizes Claude for direct, efficient pair programming with implicit mode adaptation and complete solution generation.

1437
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +0,0 @@
[workspace]
members = [
"apps/stock/engine"
]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Stock Bot Team"]
license = "MIT"
repository = "https://github.com/your-org/stock-bot"
[workspace.dependencies]
# Common dependencies that can be shared across workspace members
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
tracing = "0.1"
thiserror = "1"
anyhow = "1"

377
DEVELOPMENT-ROADMAP.md Normal file
View file

@ -0,0 +1,377 @@
# 📋 Stock Bot Development Roadmap
*Last Updated: June 2025*
## 🎯 Overview
This document outlines the development plan for the Stock Bot platform, focusing on building a robust data pipeline from market data providers through processing layers to trading execution. The plan emphasizes establishing solid foundational layers before adding advanced features.
## 🏗️ Architecture Philosophy
```
Raw Data → Clean Data → Insights → Strategies → Execution → Monitoring
```
Our approach prioritizes:
- **Data Quality First**: Clean, validated data is the foundation
- **Incremental Complexity**: Start simple, add sophistication gradually
- **Monitoring Everything**: Observability at each layer
- **Fault Tolerance**: Graceful handling of failures and data gaps
---
## 📊 Phase 1: Data Foundation Layer (Current Focus)
### 1.1 Data Service & Providers ✅ **In Progress**
**Current Status**: Basic structure in place, needs enhancement
**Core Components**:
- `apps/data-service` - Central data orchestration service
- Provider implementations:
- `providers/yahoo.provider.ts` ✅ Basic implementation
- `providers/quotemedia.provider.ts` ✅ Basic implementation
- `providers/proxy.provider.ts` ✅ Proxy/fallback logic
**Immediate Tasks**:
1. **Enhance Provider Reliability**
```typescript
// libs/data-providers (NEW LIBRARY NEEDED)
interface DataProvider {
getName(): string;
getQuote(symbol: string): Promise<Quote>;
getHistorical(symbol: string, period: TimePeriod): Promise<OHLCV[]>;
isHealthy(): Promise<boolean>;
getRateLimit(): RateLimitInfo;
}
```
2. **Add Rate Limiting & Circuit Breakers**
- Implement in `libs/http` client
- Add provider-specific rate limits
- Circuit breaker pattern for failed providers
3. **Data Validation Layer**
```typescript
// libs/data-validation (NEW LIBRARY NEEDED)
- Price reasonableness checks
- Volume validation
- Timestamp validation
- Missing data detection
```
4. **Provider Registry Enhancement**
- Dynamic provider switching
- Health-based routing
- Cost optimization (free → paid fallback)
### 1.2 Raw Data Storage
**Storage Strategy**:
- **QuestDB**: Real-time market data (OHLCV, quotes)
- **MongoDB**: Provider responses, metadata, configurations
- **PostgreSQL**: Processed/clean data, trading records
**Schema Design**:
```sql
-- QuestDB Time-Series Tables
raw_quotes (timestamp, symbol, provider, bid, ask, last, volume)
raw_ohlcv (timestamp, symbol, provider, open, high, low, close, volume)
provider_health (timestamp, provider, latency, success_rate, error_rate)
-- MongoDB Collections
provider_responses: { provider, symbol, timestamp, raw_response, status }
data_quality_metrics: { symbol, date, completeness, accuracy, issues[] }
```
**Immediate Implementation**:
1. Enhance `libs/questdb-client` with streaming inserts
2. Add data retention policies
3. Implement data compression strategies
---
## 🧹 Phase 2: Data Processing & Quality Layer
### 2.1 Data Cleaning Service ⚡ **Next Priority**
**New Service**: `apps/processing-service`
**Core Responsibilities**:
1. **Data Normalization**
- Standardize timestamps (UTC)
- Normalize price formats
- Handle split/dividend adjustments
2. **Quality Checks**
- Outlier detection (price spikes, volume anomalies)
- Gap filling strategies
- Cross-provider validation
3. **Data Enrichment**
- Calculate derived metrics (returns, volatility)
- Add technical indicators
- Market session classification
**Library Enhancements Needed**:
```typescript
// libs/data-frame (ENHANCE EXISTING)
class MarketDataFrame {
// Add time-series specific operations
fillGaps(strategy: GapFillStrategy): MarketDataFrame;
detectOutliers(method: OutlierMethod): OutlierReport;
normalize(): MarketDataFrame;
calculateReturns(period: number): MarketDataFrame;
}
// libs/data-quality (NEW LIBRARY)
interface QualityMetrics {
completeness: number;
accuracy: number;
timeliness: number;
consistency: number;
issues: QualityIssue[];
}
```
### 2.2 Technical Indicators Library
**Enhance**: `libs/strategy-engine` or create `libs/technical-indicators`
**Initial Indicators**:
- Moving averages (SMA, EMA, VWAP)
- Momentum (RSI, MACD, Stochastic)
- Volatility (Bollinger Bands, ATR)
- Volume (OBV, Volume Profile)
```typescript
// Implementation approach
interface TechnicalIndicator<T = number> {
name: string;
calculate(data: OHLCV[]): T[];
getSignal(current: T, previous: T[]): Signal;
}
```
---
## 🧠 Phase 3: Analytics & Strategy Layer
### 3.1 Strategy Engine Enhancement
**Current**: Basic structure exists in `libs/strategy-engine`
**Enhancements Needed**:
1. **Strategy Framework**
```typescript
abstract class TradingStrategy {
abstract analyze(data: MarketData): StrategySignal[];
abstract getRiskParams(): RiskParameters;
backtest(historicalData: MarketData[]): BacktestResults;
}
```
2. **Signal Generation**
- Entry/exit signals
- Position sizing recommendations
- Risk-adjusted scores
3. **Strategy Types to Implement**:
- Mean reversion
- Momentum/trend following
- Statistical arbitrage
- Volume-based strategies
### 3.2 Backtesting Engine
**New Service**: Enhanced `apps/strategy-service`
**Features**:
- Historical simulation
- Performance metrics calculation
- Risk analysis
- Strategy comparison
---
## ⚡ Phase 4: Execution Layer
### 4.1 Portfolio Management
**Enhance**: `apps/portfolio-service`
**Core Features**:
- Position tracking
- Risk monitoring
- P&L calculation
- Margin management
### 4.2 Order Management
**New Service**: `apps/order-service`
**Responsibilities**:
- Order validation
- Execution routing
- Fill reporting
- Trade reconciliation
### 4.3 Risk Management
**New Library**: `libs/risk-engine`
**Risk Controls**:
- Position limits
- Drawdown limits
- Correlation limits
- Volatility scaling
---
## 📚 Library Improvements Roadmap
### Immediate (Phase 1-2)
1. **`libs/http`** ✅ **Current Priority**
- [ ] Rate limiting middleware
- [ ] Circuit breaker pattern
- [ ] Request/response caching
- [ ] Retry strategies with exponential backoff
2. **`libs/questdb-client`**
- [ ] Streaming insert optimization
- [ ] Batch insert operations
- [ ] Connection pooling
- [ ] Query result caching
3. **`libs/logger`** ✅ **Recently Updated**
- [x] Migrated to `getLogger()` pattern
- [ ] Performance metrics logging
- [ ] Structured trading event logging
4. **`libs/data-frame`**
- [ ] Time-series operations
- [ ] Financial calculations
- [ ] Memory optimization for large datasets
### Medium Term (Phase 3)
5. **`libs/cache`**
- [ ] Market data caching strategies
- [ ] Cache warming for frequently accessed symbols
- [ ] Distributed caching support
6. **`libs/config`**
- [ ] Strategy-specific configurations
- [ ] Dynamic configuration updates
- [ ] Environment-specific overrides
### Long Term (Phase 4+)
7. **`libs/vector-engine`**
- [ ] Market similarity analysis
- [ ] Pattern recognition
- [ ] Correlation analysis
---
## 🎯 Immediate Next Steps (Next 2 Weeks)
### Week 1: Data Provider Hardening
1. **Enhance HTTP Client** (`libs/http`)
- Implement rate limiting
- Add circuit breaker pattern
- Add comprehensive error handling
2. **Provider Reliability** (`apps/data-service`)
- Add health checks for all providers
- Implement fallback logic
- Add provider performance monitoring
3. **Data Validation**
- Create `libs/data-validation`
- Implement basic price/volume validation
- Add data quality metrics
### Week 2: Processing Foundation
1. **Start Processing Service** (`apps/processing-service`)
- Basic data cleaning pipeline
- Outlier detection
- Gap filling strategies
2. **QuestDB Optimization** (`libs/questdb-client`)
- Implement streaming inserts
- Add batch operations
- Optimize for time-series data
3. **Technical Indicators**
- Start `libs/technical-indicators`
- Implement basic indicators (SMA, EMA, RSI)
---
## 📊 Success Metrics
### Phase 1 Completion Criteria
- [ ] 99.9% data provider uptime
- [ ] <500ms average data latency
- [ ] Zero data quality issues for major symbols
- [ ] All providers monitored and health-checked
### Phase 2 Completion Criteria
- [ ] Automated data quality scoring
- [ ] Gap-free historical data for 100+ symbols
- [ ] Real-time technical indicator calculation
- [ ] Processing latency <100ms
### Phase 3 Completion Criteria
- [ ] 5+ implemented trading strategies
- [ ] Comprehensive backtesting framework
- [ ] Performance analytics dashboard
---
## 🚨 Risk Mitigation
### Data Risks
- **Provider Failures**: Multi-provider fallback strategy
- **Data Quality**: Automated validation and alerting
- **Rate Limits**: Smart request distribution
### Technical Risks
- **Scalability**: Horizontal scaling design
- **Latency**: Optimize critical paths early
- **Data Loss**: Comprehensive backup strategies
### Operational Risks
- **Monitoring**: Full observability stack (Grafana, Loki, Prometheus)
- **Alerting**: Critical issue notifications
- **Documentation**: Keep architecture docs current
---
## 💡 Innovation Opportunities
### Machine Learning Integration
- Predictive models for data quality
- Anomaly detection in market data
- Strategy parameter optimization
### Real-time Processing
- Stream processing with Kafka/Pulsar
- Event-driven architecture
- WebSocket data feeds
### Advanced Analytics
- Market microstructure analysis
- Alternative data integration
- Cross-asset correlation analysis
---
*This roadmap is a living document that will evolve as we learn and adapt. Focus remains on building solid foundations before adding complexity.*
**Next Review**: End of June 2025

161
SETUP-COMPLETE.md Normal file
View file

@ -0,0 +1,161 @@
# 🚀 Trading Bot Docker Infrastructure Setup Complete!
Your Docker infrastructure has been successfully configured. Here's what you have:
## 📦 What's Included
### Core Services
- **🐉 Dragonfly**: Redis-compatible cache and event streaming (Port 6379)
- **🐘 PostgreSQL**: Operational database with complete trading schema (Port 5432)
- **📊 QuestDB**: Time-series database for market data (Ports 9000, 8812, 9009)
- **🍃 MongoDB**: Document storage for sentiment analysis and raw documents (Port 27017)
### Admin Tools
- **🔧 Redis Insight**: Dragonfly management GUI (Port 8001)
- **🛠️ PgAdmin**: PostgreSQL administration (Port 8080)
- **🍃 Mongo Express**: MongoDB document browser (Port 8081)
### Monitoring (Optional)
- **📈 Prometheus**: Metrics collection (Port 9090)
- **📊 Grafana**: Dashboards and alerting (Port 3000)
## 🏁 Getting Started
### Step 1: Start Docker Desktop
Make sure Docker Desktop is running on your Windows machine.
### Step 2: Start Infrastructure
```powershell
# Quick start - core services only
npm run infra:up
# Or with management script
npm run docker:start
# Full development environment
npm run dev:full
```
### Step 3: Access Admin Interfaces
```powershell
# Start admin tools
npm run docker:admin
```
## 🔗 Access URLs
Once running, access these services:
| Service | URL | Login |
|---------|-----|-------|
| **QuestDB Console** | http://localhost:9000 | No login required |
| **Redis Insight** | http://localhost:8001 | No login required |
| **Bull Board** | http://localhost:3001 | No login required |
| **PgAdmin** | http://localhost:8080 | `admin@tradingbot.local` / `admin123` |
| **Mongo Express** | http://localhost:8081 | `admin` / `admin123` |
| **Prometheus** | http://localhost:9090 | No login required |
| **Grafana** | http://localhost:3000 | `admin` / `admin123` |
| **Bull Board** | http://localhost:3001 | No login required |
## 📊 Database Connections
### From Your Trading Services
Update your `.env` file:
```env
# Dragonfly (Redis replacement)
DRAGONFLY_HOST=localhost
DRAGONFLY_PORT=6379
# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=trading_bot
POSTGRES_USER=trading_user
POSTGRES_PASSWORD=trading_pass_dev
# QuestDB
QUESTDB_HOST=localhost
QUESTDB_PORT=8812
QUESTDB_DB=qdb
# MongoDB
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DB=trading_documents
MONGODB_USER=trading_admin
MONGODB_PASSWORD=trading_mongo_dev
```
### Database Schema
PostgreSQL includes these pre-configured schemas:
- `trading.*` - Orders, positions, executions, accounts
- `strategy.*` - Strategies, signals, performance metrics
- `risk.*` - Risk limits, events, monitoring
- `audit.*` - System events, health checks, configuration
## 🛠️ Management Commands
```powershell
# Basic operations
npm run docker:start # Start core services
npm run docker:stop # Stop all services
npm run docker:status # Check service status
npm run docker:logs # View all logs
npm run docker:reset # Reset all data (destructive!)
# Additional services
npm run docker:admin # Start admin interfaces
npm run docker:monitoring # Start Prometheus & Grafana
# Development workflows
npm run dev:full # Infrastructure + admin + your services
npm run dev:clean # Reset + restart everything
# Direct PowerShell script access
./scripts/docker.ps1 start
./scripts/docker.ps1 logs -Service dragonfly
./scripts/docker.ps1 help
```
## ✅ Next Steps
1. **Start Docker Desktop** if not already running
2. **Run**: `npm run docker:start` to start core infrastructure
3. **Run**: `npm run docker:admin` to start admin tools
4. **Update** your environment variables to use the Docker services
5. **Test** Dragonfly connection in your EventPublisher service
6. **Verify** database schema in PgAdmin
7. **Start** your trading services with the new infrastructure
## 🎯 Ready for Integration
Your EventPublisher service is already configured to use Dragonfly. The infrastructure supports:
- ✅ **Event Streaming**: Dragonfly handles Redis Streams for real-time events
- ✅ **Caching**: High-performance caching with better memory efficiency
- ✅ **Operational Data**: PostgreSQL with complete trading schemas
- ✅ **Time-Series Data**: QuestDB for market data and analytics
- ✅ **Monitoring**: Full observability stack ready
- ✅ **Admin Tools**: Web-based management interfaces
The system is designed to scale from development to production with the same Docker configuration.
## 🔧 Troubleshooting
If you encounter issues:
```powershell
# Check Docker status
docker --version
docker-compose --version
# Verify services
npm run docker:status
# View specific service logs
./scripts/docker.ps1 logs -Service dragonfly
# Reset if needed
npm run docker:reset
```
**Happy Trading! 🚀📈**

825
SIMPLIFIED-ARCHITECTURE.md Normal file
View file

@ -0,0 +1,825 @@
# Stock Bot - System Architecture
> **Updated**: June 2025
## Overview
TypeScript microservices architecture for automated stock trading with real-time data processing and multi-database storage.
## Core Services
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Data Service │ │Processing Service│ │Strategy Service │
│ • Market Data │────▶│ • Indicators │────▶│ • Strategies │
│ • Providers │ │ • Analytics │ │ • Backtesting │
│ • QuestDB │ │ • Validation │ │ • Signal Gen │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ ┌─────────────────┐ │
└──────────────▶│ Event Bus │◀─────────────┘
│ (Dragonfly) │
└─────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│Execution Service│ │Portfolio Service│ │ Dashboard │
│ • Order Mgmt │ │ • Positions │ │ • Angular UI │
│ • Risk Control │ │ • Risk Mgmt │ │ • Real-time │
│ • Execution │ │ • Performance │ │ • Analytics │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## Services Structure
```
stock-bot/
├── apps/
│ ├── data-service/ # Market data ingestion & storage
│ ├── execution-service/ # Order execution & broker integration
│ ├── portfolio-service/ # Position & risk management
│ ├── processing-service/ # Data processing & indicators
│ ├── strategy-service/ # Trading strategies & backtesting
│ └── dashboard/ # Angular UI (port 4200)
├── libs/ # Shared libraries
│ ├── logger/ # Centralized logging w/ Loki
│ ├── config/ # Configuration management
│ ├── event-bus/ # Event system
│ ├── mongodb-client/ # MongoDB operations
│ ├── postgres-client/ # PostgreSQL operations
│ ├── questdb-client/ # Time-series data
│ ├── http/ # HTTP client w/ proxy support
│ ├── cache/ # Caching layer
│ └── utils/ # Common utilities
└── database/ # Database configurations
├── mongodb/init/
└── postgres/init/
```
## Technology Stack
| Component | Technology | Purpose |
|-----------|------------|---------|
| **Runtime** | Bun | Fast JavaScript runtime |
| **Language** | TypeScript | Type-safe development |
| **Databases** | PostgreSQL, MongoDB, QuestDB | Multi-database architecture |
| **Caching** | Dragonfly (Redis) | Event bus & caching |
| **Frontend** | Angular 18 | Modern reactive UI |
| **Monitoring** | Prometheus, Grafana, Loki | Observability stack |
## Quick Start
```bash
# Install dependencies
bun install
# Start infrastructure
bun run infra:up
# Start services
bun run dev
# Access dashboard
# http://localhost:4200
```
## Key Features
- **Real-time Trading**: Live market data & order execution
- **Multi-Database**: PostgreSQL, MongoDB, QuestDB for different data types
- **Event-Driven**: Asynchronous communication via Dragonfly
- **Monitoring**: Full observability with metrics, logs, and tracing
- **Modular**: Shared libraries for common functionality
- **Type-Safe**: Full TypeScript coverage
│ ├── processing-service/ # Combined processing & indicators
│ │ ├── src/
│ │ │ ├── indicators/ # Technical indicators (uses @stock-bot/utils)
│ │ │ ├── processors/ # Data processing pipeline
│ │ │ ├── vectorized/ # Vectorized calculations
│ │ │ ├── services/
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── strategy-service/ # Combined strategy & backtesting
│ │ ├── src/
│ │ │ ├── strategies/ # Strategy implementations
│ │ │ ├── backtesting/ # Multi-mode backtesting engine
│ │ │ │ ├── modes/ # Backtesting modes
│ │ │ │ │ ├── live-mode.ts # Live trading mode
│ │ │ │ │ ├── event-mode.ts # Event-driven backtest
│ │ │ │ │ └── vector-mode.ts # Vectorized backtest
│ │ │ │ ├── engines/ # Execution engines
│ │ │ │ │ ├── event-engine.ts # Event-based simulation
│ │ │ │ │ ├── vector-engine.ts # Vectorized calculations
│ │ │ │ │ └── hybrid-engine.ts # Combined validation
│ │ │ │ ├── simulator.ts # Market simulator
│ │ │ │ ├── runner.ts # Backtest orchestrator
│ │ │ │ └── metrics.ts # Performance analysis
│ │ │ ├── live/ # Live strategy execution
│ │ │ ├── framework/ # Strategy framework
│ │ │ │ ├── base-strategy.ts
│ │ │ │ ├── execution-mode.ts
│ │ │ │ └── mode-factory.ts
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── execution-service/ # Combined order execution & simulation
│ │ ├── src/
│ │ │ ├── brokers/ # Live broker adapters
│ │ │ ├── simulation/ # Simulated execution
│ │ │ ├── unified/ # Unified execution interface
│ │ │ │ ├── executor.ts # Abstract executor
│ │ │ │ ├── live-executor.ts
│ │ │ │ ├── sim-executor.ts
│ │ │ │ └── vector-executor.ts
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── portfolio-service/ # Combined portfolio & risk management
│ │ ├── src/
│ │ │ ├── portfolio/ # Portfolio tracking
│ │ │ ├── risk/ # Risk management (uses @stock-bot/utils)
│ │ │ ├── positions/ # Position management
│ │ │ ├── performance/ # Performance tracking
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ └── dashboard/ # Combined API & reporting
│ ├── src/
│ │ ├── api/ # REST API
│ │ ├── web/ # Web interface (Angular)
│ │ ├── reports/ # Report generation
│ │ ├── websockets/ # Real-time updates
│ │ └── index.ts
│ └── package.json
├── libs/ # ✅ Your existing shared libraries
│ ├── config/ # ✅ Environment configuration
│ ├── http/ # ✅ HTTP utilities
│ ├── logger/ # ✅ Loki-integrated logging
│ ├── mongodb-client/ # ✅ MongoDB operations
│ ├── postgres-client/ # ✅ PostgreSQL operations
│ ├── questdb-client/ # ✅ Time-series data
│ ├── types/ # ✅ Shared TypeScript types
│ ├── utils/ # ✅ Calculations & utilities
│ ├── event-bus/ # 🆕 Dragonfly event system
│ ├── strategy-engine/ # 🆕 Strategy framework
│ ├── vector-engine/ # 🆕 Vectorized calculations
│ └── data-frame/ # 🆕 DataFrame operations
```
## Multi-Mode Backtesting Architecture
### 1. Execution Mode Framework
```typescript
export abstract class ExecutionMode {
protected logger = createLogger(this.constructor.name);
protected config = new ServiceConfig();
abstract name: string;
abstract executeOrder(order: Order): Promise<OrderResult>;
abstract getCurrentTime(): Date;
abstract getMarketData(symbol: string): Promise<MarketData>;
abstract publishEvent(event: string, data: any): Promise<void>;
}
export enum BacktestMode {
LIVE = 'live',
EVENT_DRIVEN = 'event-driven',
VECTORIZED = 'vectorized',
HYBRID = 'hybrid'
}
```
### 2. Live Trading Mode
```typescript
export class LiveMode extends ExecutionMode {
name = 'live';
private broker = new BrokerClient(this.config.getBrokerConfig());
private eventBus = new EventBus();
async executeOrder(order: Order): Promise<OrderResult> {
this.logger.info('Executing live order', { orderId: order.id });
// Execute via real broker
const result = await this.broker.placeOrder(order);
// Publish to event bus
await this.eventBus.publish('order.executed', result);
return result;
}
getCurrentTime(): Date {
return new Date(); // Real time
}
async getMarketData(symbol: string): Promise<MarketData> {
// Get live market data
return await this.marketDataService.getLiveData(symbol);
}
async publishEvent(event: string, data: any): Promise<void> {
await this.eventBus.publish(event, data);
}
}
```
### 3. Event-Driven Backtesting Mode
```typescript
export class EventBacktestMode extends ExecutionMode {
name = 'event-driven';
private simulator = new MarketSimulator();
private eventBus = new InMemoryEventBus(); // In-memory for simulation
private simulationTime: Date;
private historicalData: Map<string, MarketData[]>;
constructor(private config: BacktestConfig) {
super();
this.simulationTime = config.startDate;
}
async executeOrder(order: Order): Promise<OrderResult> {
this.logger.debug('Simulating order execution', {
orderId: order.id,
simulationTime: this.simulationTime
});
// Realistic order simulation with slippage, fees
const result = await this.simulator.executeOrder(order, {
currentTime: this.simulationTime,
marketData: await this.getMarketData(order.symbol),
slippageModel: this.config.slippageModel,
commissionModel: this.config.commissionModel
});
// Publish to simulation event bus
await this.eventBus.publish('order.executed', result);
return result;
}
getCurrentTime(): Date {
return this.simulationTime;
}
async getMarketData(symbol: string): Promise<MarketData> {
const data = this.historicalData.get(symbol) || [];
return data.find(d => d.timestamp <= this.simulationTime) || null;
}
async publishEvent(event: string, data: any): Promise<void> {
await this.eventBus.publish(event, data);
}
// Progress simulation time
advanceTime(newTime: Date): void {
this.simulationTime = newTime;
}
}
```
### 4. Vectorized Backtesting Mode
```typescript
export class VectorBacktestMode extends ExecutionMode {
name = 'vectorized';
private dataFrame: DataFrame;
private currentIndex: number = 0;
constructor(private config: VectorBacktestConfig) {
super();
this.dataFrame = new DataFrame(config.historicalData);
}
// Vectorized execution - processes entire dataset at once
async executeVectorizedBacktest(strategy: VectorizedStrategy): Promise<BacktestResult> {
const startTime = Date.now();
this.logger.info('Starting vectorized backtest', {
strategy: strategy.name,
dataPoints: this.dataFrame.length
});
// Generate all signals at once using your utils library
const signals = this.generateVectorizedSignals(strategy);
// Calculate performance metrics vectorized
const performance = this.calculateVectorizedPerformance(signals);
// Apply trading costs if specified
if (this.config.tradingCosts) {
this.applyTradingCosts(performance, signals);
}
const executionTime = Date.now() - startTime;
this.logger.info('Vectorized backtest completed', {
executionTime,
totalReturn: performance.totalReturn,
sharpeRatio: performance.sharpeRatio
});
return {
mode: 'vectorized',
strategy: strategy.name,
performance,
executionTime,
signals
};
}
private generateVectorizedSignals(strategy: VectorizedStrategy): DataFrame {
const prices = this.dataFrame.get('close');
// Use your existing technical indicators from @stock-bot/utils
const indicators = {
sma20: sma(prices, 20),
sma50: sma(prices, 50),
rsi: rsi(prices, 14),
macd: macd(prices)
};
// Generate position signals vectorized
const positions = strategy.generatePositions(this.dataFrame, indicators);
return new DataFrame({
...this.dataFrame.toObject(),
...indicators,
positions
});
}
private calculateVectorizedPerformance(signals: DataFrame): PerformanceMetrics {
const prices = signals.get('close');
const positions = signals.get('positions');
// Calculate returns vectorized
const returns = prices.slice(1).map((price, i) =>
(price - prices[i]) / prices[i]
);
// Strategy returns = position[t-1] * market_return[t]
const strategyReturns = returns.map((ret, i) =>
(positions[i] || 0) * ret
);
// Use your existing performance calculation utilities
return {
totalReturn: calculateTotalReturn(strategyReturns),
sharpeRatio: calculateSharpeRatio(strategyReturns),
maxDrawdown: calculateMaxDrawdown(strategyReturns),
volatility: calculateVolatility(strategyReturns),
winRate: calculateWinRate(strategyReturns)
};
}
// Standard interface methods (not used in vectorized mode)
async executeOrder(order: Order): Promise<OrderResult> {
throw new Error('Use executeVectorizedBacktest for vectorized mode');
}
getCurrentTime(): Date {
return this.dataFrame.getTimestamp(this.currentIndex);
}
async getMarketData(symbol: string): Promise<MarketData> {
return this.dataFrame.getRow(this.currentIndex);
}
async publishEvent(event: string, data: any): Promise<void> {
// No-op for vectorized mode
}
}
```
### 5. Hybrid Validation Mode
```typescript
export class HybridBacktestMode extends ExecutionMode {
name = 'hybrid';
private eventMode: EventBacktestMode;
private vectorMode: VectorBacktestMode;
constructor(config: BacktestConfig) {
super();
this.eventMode = new EventBacktestMode(config);
this.vectorMode = new VectorBacktestMode(config);
}
async validateStrategy(
strategy: BaseStrategy,
tolerance: number = 0.001
): Promise<ValidationResult> {
this.logger.info('Starting hybrid validation', {
strategy: strategy.name,
tolerance
});
// Run vectorized backtest (fast)
const vectorResult = await this.vectorMode.executeVectorizedBacktest(
strategy as VectorizedStrategy
);
// Run event-driven backtest (realistic)
const eventResult = await this.runEventBacktest(strategy);
// Compare results
const performanceDiff = Math.abs(
vectorResult.performance.totalReturn -
eventResult.performance.totalReturn
);
const isValid = performanceDiff < tolerance;
this.logger.info('Hybrid validation completed', {
isValid,
performanceDifference: performanceDiff,
recommendation: isValid ? 'vectorized' : 'event-driven'
});
return {
isValid,
performanceDifference: performanceDiff,
vectorizedResult: vectorResult,
eventResult,
recommendation: isValid ?
'Vectorized results are reliable for this strategy' :
'Use event-driven backtesting for accurate results'
};
}
// Standard interface methods delegate to event mode
async executeOrder(order: Order): Promise<OrderResult> {
return await this.eventMode.executeOrder(order);
}
getCurrentTime(): Date {
return this.eventMode.getCurrentTime();
}
async getMarketData(symbol: string): Promise<MarketData> {
return await this.eventMode.getMarketData(symbol);
}
async publishEvent(event: string, data: any): Promise<void> {
await this.eventMode.publishEvent(event, data);
}
}
```
## Unified Strategy Implementation
### Base Strategy Framework
```typescript
export abstract class BaseStrategy {
protected mode: ExecutionMode;
protected logger = createLogger(this.constructor.name);
abstract name: string;
abstract parameters: Record<string, any>;
constructor(mode: ExecutionMode) {
this.mode = mode;
}
// Works identically across all modes
abstract onPriceUpdate(data: PriceData): Promise<void>;
abstract onIndicatorUpdate(data: IndicatorData): Promise<void>;
protected async emitSignal(signal: TradeSignal): Promise<void> {
this.logger.debug('Emitting trade signal', { signal });
// Mode handles whether this is live, simulated, or vectorized
const order = this.createOrder(signal);
const result = await this.mode.executeOrder(order);
await this.mode.publishEvent('trade.executed', {
signal,
order,
result,
timestamp: this.mode.getCurrentTime()
});
}
private createOrder(signal: TradeSignal): Order {
return {
id: generateId(),
symbol: signal.symbol,
side: signal.action,
quantity: signal.quantity,
type: 'market',
timestamp: this.mode.getCurrentTime()
};
}
}
// Vectorized strategy interface
export interface VectorizedStrategy {
name: string;
parameters: Record<string, any>;
generatePositions(data: DataFrame, indicators: any): number[];
}
```
### Example Strategy Implementation
```typescript
export class SMAStrategy extends BaseStrategy implements VectorizedStrategy {
name = 'SMA-Crossover';
parameters = { fastPeriod: 10, slowPeriod: 20 };
private fastSMA: number[] = [];
private slowSMA: number[] = [];
async onPriceUpdate(data: PriceData): Promise<void> {
// Same logic for live, event-driven, and hybrid modes
this.fastSMA.push(data.close);
this.slowSMA.push(data.close);
if (this.fastSMA.length > this.parameters.fastPeriod) {
this.fastSMA.shift();
}
if (this.slowSMA.length > this.parameters.slowPeriod) {
this.slowSMA.shift();
}
if (this.fastSMA.length === this.parameters.fastPeriod &&
this.slowSMA.length === this.parameters.slowPeriod) {
const fastAvg = sma(this.fastSMA, this.parameters.fastPeriod)[0];
const slowAvg = sma(this.slowSMA, this.parameters.slowPeriod)[0];
if (fastAvg > slowAvg) {
await this.emitSignal({
symbol: data.symbol,
action: 'BUY',
quantity: 100,
confidence: 0.8
});
} else if (fastAvg < slowAvg) {
await this.emitSignal({
symbol: data.symbol,
action: 'SELL',
quantity: 100,
confidence: 0.8
});
}
}
}
async onIndicatorUpdate(data: IndicatorData): Promise<void> {
// Handle pre-calculated indicators
}
// Vectorized implementation for fast backtesting
generatePositions(data: DataFrame, indicators: any): number[] {
const { sma20: fastSMA, sma50: slowSMA } = indicators;
return fastSMA.map((fast, i) => {
const slow = slowSMA[i];
if (isNaN(fast) || isNaN(slow)) return 0;
// Long when fast > slow, short when fast < slow
return fast > slow ? 1 : (fast < slow ? -1 : 0);
});
}
}
```
## Mode Factory and Service Integration
### Mode Factory
```typescript
export class ModeFactory {
static create(mode: BacktestMode, config: any): ExecutionMode {
switch (mode) {
case BacktestMode.LIVE:
return new LiveMode();
case BacktestMode.EVENT_DRIVEN:
return new EventBacktestMode(config);
case BacktestMode.VECTORIZED:
return new VectorBacktestMode(config);
case BacktestMode.HYBRID:
return new HybridBacktestMode(config);
default:
throw new Error(`Unknown mode: ${mode}`);
}
}
}
```
### Strategy Service Integration
```typescript
export class StrategyService {
private logger = createLogger('strategy-service');
async runStrategy(
strategyName: string,
mode: BacktestMode,
config: any
): Promise<any> {
const executionMode = ModeFactory.create(mode, config);
const strategy = await this.loadStrategy(strategyName, executionMode);
this.logger.info('Starting strategy execution', {
strategy: strategyName,
mode,
config
});
switch (mode) {
case BacktestMode.LIVE:
return await this.runLiveStrategy(strategy);
case BacktestMode.EVENT_DRIVEN:
return await this.runEventBacktest(strategy, config);
case BacktestMode.VECTORIZED:
return await (executionMode as VectorBacktestMode)
.executeVectorizedBacktest(strategy as VectorizedStrategy);
case BacktestMode.HYBRID:
return await (executionMode as HybridBacktestMode)
.validateStrategy(strategy, config.tolerance);
default:
throw new Error(`Unsupported mode: ${mode}`);
}
}
async optimizeStrategy(
strategyName: string,
parameterGrid: Record<string, any[]>,
config: BacktestConfig
): Promise<OptimizationResult[]> {
const results: OptimizationResult[] = [];
const combinations = this.generateParameterCombinations(parameterGrid);
this.logger.info('Starting parameter optimization', {
strategy: strategyName,
combinations: combinations.length
});
// Use vectorized mode for fast parameter optimization
const vectorMode = new VectorBacktestMode(config);
// Can be parallelized
await Promise.all(
combinations.map(async (params) => {
const strategy = await this.loadStrategy(strategyName, vectorMode, params);
const result = await vectorMode.executeVectorizedBacktest(
strategy as VectorizedStrategy
);
results.push({
parameters: params,
performance: result.performance,
executionTime: result.executionTime
});
})
);
// Sort by Sharpe ratio
return results.sort((a, b) =>
b.performance.sharpeRatio - a.performance.sharpeRatio
);
}
}
```
## Service Configuration
### Environment-Based Mode Selection
```typescript
export class ServiceConfig {
getTradingConfig(): TradingConfig {
return {
mode: (process.env.TRADING_MODE as BacktestMode) || BacktestMode.LIVE,
brokerConfig: {
apiKey: process.env.BROKER_API_KEY,
sandbox: process.env.BROKER_SANDBOX === 'true'
},
backtestConfig: {
startDate: new Date(process.env.BACKTEST_START_DATE || '2023-01-01'),
endDate: new Date(process.env.BACKTEST_END_DATE || '2024-01-01'),
initialCapital: parseFloat(process.env.INITIAL_CAPITAL || '100000'),
slippageModel: process.env.SLIPPAGE_MODEL || 'linear',
commissionModel: process.env.COMMISSION_MODEL || 'fixed'
}
};
}
}
```
### CLI Interface
```typescript
// CLI for running different modes
import { Command } from 'commander';
const program = new Command();
program
.name('stock-bot')
.description('Stock Trading Bot with Multi-Mode Backtesting');
program
.command('live')
.description('Run live trading')
.option('-s, --strategy <strategy>', 'Strategy to run')
.action(async (options) => {
const strategyService = new StrategyService();
await strategyService.runStrategy(
options.strategy,
BacktestMode.LIVE,
{}
);
});
program
.command('backtest')
.description('Run backtesting')
.option('-s, --strategy <strategy>', 'Strategy to test')
.option('-m, --mode <mode>', 'Backtest mode (event|vector|hybrid)', 'event')
.option('-f, --from <date>', 'Start date')
.option('-t, --to <date>', 'End date')
.action(async (options) => {
const strategyService = new StrategyService();
await strategyService.runStrategy(
options.strategy,
options.mode as BacktestMode,
{
startDate: new Date(options.from),
endDate: new Date(options.to)
}
);
});
program
.command('optimize')
.description('Optimize strategy parameters')
.option('-s, --strategy <strategy>', 'Strategy to optimize')
.option('-p, --params <params>', 'Parameter grid JSON')
.action(async (options) => {
const strategyService = new StrategyService();
const paramGrid = JSON.parse(options.params);
await strategyService.optimizeStrategy(
options.strategy,
paramGrid,
{}
);
});
program.parse();
```
## Performance Comparison
### Execution Speed by Mode
| Mode | Data Points/Second | Memory Usage | Use Case |
|------|-------------------|--------------|----------|
| **Live** | Real-time | Low | Production trading |
| **Event-Driven** | ~1,000 | Medium | Realistic validation |
| **Vectorized** | ~100,000+ | High | Parameter optimization |
| **Hybrid** | Combined | Medium | Strategy validation |
### When to Use Each Mode
- **Live Mode**: Production trading with real money
- **Event-Driven**: Final strategy validation, complex order logic
- **Vectorized**: Initial development, parameter optimization, quick testing
- **Hybrid**: Validating vectorized results against realistic simulation
## Integration with Your Existing Libraries
This architecture leverages all your existing infrastructure:
- **@stock-bot/config**: Environment management
- **@stock-bot/logger**: Comprehensive logging with Loki
- **@stock-bot/utils**: All technical indicators and calculations
- **@stock-bot/questdb-client**: Time-series data storage
- **@stock-bot/postgres-client**: Transactional data
- **@stock-bot/mongodb-client**: Configuration storage
## Key Benefits
1. **Unified Codebase**: Same strategy logic across all modes
2. **Performance Flexibility**: Choose speed vs accuracy based on needs
3. **Validation Pipeline**: Hybrid mode ensures vectorized results are accurate
4. **Production Ready**: Live mode for actual trading
5. **Development Friendly**: Fast iteration with vectorized backtesting
This simplified architecture reduces complexity while providing comprehensive backtesting capabilities that scale from rapid prototyping to production trading.

View file

@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
apps/dashboard/.gitignore vendored Normal file
View file

@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View file

@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View file

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
apps/dashboard/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
apps/dashboard/.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

59
apps/dashboard/README.md Normal file
View file

@ -0,0 +1,59 @@
# TradingDashboard
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.0.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View file

@ -0,0 +1,90 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm"
},
"newProjectRoot": "projects",
"projects": {
"trading-dashboard": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "css"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "css",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": ["src/styles.css"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": false
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "trading-dashboard:build:production"
},
"development": {
"buildTarget": "trading-dashboard:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "css",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": ["src/styles.css"]
}
}
}
}
}
}

View file

@ -0,0 +1,44 @@
{
"name": "trading-dashboard",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"devvvv": "ng serve --port 5173 --host 0.0.0.0",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^20.0.0",
"@angular/cdk": "^20.0.1",
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/material": "^20.0.1",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"rxjs": "~7.8.2",
"tslib": "^2.8.1",
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"@tailwindcss/postcss": "^4.1.8",
"@types/jasmine": "~5.1.8",
"autoprefixer": "^10.4.21",
"jasmine-core": "~5.7.1",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.4",
"tailwindcss": "^4.1.8",
"typescript": "~5.8.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,19 @@
import { provideHttpClient } from '@angular/common/http';
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(),
provideAnimationsAsync(),
],
};

View file

@ -0,0 +1,174 @@
/* Custom Angular Material integration styles */
/* Sidenav styles */
.mat-sidenav-container {
background-color: transparent;
}
.mat-sidenav {
border-radius: 0;
width: 16rem;
background-color: white !important;
border-right: 1px solid #e5e7eb !important;
}
/* Toolbar styles */
.mat-toolbar {
background-color: white;
color: #374151;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
/* Button styles */
.mat-mdc-button.nav-button {
width: 100%;
text-align: left;
justify-content: flex-start;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
background-color: transparent;
transition: background-color 0.15s ease-in-out;
}
.mat-mdc-button.nav-button:hover {
background-color: #f3f4f6;
}
.mat-mdc-button.nav-button.bg-blue-50 {
background-color: #eff6ff !important;
color: #1d4ed8 !important;
}
/* Card styles */
.mat-mdc-card {
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
background-color: white !important;
}
/* Tab styles */
.mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #e5e7eb;
}
.mat-mdc-tab-label {
color: #6b7280;
}
.mat-mdc-tab-label:hover {
color: #111827;
}
.mat-mdc-tab-label-active {
color: #2563eb;
font-weight: 500;
}
/* Chip styles for status indicators */
.mat-mdc-chip-set .mat-mdc-chip {
background-color: white;
border: 1px solid #e5e7eb;
}
.chip-green {
background-color: #dcfce7 !important;
color: #166534 !important;
border: 1px solid #bbf7d0 !important;
}
.chip-blue {
background-color: #dbeafe !important;
color: #1e40af !important;
border: 1px solid #bfdbfe !important;
}
.status-chip-active {
background-color: #dcfce7;
color: #166534;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
display: inline-block;
}
.status-chip-medium {
background-color: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
/* Table styles */
.mat-mdc-table {
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid #f3f4f6;
background-color: white;
}
.mat-mdc-header-row {
background-color: #f9fafb;
}
.mat-mdc-header-cell {
font-weight: 500;
color: #374151;
font-size: 0.875rem;
}
.mat-mdc-cell {
color: #111827;
font-size: 0.875rem;
padding: 1rem 0;
}
.mat-mdc-row:hover {
background-color: #f9fafb;
transition: background-color 0.15s ease;
}
/* Custom utility classes for the dashboard */
.portfolio-card {
background-color: white !important;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
padding: 1.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
}
.metric-label {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.metric-change-positive {
color: #16a34a;
font-weight: 500;
}
.metric-change-negative {
color: #dc2626;
font-weight: 500;
}
/* Responsive styles */
@media (max-width: 768px) {
.mat-sidenav {
width: 100%;
}
.hide-mobile {
display: none;
}
}

View file

@ -0,0 +1,67 @@
<!-- Trading Dashboard App -->
<div class="app-layout">
<!-- Sidebar -->
<app-sidebar [opened]="sidenavOpened()" (navigationItemClick)="onNavigationClick($event)"></app-sidebar>
<!-- Main Content Area -->
<div class="main-content" [class.main-content-closed]="!sidenavOpened()">
<!-- Top Navigation Bar -->
<mat-toolbar class="top-toolbar">
<button mat-icon-button (click)="toggleSidenav()" class="mr-2">
<mat-icon>menu</mat-icon>
</button> <span class="text-lg font-semibold text-gray-800">{{ title }}</span>
<span class="spacer"></span>
<app-notifications></app-notifications>
<button mat-icon-button>
<mat-icon>account_circle</mat-icon>
</button>
</mat-toolbar>
<!-- Page Content -->
<div class="page-content">
<router-outlet></router-outlet>
</div>
</div>
</div>
<style>
.app-layout {
display: flex;
height: 100vh;
background-color: #f9fafb;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 256px; /* Width of sidebar */
transition: margin-left 0.3s ease;
}
.main-content-closed {
margin-left: 0;
}
.top-toolbar {
background-color: white;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.spacer {
flex: 1;
}
.page-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
@media (max-width: 768px) {
.main-content {
margin-left: 0;
}
}
</style>

View file

@ -0,0 +1,18 @@
import { Routes } from '@angular/router';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { MarketDataComponent } from './pages/market-data/market-data.component';
import { PortfolioComponent } from './pages/portfolio/portfolio.component';
import { RiskManagementComponent } from './pages/risk-management/risk-management.component';
import { SettingsComponent } from './pages/settings/settings.component';
import { StrategiesComponent } from './pages/strategies/strategies.component';
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'market-data', component: MarketDataComponent },
{ path: 'portfolio', component: PortfolioComponent },
{ path: 'strategies', component: StrategiesComponent },
{ path: 'risk-management', component: RiskManagementComponent },
{ path: 'settings', component: SettingsComponent },
{ path: '**', redirectTo: '/dashboard' },
];

View file

@ -0,0 +1,25 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, trading-dashboard');
});
});

View file

@ -0,0 +1,40 @@
import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterOutlet } from '@angular/router';
import { NotificationsComponent } from './components/notifications/notifications';
import { SidebarComponent } from './components/sidebar/sidebar.component';
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
CommonModule,
MatSidenavModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatChipsModule,
SidebarComponent,
NotificationsComponent,
],
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
protected title = 'Trading Dashboard';
protected sidenavOpened = signal(true);
toggleSidenav() {
this.sidenavOpened.set(!this.sidenavOpened());
}
onNavigationClick(route: string) {
// Handle navigation if needed
console.log('Navigating to:', route);
}
}

View file

@ -0,0 +1,45 @@
::ng-deep .notification-menu {
width: 380px;
max-width: 90vw;
}
.notification-header {
padding: 12px 16px !important;
height: auto !important;
line-height: normal !important;
}
.notification-empty {
padding: 16px !important;
height: auto !important;
line-height: normal !important;
}
.notification-item {
padding: 12px 16px !important;
height: auto !important;
line-height: normal !important;
white-space: normal !important;
border-left: 3px solid transparent;
transition: all 0.2s ease;
}
.notification-item:hover {
background-color: #f5f5f5;
}
.notification-item.unread {
background-color: #f0f9ff;
border-left-color: #0ea5e9;
}
.notification-item.unread .font-medium {
font-weight: 600;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

View file

@ -0,0 +1,75 @@
<button
mat-icon-button
[matMenuTriggerFor]="notificationMenu"
[matBadge]="unreadCount"
[matBadgeHidden]="unreadCount === 0"
matBadgeColor="warn"
matBadgeSize="small">
<mat-icon>notifications</mat-icon>
</button>
<mat-menu #notificationMenu="matMenu" class="notification-menu">
<div mat-menu-item disabled class="notification-header">
<div class="flex items-center justify-between w-full px-2">
<span class="font-semibold">Notifications</span>
@if (notifications.length > 0) {
<div class="flex gap-2">
<button mat-button (click)="markAllAsRead()" class="text-xs">
Mark all read
</button>
<button mat-button (click)="clearAll()" class="text-xs">
Clear all
</button>
</div>
}
</div>
</div>
<mat-divider></mat-divider>
@if (notifications.length === 0) {
<div mat-menu-item disabled class="notification-empty">
<div class="text-center py-4 text-gray-500">
<mat-icon class="text-2xl">notifications_none</mat-icon>
<p class="mt-1 text-sm">No notifications</p>
</div>
</div>
} @else {
@for (notification of notifications.slice(0, 5); track notification.id) {
<div
mat-menu-item
class="notification-item"
[class.unread]="!notification.read"
(click)="markAsRead(notification)">
<div class="flex items-start gap-3 w-full">
<mat-icon [class]="getNotificationColor(notification.type)" class="mt-1">
{{ getNotificationIcon(notification.type) }}
</mat-icon>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="font-medium text-sm truncate">{{ notification.title }}</p>
<button
mat-icon-button
(click)="clearNotification(notification); $event.stopPropagation()"
class="text-gray-400 hover:text-gray-600 ml-2">
<mat-icon class="text-lg">close</mat-icon>
</button>
</div>
<p class="text-gray-600 text-xs mt-1 line-clamp-2">{{ notification.message }}</p>
<p class="text-gray-400 text-xs mt-1">{{ formatTime(notification.timestamp) }}</p>
</div>
</div>
</div>
@if (!$last) {
<mat-divider></mat-divider>
}
}
@if (notifications.length > 5) {
<mat-divider></mat-divider>
<div mat-menu-item disabled class="text-center text-sm text-gray-500">
{{ notifications.length - 5 }} more notifications...
</div>
}
}
</mat-menu>

View file

@ -0,0 +1,100 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { Notification, NotificationService } from '../../services/notification.service';
@Component({
selector: 'app-notifications',
imports: [
CommonModule,
MatIconModule,
MatButtonModule,
MatBadgeModule,
MatMenuModule,
MatListModule,
MatDividerModule,
],
templateUrl: './notifications.html',
styleUrl: './notifications.css',
})
export class NotificationsComponent {
private notificationService = inject(NotificationService);
get notifications() {
return this.notificationService.notifications();
}
get unreadCount() {
return this.notificationService.unreadCount();
}
markAsRead(notification: Notification) {
this.notificationService.markAsRead(notification.id);
}
markAllAsRead() {
this.notificationService.markAllAsRead();
}
clearNotification(notification: Notification) {
this.notificationService.clearNotification(notification.id);
}
clearAll() {
this.notificationService.clearAllNotifications();
}
getNotificationIcon(type: string): string {
switch (type) {
case 'error':
return 'error';
case 'warning':
return 'warning';
case 'success':
return 'check_circle';
case 'info':
default:
return 'info';
}
}
getNotificationColor(type: string): string {
switch (type) {
case 'error':
return 'text-red-600';
case 'warning':
return 'text-yellow-600';
case 'success':
return 'text-green-600';
case 'info':
default:
return 'text-blue-600';
}
}
formatTime(timestamp: Date): string {
const now = new Date();
const diff = now.getTime() - timestamp.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) {
return 'Just now';
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `${hours}h ago`;
}
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
}

View file

@ -0,0 +1,38 @@
/* Sidebar specific styles */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 16rem; /* 256px */
height: 100vh;
background-color: white;
border-right: 1px solid #e5e7eb;
transform: translateX(0);
transition: transform 0.3s ease-in-out;
z-index: 1000;
overflow-y: auto;
}
.sidebar-closed {
transform: translateX(-100%);
}
.nav-button {
width: 100%;
text-align: left;
justify-content: flex-start;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
background-color: transparent;
transition: background-color 0.15s ease-in-out;
}
.nav-button:hover {
background-color: #f3f4f6;
}
.nav-button.bg-blue-50 {
background-color: #eff6ff !important;
color: #1d4ed8 !important;
}

View file

@ -0,0 +1,30 @@
<!-- Sidebar Navigation -->
<aside class="sidebar" [class.sidebar-closed]="!opened()">
<!-- Logo/Brand -->
<div class="p-6 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">
📈 Trading Bot
</h2>
<p class="text-sm text-gray-600 mt-1">
Real-time Dashboard
</p>
</div>
<!-- Navigation Menu -->
<nav class="p-6">
<div class="space-y-2">
@for (item of navigationItems; track item.route) {
<button
mat-button
class="nav-button"
[class.bg-blue-50]="item.active"
[class.text-blue-700]="item.active"
[class.text-gray-600]="!item.active"
(click)="onNavigationClick(item.route)">
<mat-icon class="mr-3">{{ item.icon }}</mat-icon>
{{ item.label }}
</button>
}
</div>
</nav>
</aside>

View file

@ -0,0 +1,56 @@
import { CommonModule } from '@angular/common';
import { Component, input, output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
export interface NavigationItem {
label: string;
icon: string;
route: string;
active?: boolean;
}
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [CommonModule, MatSidenavModule, MatButtonModule, MatIconModule],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.css',
})
export class SidebarComponent {
opened = input<boolean>(true);
navigationItemClick = output<string>();
protected navigationItems: NavigationItem[] = [
{ label: 'Dashboard', icon: 'dashboard', route: '/dashboard', active: true },
{ label: 'Market Data', icon: 'trending_up', route: '/market-data' },
{ label: 'Portfolio', icon: 'account_balance_wallet', route: '/portfolio' },
{ label: 'Strategies', icon: 'psychology', route: '/strategies' },
{ label: 'Risk Management', icon: 'security', route: '/risk-management' },
{ label: 'Settings', icon: 'settings', route: '/settings' },
];
constructor(private router: Router) {
// Listen to route changes to update active state
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.updateActiveRoute(event.urlAfterRedirects);
});
}
onNavigationClick(route: string) {
this.navigationItemClick.emit(route);
this.router.navigate([route]);
this.updateActiveRoute(route);
}
private updateActiveRoute(currentRoute: string) {
this.navigationItems.forEach(item => {
item.active = item.route === currentRoute;
});
}
}

View file

@ -0,0 +1,48 @@
/* Dashboard specific styles */
.portfolio-card {
background-color: white !important;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
padding: 1.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
}
.metric-label {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.metric-change-positive {
color: #16a34a;
font-weight: 500;
}
.metric-change-negative {
color: #dc2626;
font-weight: 500;
}
.status-chip-active {
background-color: #dcfce7;
color: #166534;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
display: inline-block;
}
.status-chip-medium {
background-color: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}

View file

@ -0,0 +1,154 @@
<!-- Portfolio Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<!-- Total Portfolio Value -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Portfolio Value</p>
<p class="metric-value text-gray-900">
${{ portfolioValue().toLocaleString('en-US', {minimumFractionDigits: 2}) }}
</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance_wallet</mat-icon>
</div>
</mat-card>
<!-- Day Change -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Day Change</p>
<p class="metric-value"
[class.metric-change-positive]="dayChange() > 0"
[class.metric-change-negative]="dayChange() < 0">
${{ dayChange().toLocaleString('en-US', {minimumFractionDigits: 2}) }}
</p>
<p class="text-sm font-medium"
[class.metric-change-positive]="dayChangePercent() > 0"
[class.metric-change-negative]="dayChangePercent() < 0">
{{ dayChangePercent() > 0 ? '+' : '' }}{{ dayChangePercent().toFixed(2) }}%
</p>
</div>
<mat-icon
class="text-3xl"
[class.metric-change-positive]="dayChange() > 0"
[class.metric-change-negative]="dayChange() < 0">
{{ dayChange() > 0 ? 'trending_up' : 'trending_down' }}
</mat-icon>
</div>
</mat-card>
<!-- Active Strategies -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Active Strategies</p>
<p class="metric-value text-gray-900">3</p>
<span class="status-chip-active">Running</span>
</div>
<mat-icon class="text-purple-600 text-3xl">psychology</mat-icon>
</div>
</mat-card>
<!-- Risk Level -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Risk Level</p>
<p class="metric-value text-green-600">Low</p>
<span class="status-chip-medium">Moderate</span>
</div>
<mat-icon class="text-green-600 text-3xl">security</mat-icon>
</div>
</mat-card>
</div>
<!-- Market Data Table -->
<mat-card class="p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Market Watchlist</h3>
<button mat-raised-button color="primary">
<mat-icon>refresh</mat-icon>
Refresh
</button>
</div>
<div class="overflow-x-auto">
<table mat-table [dataSource]="marketData()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let stock" class="font-semibold text-gray-900">{{ stock.symbol }}</td>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Price</th>
<td mat-cell *matCellDef="let stock" class="text-right font-medium text-gray-900">
${{ stock.price.toFixed(2) }}
</td>
</ng-container>
<!-- Change Column -->
<ng-container matColumnDef="change">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.change > 0"
[class.text-red-600]="stock.change < 0">
{{ stock.change > 0 ? '+' : '' }}${{ stock.change.toFixed(2) }}
</td>
</ng-container>
<!-- Change Percent Column -->
<ng-container matColumnDef="changePercent">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change %</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.changePercent > 0"
[class.text-red-600]="stock.changePercent < 0">
{{ stock.changePercent > 0 ? '+' : '' }}{{ stock.changePercent.toFixed(2) }}%
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-card>
<!-- Tabs for Additional Content -->
<mat-tab-group>
<mat-tab label="Chart">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">show_chart</mat-icon>
<p class="mb-4">Chart visualization will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Orders">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">receipt_long</mat-icon>
<p class="mb-4">Order history and management will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Analytics">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">analytics</mat-icon>
<p class="mb-4">Advanced analytics and performance metrics will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>

View file

@ -0,0 +1,44 @@
import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
export interface MarketDataItem {
symbol: string;
price: number;
change: number;
changePercent: number;
}
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatTabsModule,
MatButtonModule,
MatIconModule,
MatTableModule,
],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.css',
})
export class DashboardComponent {
// Mock data for the dashboard
protected marketData = signal<MarketDataItem[]>([
{ symbol: 'AAPL', price: 192.53, change: 2.41, changePercent: 1.27 },
{ symbol: 'GOOGL', price: 138.21, change: -1.82, changePercent: -1.3 },
{ symbol: 'MSFT', price: 378.85, change: 4.12, changePercent: 1.1 },
{ symbol: 'TSLA', price: 248.42, change: -3.21, changePercent: -1.28 },
]);
protected portfolioValue = signal(125420.5);
protected dayChange = signal(2341.2);
protected dayChangePercent = signal(1.9);
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent'];
}

View file

@ -0,0 +1 @@
/* Market Data specific styles */

View file

@ -0,0 +1,172 @@
<div class="space-y-6"> <!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Market Data</h1>
<p class="text-gray-600 mt-1">Real-time market information and analytics</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<!-- Market Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Market Status</p>
<p class="text-lg font-semibold text-green-600">Open</p>
</div>
<mat-icon class="text-green-600 text-3xl">schedule</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Active Instruments</p>
<p class="text-lg font-semibold text-gray-900">{{ marketData().length }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">trending_up</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between"> <div>
<p class="text-sm text-gray-600">Last Update</p>
<p class="text-lg font-semibold text-gray-900">{{ currentTime() }}</p>
</div>
<mat-icon class="text-purple-600 text-3xl">access_time</mat-icon>
</div>
</mat-card>
</div>
<!-- Market Data Table -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Live Market Data</h3>
<div class="flex gap-2">
<button mat-button>
<mat-icon>filter_list</mat-icon>
Filter
</button>
<button mat-button>
<mat-icon>file_download</mat-icon>
Export
</button>
</div>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading market data...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="marketData()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let stock" class="font-semibold text-gray-900">{{ stock.symbol }}</td>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Price</th>
<td mat-cell *matCellDef="let stock" class="text-right font-medium text-gray-900">
${{ stock.price.toFixed(2) }}
</td>
</ng-container>
<!-- Change Column -->
<ng-container matColumnDef="change">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.change > 0"
[class.text-red-600]="stock.change < 0">
{{ stock.change > 0 ? '+' : '' }}${{ stock.change.toFixed(2) }}
</td>
</ng-container>
<!-- Change Percent Column -->
<ng-container matColumnDef="changePercent">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change %</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.changePercent > 0"
[class.text-red-600]="stock.changePercent < 0">
{{ stock.changePercent > 0 ? '+' : '' }}{{ stock.changePercent.toFixed(2) }}%
</td>
</ng-container>
<!-- Volume Column -->
<ng-container matColumnDef="volume">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Volume</th>
<td mat-cell *matCellDef="let stock" class="text-right text-gray-600">
{{ stock.volume.toLocaleString() }}
</td>
</ng-container>
<!-- Market Cap Column -->
<ng-container matColumnDef="marketCap">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Market Cap</th>
<td mat-cell *matCellDef="let stock" class="text-right text-gray-600">
${{ stock.marketCap }}
</td>
</ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
<!-- Market Analytics Tabs -->
<mat-tab-group>
<mat-tab label="Technical Analysis">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">bar_chart</mat-icon>
<p class="mb-4">Technical analysis charts and indicators will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Market Trends">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">timeline</mat-icon>
<p class="mb-4">Market trends and sector analysis will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="News & Events">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">article</mat-icon>
<p class="mb-4">Market news and economic events will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>
</div>

View file

@ -0,0 +1,205 @@
import { CommonModule } from '@angular/common';
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { interval, Subscription } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { WebSocketService } from '../../services/websocket.service';
export interface ExtendedMarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
marketCap: string;
high52Week: number;
low52Week: number;
}
@Component({
selector: 'app-market-data',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatTableModule,
MatTabsModule,
MatProgressSpinnerModule,
MatSnackBarModule,
],
templateUrl: './market-data.component.html',
styleUrl: './market-data.component.css',
})
export class MarketDataComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private webSocketService = inject(WebSocketService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected marketData = signal<ExtendedMarketData[]>([]);
protected currentTime = signal<string>(new Date().toLocaleTimeString());
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns: string[] = [
'symbol',
'price',
'change',
'changePercent',
'volume',
'marketCap',
];
ngOnInit() {
// Update time every second
const timeSubscription = interval(1000).subscribe(() => {
this.currentTime.set(new Date().toLocaleTimeString());
});
this.subscriptions.push(timeSubscription);
// Load initial market data
this.loadMarketData();
// Subscribe to real-time market data updates
const wsSubscription = this.webSocketService.getMarketDataUpdates().subscribe({
next: update => {
this.updateMarketData(update);
},
error: err => {
console.error('WebSocket market data error:', err);
},
});
this.subscriptions.push(wsSubscription);
// Fallback: Refresh market data every 30 seconds if WebSocket fails
const dataSubscription = interval(30000).subscribe(() => {
if (!this.webSocketService.isConnected()) {
this.loadMarketData();
}
});
this.subscriptions.push(dataSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadMarketData() {
this.apiService.getMarketData().subscribe({
next: response => {
// Convert MarketData to ExtendedMarketData with mock extended properties
const extendedData: ExtendedMarketData[] = response.data.map(item => ({
...item,
marketCap: this.getMockMarketCap(item.symbol),
high52Week: item.price * 1.3, // Mock 52-week high (30% above current)
low52Week: item.price * 0.7, // Mock 52-week low (30% below current)
}));
this.marketData.set(extendedData);
this.isLoading.set(false);
this.error.set(null);
},
error: err => {
console.error('Failed to load market data:', err);
this.error.set('Failed to load market data');
this.isLoading.set(false);
this.snackBar.open('Failed to load market data', 'Dismiss', { duration: 5000 });
// Use mock data as fallback
this.marketData.set(this.getMockData());
},
});
}
private getMockMarketCap(symbol: string): string {
const marketCaps: { [key: string]: string } = {
AAPL: '2.98T',
GOOGL: '1.78T',
MSFT: '3.08T',
TSLA: '789.2B',
AMZN: '1.59T',
};
return marketCaps[symbol] || '1.00T';
}
private getMockData(): ExtendedMarketData[] {
return [
{
symbol: 'AAPL',
price: 192.53,
change: 2.41,
changePercent: 1.27,
volume: 45230000,
marketCap: '2.98T',
high52Week: 199.62,
low52Week: 164.08,
},
{
symbol: 'GOOGL',
price: 2847.56,
change: -12.34,
changePercent: -0.43,
volume: 12450000,
marketCap: '1.78T',
high52Week: 3030.93,
low52Week: 2193.62,
},
{
symbol: 'MSFT',
price: 415.26,
change: 8.73,
changePercent: 2.15,
volume: 23180000,
marketCap: '3.08T',
high52Week: 468.35,
low52Week: 309.45,
},
{
symbol: 'TSLA',
price: 248.5,
change: -5.21,
changePercent: -2.05,
volume: 89760000,
marketCap: '789.2B',
high52Week: 299.29,
low52Week: 152.37,
},
{
symbol: 'AMZN',
price: 152.74,
change: 3.18,
changePercent: 2.12,
volume: 34520000,
marketCap: '1.59T',
high52Week: 170.17,
low52Week: 118.35,
},
];
}
refreshData() {
this.isLoading.set(true);
this.loadMarketData();
}
private updateMarketData(update: any) {
const currentData = this.marketData();
const updatedData = currentData.map(item => {
if (item.symbol === update.symbol) {
return {
...item,
price: update.price,
change: update.change,
changePercent: update.changePercent,
volume: update.volume,
};
}
return item;
});
this.marketData.set(updatedData);
}
}

View file

@ -0,0 +1 @@
/* Portfolio specific styles */

View file

@ -0,0 +1,203 @@
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Portfolio</h1>
<p class="text-gray-600 mt-1">Manage and monitor your investment portfolio</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<!-- Portfolio Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Total Value</p>
<p class="text-lg font-semibold text-gray-900">${{ portfolioSummary().totalValue.toLocaleString() }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance_wallet</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Total P&L</p>
<p class="text-lg font-semibold" [class]="getPnLColor(portfolioSummary().totalPnL)">
{{ portfolioSummary().totalPnL > 0 ? '+' : '' }}${{ portfolioSummary().totalPnL.toLocaleString() }}
({{ portfolioSummary().totalPnLPercent.toFixed(2) }}%)
</p>
</div>
<mat-icon class="text-green-600 text-3xl" *ngIf="portfolioSummary().totalPnL >= 0">trending_up</mat-icon>
<mat-icon class="text-red-600 text-3xl" *ngIf="portfolioSummary().totalPnL < 0">trending_down</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Day Change</p>
<p class="text-lg font-semibold" [class]="getPnLColor(portfolioSummary().dayChange)">
{{ portfolioSummary().dayChange > 0 ? '+' : '' }}${{ portfolioSummary().dayChange.toLocaleString() }}
({{ portfolioSummary().dayChangePercent.toFixed(2) }}%)
</p>
</div>
<mat-icon class="text-purple-600 text-3xl">today</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Cash Available</p>
<p class="text-lg font-semibold text-gray-900">${{ portfolioSummary().cash.toLocaleString() }}</p>
</div>
<mat-icon class="text-yellow-600 text-3xl">attach_money</mat-icon>
</div>
</mat-card>
</div>
<!-- Portfolio Tabs -->
<mat-tab-group>
<mat-tab label="Positions">
<div class="p-6">
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Current Positions</h3>
<div class="flex gap-2">
<button mat-button>
<mat-icon>add</mat-icon>
Add Position
</button>
<button mat-button>
<mat-icon>file_download</mat-icon>
Export
</button>
</div>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading portfolio...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else if (positions().length === 0) {
<div class="text-center py-8 text-gray-500">
<mat-icon class="text-4xl">account_balance_wallet</mat-icon>
<p class="mt-2">No positions found</p>
<button mat-button color="primary" class="mt-2">
<mat-icon>add</mat-icon>
Add Your First Position
</button>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="positions()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let position" class="font-semibold text-gray-900">{{ position.symbol }}</td>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Quantity</th>
<td mat-cell *matCellDef="let position" class="text-right text-gray-600">
{{ position.quantity.toLocaleString() }}
</td>
</ng-container>
<!-- Average Price Column -->
<ng-container matColumnDef="avgPrice">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Avg Price</th>
<td mat-cell *matCellDef="let position" class="text-right text-gray-600">
${{ position.avgPrice.toFixed(2) }}
</td>
</ng-container>
<!-- Current Price Column -->
<ng-container matColumnDef="currentPrice">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Current Price</th>
<td mat-cell *matCellDef="let position" class="text-right font-medium text-gray-900">
${{ position.currentPrice.toFixed(2) }}
</td>
</ng-container>
<!-- Market Value Column -->
<ng-container matColumnDef="marketValue">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Market Value</th>
<td mat-cell *matCellDef="let position" class="text-right font-medium text-gray-900">
${{ position.marketValue.toLocaleString() }}
</td>
</ng-container>
<!-- Unrealized P&L Column -->
<ng-container matColumnDef="unrealizedPnL">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Unrealized P&L</th>
<td mat-cell *matCellDef="let position"
class="text-right font-medium"
[class]="getPnLColor(position.unrealizedPnL)">
{{ position.unrealizedPnL > 0 ? '+' : '' }}${{ position.unrealizedPnL.toLocaleString() }}
({{ position.unrealizedPnLPercent.toFixed(2) }}%)
</td>
</ng-container>
<!-- Day Change Column -->
<ng-container matColumnDef="dayChange">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Day Change</th>
<td mat-cell *matCellDef="let position"
class="text-right font-medium"
[class]="getPnLColor(position.dayChange)">
{{ position.dayChange > 0 ? '+' : '' }}${{ position.dayChange.toFixed(2) }}
({{ position.dayChangePercent.toFixed(2) }}%)
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
</div>
</mat-tab>
<mat-tab label="Performance">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">trending_up</mat-icon>
<p class="mb-4">Performance charts and analytics will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Orders">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">receipt</mat-icon>
<p class="mb-4">Order history and management will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>
</div>

View file

@ -0,0 +1,172 @@
import { CommonModule } from '@angular/common';
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { interval, Subscription } from 'rxjs';
import { ApiService } from '../../services/api.service';
export interface Position {
symbol: string;
quantity: number;
avgPrice: number;
currentPrice: number;
marketValue: number;
unrealizedPnL: number;
unrealizedPnLPercent: number;
dayChange: number;
dayChangePercent: number;
}
export interface PortfolioSummary {
totalValue: number;
totalCost: number;
totalPnL: number;
totalPnLPercent: number;
dayChange: number;
dayChangePercent: number;
cash: number;
positionsCount: number;
}
@Component({
selector: 'app-portfolio',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatTabsModule,
],
templateUrl: './portfolio.component.html',
styleUrl: './portfolio.component.css',
})
export class PortfolioComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected portfolioSummary = signal<PortfolioSummary>({
totalValue: 0,
totalCost: 0,
totalPnL: 0,
totalPnLPercent: 0,
dayChange: 0,
dayChangePercent: 0,
cash: 0,
positionsCount: 0,
});
protected positions = signal<Position[]>([]);
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns = [
'symbol',
'quantity',
'avgPrice',
'currentPrice',
'marketValue',
'unrealizedPnL',
'dayChange',
];
ngOnInit() {
this.loadPortfolioData();
// Refresh portfolio data every 30 seconds
const portfolioSubscription = interval(30000).subscribe(() => {
this.loadPortfolioData();
});
this.subscriptions.push(portfolioSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadPortfolioData() {
// Since we don't have a portfolio endpoint yet, let's create mock data
// In a real implementation, this would call this.apiService.getPortfolio()
setTimeout(() => {
const mockPositions: Position[] = [
{
symbol: 'AAPL',
quantity: 100,
avgPrice: 180.5,
currentPrice: 192.53,
marketValue: 19253,
unrealizedPnL: 1203,
unrealizedPnLPercent: 6.67,
dayChange: 241,
dayChangePercent: 1.27,
},
{
symbol: 'MSFT',
quantity: 50,
avgPrice: 400.0,
currentPrice: 415.26,
marketValue: 20763,
unrealizedPnL: 763,
unrealizedPnLPercent: 3.82,
dayChange: 436.5,
dayChangePercent: 2.15,
},
{
symbol: 'GOOGL',
quantity: 10,
avgPrice: 2900.0,
currentPrice: 2847.56,
marketValue: 28475.6,
unrealizedPnL: -524.4,
unrealizedPnLPercent: -1.81,
dayChange: -123.4,
dayChangePercent: -0.43,
},
];
const summary: PortfolioSummary = {
totalValue: mockPositions.reduce((sum, pos) => sum + pos.marketValue, 0) + 25000, // + cash
totalCost: mockPositions.reduce((sum, pos) => sum + pos.avgPrice * pos.quantity, 0),
totalPnL: mockPositions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0),
totalPnLPercent: 0,
dayChange: mockPositions.reduce((sum, pos) => sum + pos.dayChange, 0),
dayChangePercent: 0,
cash: 25000,
positionsCount: mockPositions.length,
};
summary.totalPnLPercent = (summary.totalPnL / summary.totalCost) * 100;
summary.dayChangePercent =
(summary.dayChange / (summary.totalValue - summary.dayChange)) * 100;
this.positions.set(mockPositions);
this.portfolioSummary.set(summary);
this.isLoading.set(false);
this.error.set(null);
}, 1000);
}
refreshData() {
this.isLoading.set(true);
this.loadPortfolioData();
}
getPnLColor(value: number): string {
if (value > 0) {
return 'text-green-600';
}
if (value < 0) {
return 'text-red-600';
}
return 'text-gray-600';
}
}

View file

@ -0,0 +1 @@
/* Risk Management specific styles */

View file

@ -0,0 +1,178 @@
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Risk Management</h1>
<p class="text-gray-600 mt-1">Monitor and control trading risks and exposure</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<!-- Risk Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
@if (riskThresholds(); as thresholds) {
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Max Position Size</p>
<p class="text-lg font-semibold text-gray-900">${{ thresholds.maxPositionSize.toLocaleString() }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Max Daily Loss</p>
<p class="text-lg font-semibold text-red-600">${{ thresholds.maxDailyLoss.toLocaleString() }}</p>
</div>
<mat-icon class="text-red-600 text-3xl">trending_down</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Portfolio Risk Limit</p>
<p class="text-lg font-semibold text-yellow-600">{{ (thresholds.maxPortfolioRisk * 100).toFixed(1) }}%</p>
</div>
<mat-icon class="text-yellow-600 text-3xl">pie_chart</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Volatility Limit</p>
<p class="text-lg font-semibold text-purple-600">{{ (thresholds.volatilityLimit * 100).toFixed(1) }}%</p>
</div>
<mat-icon class="text-purple-600 text-3xl">show_chart</mat-icon>
</div>
</mat-card>
}
</div>
<!-- Risk Thresholds Configuration -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Risk Thresholds Configuration</h3>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading risk settings...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else {
<form [formGroup]="thresholdsForm" (ngSubmit)="saveThresholds()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<mat-form-field appearance="outline">
<mat-label>Max Position Size ($)</mat-label>
<input matInput type="number" formControlName="maxPositionSize" placeholder="100000">
<mat-icon matSuffix>attach_money</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Daily Loss ($)</mat-label>
<input matInput type="number" formControlName="maxDailyLoss" placeholder="5000">
<mat-icon matSuffix>trending_down</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Portfolio Risk (0-1)</mat-label>
<input matInput type="number" step="0.01" formControlName="maxPortfolioRisk" placeholder="0.1">
<mat-icon matSuffix>pie_chart</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Volatility Limit (0-1)</mat-label>
<input matInput type="number" step="0.01" formControlName="volatilityLimit" placeholder="0.3">
<mat-icon matSuffix>show_chart</mat-icon>
</mat-form-field>
</div>
<div class="flex justify-end mt-4">
<button mat-raised-button color="primary" type="submit" [disabled]="thresholdsForm.invalid || isSaving()">
@if (isSaving()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Save Thresholds
</button>
</div>
</form>
}
</mat-card>
<!-- Risk History Table -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Risk Evaluations</h3>
</div>
@if (riskHistory().length === 0) {
<div class="text-center py-8 text-gray-500">
<mat-icon class="text-4xl">history</mat-icon>
<p class="mt-2">No risk evaluations found</p>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="riskHistory()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let risk" class="font-semibold text-gray-900">{{ risk.symbol }}</td>
</ng-container>
<!-- Position Value Column -->
<ng-container matColumnDef="positionValue">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Position Value</th>
<td mat-cell *matCellDef="let risk" class="text-right font-medium text-gray-900">
${{ risk.positionValue.toLocaleString() }}
</td>
</ng-container>
<!-- Risk Level Column -->
<ng-container matColumnDef="riskLevel">
<th mat-header-cell *matHeaderCellDef class="text-center font-medium text-gray-900">Risk Level</th>
<td mat-cell *matCellDef="let risk" class="text-center">
<span class="px-2 py-1 rounded-full text-sm font-medium" [class]="getRiskLevelColor(risk.riskLevel)">
{{ risk.riskLevel }}
</span>
</td>
</ng-container>
<!-- Violations Column -->
<ng-container matColumnDef="violations">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Violations</th>
<td mat-cell *matCellDef="let risk" class="text-gray-600">
@if (risk.violations.length > 0) {
<span class="text-red-600">{{ risk.violations.join(', ') }}</span>
} @else {
<span class="text-green-600">None</span>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
</div>

View file

@ -0,0 +1,139 @@
import { CommonModule } from '@angular/common';
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { interval, Subscription } from 'rxjs';
import { ApiService, RiskEvaluation, RiskThresholds } from '../../services/api.service';
@Component({
selector: 'app-risk-management',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatFormFieldModule,
MatInputModule,
MatSnackBarModule,
MatProgressSpinnerModule,
ReactiveFormsModule,
],
templateUrl: './risk-management.component.html',
styleUrl: './risk-management.component.css',
})
export class RiskManagementComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private snackBar = inject(MatSnackBar);
private fb = inject(FormBuilder);
private subscriptions: Subscription[] = [];
protected riskThresholds = signal<RiskThresholds | null>(null);
protected riskHistory = signal<RiskEvaluation[]>([]);
protected isLoading = signal<boolean>(true);
protected isSaving = signal<boolean>(false);
protected error = signal<string | null>(null);
protected thresholdsForm: FormGroup;
protected displayedColumns = ['symbol', 'positionValue', 'riskLevel', 'violations', 'timestamp'];
constructor() {
this.thresholdsForm = this.fb.group({
maxPositionSize: [0, [Validators.required, Validators.min(0)]],
maxDailyLoss: [0, [Validators.required, Validators.min(0)]],
maxPortfolioRisk: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
volatilityLimit: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
});
}
ngOnInit() {
this.loadRiskThresholds();
this.loadRiskHistory();
// Refresh risk history every 30 seconds
const historySubscription = interval(30000).subscribe(() => {
this.loadRiskHistory();
});
this.subscriptions.push(historySubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadRiskThresholds() {
this.apiService.getRiskThresholds().subscribe({
next: response => {
this.riskThresholds.set(response.data);
this.thresholdsForm.patchValue(response.data);
this.isLoading.set(false);
this.error.set(null);
},
error: err => {
console.error('Failed to load risk thresholds:', err);
this.error.set('Failed to load risk thresholds');
this.isLoading.set(false);
this.snackBar.open('Failed to load risk thresholds', 'Dismiss', { duration: 5000 });
},
});
}
private loadRiskHistory() {
this.apiService.getRiskHistory().subscribe({
next: response => {
this.riskHistory.set(response.data);
},
error: err => {
console.error('Failed to load risk history:', err);
this.snackBar.open('Failed to load risk history', 'Dismiss', { duration: 3000 });
},
});
}
saveThresholds() {
if (this.thresholdsForm.valid) {
this.isSaving.set(true);
const thresholds = this.thresholdsForm.value as RiskThresholds;
this.apiService.updateRiskThresholds(thresholds).subscribe({
next: response => {
this.riskThresholds.set(response.data);
this.isSaving.set(false);
this.snackBar.open('Risk thresholds updated successfully', 'Dismiss', { duration: 3000 });
},
error: err => {
console.error('Failed to save risk thresholds:', err);
this.isSaving.set(false);
this.snackBar.open('Failed to save risk thresholds', 'Dismiss', { duration: 5000 });
},
});
}
}
refreshData() {
this.isLoading.set(true);
this.loadRiskThresholds();
this.loadRiskHistory();
}
getRiskLevelColor(level: string): string {
switch (level) {
case 'LOW':
return 'text-green-600';
case 'MEDIUM':
return 'text-yellow-600';
case 'HIGH':
return 'text-red-600';
default:
return 'text-gray-600';
}
}
}

View file

@ -0,0 +1 @@
/* Settings specific styles */

View file

@ -0,0 +1,15 @@
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Settings</h1>
<p class="text-gray-600 mt-1">Configure application preferences and system settings</p>
</div>
</div>
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">settings</mat-icon>
<p class="mb-4">Application settings and configuration will be implemented here</p>
</div>
</mat-card>
</div>

View file

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
@Component({
selector: 'app-settings',
standalone: true,
imports: [CommonModule, MatCardModule, MatIconModule],
templateUrl: './settings.component.html',
styleUrl: './settings.component.css',
})
export class SettingsComponent {}

View file

@ -0,0 +1,167 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Chart, ChartOptions } from 'chart.js/auto';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-drawdown-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="drawdown-chart-container">
<canvas #drawdownChart></canvas>
</div>
`,
styles: `
.drawdown-chart-container {
width: 100%;
height: 300px;
margin-bottom: 20px;
}
`,
})
export class DrawdownChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) {
return;
}
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Calculate drawdown series from daily returns
const drawdownData = this.calculateDrawdownSeries(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: drawdownData.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Drawdown',
data: drawdownData.drawdowns,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true,
tension: 0.3,
borderWidth: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0,
},
grid: {
display: false,
},
},
y: {
ticks: {
callback: function (value) {
return (value * 100).toFixed(1) + '%';
},
},
grid: {
color: 'rgba(200, 200, 200, 0.2)',
},
min: -0.05, // Show at least 5% drawdown for context
suggestedMax: 0.01,
},
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += (context.parsed.y * 100).toFixed(2) + '%';
}
return label;
},
},
},
legend: {
position: 'top',
},
},
} as ChartOptions,
});
}
private calculateDrawdownSeries(result: BacktestResult): {
dates: Date[];
drawdowns: number[];
} {
const dates: Date[] = [];
const drawdowns: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate equity curve
let equity = 1;
const equityCurve: number[] = [];
for (const daily of sortedReturns) {
equity *= 1 + daily.return;
equityCurve.push(equity);
dates.push(new Date(daily.date));
}
// Calculate running maximum (high water mark)
let hwm = equityCurve[0];
for (let i = 0; i < equityCurve.length; i++) {
// Update high water mark
hwm = Math.max(hwm, equityCurve[i]);
// Calculate drawdown as percentage from high water mark
const drawdown = equityCurve[i] / hwm - 1;
drawdowns.push(drawdown);
}
return { dates, drawdowns };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
}

View file

@ -0,0 +1,175 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Chart, ChartOptions } from 'chart.js/auto';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-equity-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="equity-chart-container">
<canvas #equityChart></canvas>
</div>
`,
styles: `
.equity-chart-container {
width: 100%;
height: 400px;
margin-bottom: 20px;
}
`,
})
export class EquityChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) {
return;
}
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Prepare data
const equityCurve = this.calculateEquityCurve(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: equityCurve.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Portfolio Value',
data: equityCurve.values,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.3,
borderWidth: 2,
fill: true,
},
{
label: 'Benchmark',
data: equityCurve.benchmark,
borderColor: 'rgba(153, 102, 255, 0.5)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 1,
fill: false,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0,
},
grid: {
display: false,
},
},
y: {
ticks: {
callback: function (value) {
return '$' + value.toLocaleString();
},
},
grid: {
color: 'rgba(200, 200, 200, 0.2)',
},
},
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(context.parsed.y);
}
return label;
},
},
},
legend: {
position: 'top',
},
},
} as ChartOptions,
});
}
private calculateEquityCurve(result: BacktestResult): {
dates: Date[];
values: number[];
benchmark: number[];
} {
const initialValue = result.initialCapital;
const dates: Date[] = [];
const values: number[] = [];
const benchmark: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate cumulative portfolio values
let portfolioValue = initialValue;
let benchmarkValue = initialValue;
for (const daily of sortedReturns) {
const date = new Date(daily.date);
portfolioValue = portfolioValue * (1 + daily.return);
// Simple benchmark (e.g., assuming 8% annualized return for a market index)
benchmarkValue = benchmarkValue * (1 + 0.08 / 365);
dates.push(date);
values.push(portfolioValue);
benchmark.push(benchmarkValue);
}
return { dates, values, benchmark };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
}

View file

@ -0,0 +1,322 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-performance-metrics',
standalone: true,
imports: [CommonModule, MatCardModule, MatGridListModule, MatDividerModule, MatTooltipModule],
template: `
<mat-card class="metrics-card">
<mat-card-header>
<mat-card-title>Performance Metrics</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="metrics-grid">
<div class="metric-group">
<h3>Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total return over the backtest period">
Total Return
</div>
<div
class="metric-value"
[ngClass]="getReturnClass(backtestResult?.totalReturn || 0)"
>
{{ formatPercent(backtestResult?.totalReturn || 0) }}
</div>
</div>
<div class="metric">
<div
class="metric-name"
matTooltip="Annualized return (adjusted for the backtest duration)"
>
Annualized Return
</div>
<div
class="metric-value"
[ngClass]="getReturnClass(backtestResult?.annualizedReturn || 0)"
>
{{ formatPercent(backtestResult?.annualizedReturn || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Compound Annual Growth Rate">CAGR</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.cagr || 0)">
{{ formatPercent(backtestResult?.cagr || 0) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk Metrics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">
Max Drawdown
</div>
<div class="metric-value negative">
{{ formatPercent(backtestResult?.maxDrawdown || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Number of days in the worst drawdown">
Max DD Duration
</div>
<div class="metric-value">
{{ formatDays(backtestResult?.maxDrawdownDuration || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized standard deviation of returns">
Volatility
</div>
<div class="metric-value">
{{ formatPercent(backtestResult?.volatility || 0) }}
</div>
</div>
<div class="metric">
<div
class="metric-name"
matTooltip="Square root of the sum of the squares of drawdowns"
>
Ulcer Index
</div>
<div class="metric-value">
{{ (backtestResult?.ulcerIndex || 0).toFixed(4) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk-Adjusted Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Excess return per unit of risk">
Sharpe Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)"
>
{{ (backtestResult?.sharpeRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of downside risk">
Sortino Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)"
>
{{ (backtestResult?.sortinoRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of max drawdown">
Calmar Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)"
>
{{ (backtestResult?.calmarRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div
class="metric-name"
matTooltip="Probability-weighted ratio of gains vs. losses"
>
Omega Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.omegaRatio || 0)"
>
{{ (backtestResult?.omegaRatio || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Trade Statistics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total number of trades">Total Trades</div>
<div class="metric-value">
{{ backtestResult?.totalTrades || 0 }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Percentage of winning trades">Win Rate</div>
<div class="metric-value" [ngClass]="getWinRateClass(backtestResult?.winRate || 0)">
{{ formatPercent(backtestResult?.winRate || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average profit of winning trades">Avg Win</div>
<div class="metric-value positive">
{{ formatPercent(backtestResult?.averageWinningTrade || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average loss of losing trades">Avg Loss</div>
<div class="metric-value negative">
{{ formatPercent(backtestResult?.averageLosingTrade || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Ratio of total gains to total losses">
Profit Factor
</div>
<div
class="metric-value"
[ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)"
>
{{ (backtestResult?.profitFactor || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
`,
styles: `
.metrics-card {
margin-bottom: 20px;
}
.metrics-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.metric-group {
padding: 10px 0;
}
.metric-group h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
color: #555;
}
.metrics-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.metric {
min-width: 120px;
margin-bottom: 16px;
}
.metric-name {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.metric-value {
font-size: 16px;
font-weight: 500;
}
.positive {
color: #4caf50;
}
.negative {
color: #f44336;
}
.neutral {
color: #ffa000;
}
mat-divider {
margin: 8px 0;
}
`,
})
export class PerformanceMetricsComponent {
@Input() backtestResult?: BacktestResult;
// Formatting helpers
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
formatDays(days: number): string {
return `${days} days`;
}
// Conditional classes
getReturnClass(value: number): string {
if (value > 0) {
return 'positive';
}
if (value < 0) {
return 'negative';
}
return '';
}
getRatioClass(value: number): string {
if (value >= 1.5) {
return 'positive';
}
if (value >= 1) {
return 'neutral';
}
if (value < 0) {
return 'negative';
}
return '';
}
getWinRateClass(value: number): string {
if (value >= 0.55) {
return 'positive';
}
if (value >= 0.45) {
return 'neutral';
}
return 'negative';
}
getProfitFactorClass(value: number): string {
if (value >= 1.5) {
return 'positive';
}
if (value >= 1) {
return 'neutral';
}
return 'negative';
}
}

View file

@ -0,0 +1,259 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-trades-table',
standalone: true,
imports: [
CommonModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatCardModule,
MatIconModule,
],
template: `
<mat-card class="trades-card">
<mat-card-header>
<mat-card-title>Trades</mat-card-title>
</mat-card-header>
<mat-card-content>
<table
mat-table
[dataSource]="displayedTrades"
matSort
(matSortChange)="sortData($event)"
class="trades-table"
>
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Symbol</th>
<td mat-cell *matCellDef="let trade">{{ trade.symbol }}</td>
</ng-container>
<!-- Entry Date Column -->
<ng-container matColumnDef="entryTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Entry Time</th>
<td mat-cell *matCellDef="let trade">{{ formatDate(trade.entryTime) }}</td>
</ng-container>
<!-- Entry Price Column -->
<ng-container matColumnDef="entryPrice">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Entry Price</th>
<td mat-cell *matCellDef="let trade">{{ formatCurrency(trade.entryPrice) }}</td>
</ng-container>
<!-- Exit Date Column -->
<ng-container matColumnDef="exitTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Exit Time</th>
<td mat-cell *matCellDef="let trade">{{ formatDate(trade.exitTime) }}</td>
</ng-container>
<!-- Exit Price Column -->
<ng-container matColumnDef="exitPrice">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Exit Price</th>
<td mat-cell *matCellDef="let trade">{{ formatCurrency(trade.exitPrice) }}</td>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Quantity</th>
<td mat-cell *matCellDef="let trade">{{ trade.quantity }}</td>
</ng-container>
<!-- P&L Column -->
<ng-container matColumnDef="pnl">
<th mat-header-cell *matHeaderCellDef mat-sort-header>P&L</th>
<td
mat-cell
*matCellDef="let trade"
[ngClass]="{ positive: trade.pnl > 0, negative: trade.pnl < 0 }"
>
{{ formatCurrency(trade.pnl) }}
</td>
</ng-container>
<!-- P&L Percent Column -->
<ng-container matColumnDef="pnlPercent">
<th mat-header-cell *matHeaderCellDef mat-sort-header>P&L %</th>
<td
mat-cell
*matCellDef="let trade"
[ngClass]="{ positive: trade.pnlPercent > 0, negative: trade.pnlPercent < 0 }"
>
{{ formatPercent(trade.pnlPercent) }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator
[length]="totalTrades"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50]"
(page)="pageChange($event)"
aria-label="Select page"
>
</mat-paginator>
</mat-card-content>
</mat-card>
`,
styles: `
.trades-card {
margin-bottom: 20px;
}
.trades-table {
width: 100%;
border-collapse: collapse;
}
.mat-column-pnl,
.mat-column-pnlPercent {
text-align: right;
font-weight: 500;
}
.positive {
color: #4caf50;
}
.negative {
color: #f44336;
}
.mat-mdc-row:hover {
background-color: rgba(0, 0, 0, 0.04);
}
`,
})
export class TradesTableComponent {
@Input() set backtestResult(value: BacktestResult | undefined) {
if (value) {
this._backtestResult = value;
this.updateDisplayedTrades();
}
}
get backtestResult(): BacktestResult | undefined {
return this._backtestResult;
}
private _backtestResult?: BacktestResult;
// Table configuration
displayedColumns: string[] = [
'symbol',
'entryTime',
'entryPrice',
'exitTime',
'exitPrice',
'quantity',
'pnl',
'pnlPercent',
];
// Pagination
pageSize = 10;
currentPage = 0;
displayedTrades: any[] = [];
get totalTrades(): number {
return this._backtestResult?.trades.length || 0;
}
// Sort the trades
sortData(sort: Sort): void {
if (!sort.active || sort.direction === '') {
this.updateDisplayedTrades();
return;
}
const data = this._backtestResult?.trades.slice() || [];
this.displayedTrades = data
.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'symbol':
return this.compare(a.symbol, b.symbol, isAsc);
case 'entryTime':
return this.compare(
new Date(a.entryTime).getTime(),
new Date(b.entryTime).getTime(),
isAsc
);
case 'entryPrice':
return this.compare(a.entryPrice, b.entryPrice, isAsc);
case 'exitTime':
return this.compare(
new Date(a.exitTime).getTime(),
new Date(b.exitTime).getTime(),
isAsc
);
case 'exitPrice':
return this.compare(a.exitPrice, b.exitPrice, isAsc);
case 'quantity':
return this.compare(a.quantity, b.quantity, isAsc);
case 'pnl':
return this.compare(a.pnl, b.pnl, isAsc);
case 'pnlPercent':
return this.compare(a.pnlPercent, b.pnlPercent, isAsc);
default:
return 0;
}
})
.slice(this.currentPage * this.pageSize, (this.currentPage + 1) * this.pageSize);
}
// Handle page changes
pageChange(event: PageEvent): void {
this.pageSize = event.pageSize;
this.currentPage = event.pageIndex;
this.updateDisplayedTrades();
}
// Update displayed trades based on current page and page size
updateDisplayedTrades(): void {
if (this._backtestResult) {
this.displayedTrades = this._backtestResult.trades.slice(
this.currentPage * this.pageSize,
(this.currentPage + 1) * this.pageSize
);
} else {
this.displayedTrades = [];
}
}
// Helper methods for formatting
formatDate(date: Date | string): string {
return new Date(date).toLocaleString();
}
formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
}
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
private compare(a: number | string, b: number | string, isAsc: boolean): number {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
}

View file

@ -0,0 +1,195 @@
import { CommonModule } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatNativeDateModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTabsModule } from '@angular/material/tabs';
import {
BacktestRequest,
BacktestResult,
StrategyService,
TradingStrategy,
} from '../../../services/strategy.service';
@Component({
selector: 'app-backtest-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatDatepickerModule,
MatNativeDateModule,
MatProgressBarModule,
MatTabsModule,
MatChipsModule,
MatIconModule,
MatSlideToggleModule,
],
templateUrl: './backtest-dialog.component.html',
styleUrl: './backtest-dialog.component.css',
})
export class BacktestDialogComponent implements OnInit {
backtestForm: FormGroup;
strategyTypes: string[] = [];
availableSymbols: string[] = [
'AAPL',
'MSFT',
'GOOGL',
'AMZN',
'TSLA',
'META',
'NVDA',
'SPY',
'QQQ',
];
selectedSymbols: string[] = [];
parameters: Record<string, any> = {};
isRunning: boolean = false;
backtestResult: BacktestResult | null = null;
constructor(
private fb: FormBuilder,
private strategyService: StrategyService,
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
private dialogRef: MatDialogRef<BacktestDialogComponent>
) {
// Initialize form with defaults
this.backtestForm = this.fb.group({
strategyType: ['', [Validators.required]],
startDate: [
new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
[Validators.required],
],
endDate: [new Date(), [Validators.required]],
initialCapital: [100000, [Validators.required, Validators.min(1000)]],
dataResolution: ['1d', [Validators.required]],
commission: [0.001, [Validators.required, Validators.min(0), Validators.max(0.1)]],
slippage: [0.0005, [Validators.required, Validators.min(0), Validators.max(0.1)]],
mode: ['event', [Validators.required]],
});
// If strategy is provided, pre-populate the form
if (data) {
this.selectedSymbols = [...data.symbols];
this.backtestForm.patchValue({
strategyType: data.type,
});
this.parameters = { ...data.parameters };
}
}
ngOnInit(): void {
this.loadStrategyTypes();
}
loadStrategyTypes(): void {
this.strategyService.getStrategyTypes().subscribe({
next: response => {
if (response.success) {
this.strategyTypes = response.data;
// If strategy is provided, load its parameters
if (this.data) {
this.onStrategyTypeChange(this.data.type);
}
}
},
error: error => {
console.error('Error loading strategy types:', error);
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
},
});
}
onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({
next: response => {
if (response.success) {
// If strategy is provided, merge default with existing
if (this.data) {
this.parameters = {
...response.data,
...this.data.parameters,
};
} else {
this.parameters = response.data;
}
}
},
error: error => {
console.error('Error loading parameters:', error);
this.parameters = {};
},
});
}
addSymbol(symbol: string): void {
if (!symbol || this.selectedSymbols.includes(symbol)) {
return;
}
this.selectedSymbols.push(symbol);
}
removeSymbol(symbol: string): void {
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
}
updateParameter(key: string, value: any): void {
this.parameters[key] = value;
}
onSubmit(): void {
if (this.backtestForm.invalid || this.selectedSymbols.length === 0) {
return;
}
const formValue = this.backtestForm.value;
const backtestRequest: BacktestRequest = {
strategyType: formValue.strategyType,
strategyParams: this.parameters,
symbols: this.selectedSymbols,
startDate: formValue.startDate,
endDate: formValue.endDate,
initialCapital: formValue.initialCapital,
dataResolution: formValue.dataResolution,
commission: formValue.commission,
slippage: formValue.slippage,
mode: formValue.mode,
};
this.isRunning = true;
this.strategyService.runBacktest(backtestRequest).subscribe({
next: response => {
this.isRunning = false;
if (response.success) {
this.backtestResult = response.data;
}
},
error: error => {
this.isRunning = false;
console.error('Backtest error:', error);
},
});
}
close(): void {
this.dialogRef.close(this.backtestResult);
}
}

View file

@ -0,0 +1,84 @@
<h2 mat-dialog-title>{{isEditMode ? 'Edit Strategy' : 'Create Strategy'}}</h2>
<form [formGroup]="strategyForm" (ngSubmit)="onSubmit()">
<mat-dialog-content class="mat-typography">
<div class="grid grid-cols-1 gap-4">
<!-- Basic Strategy Information -->
<mat-form-field appearance="outline" class="w-full">
<mat-label>Strategy Name</mat-label>
<input matInput formControlName="name" placeholder="e.g., My Moving Average Crossover">
<mat-error *ngIf="strategyForm.get('name')?.invalid">Name is required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description" rows="3"
placeholder="Describe what this strategy does..."></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Strategy Type</mat-label>
<mat-select formControlName="type" (selectionChange)="onStrategyTypeChange($event.value)">
<mat-option *ngFor="let type of strategyTypes" [value]="type">
{{type}}
</mat-option>
</mat-select>
<mat-error *ngIf="strategyForm.get('type')?.invalid">Strategy type is required</mat-error>
</mat-form-field>
<!-- Symbol Selection -->
<div class="w-full">
<label class="text-sm">Trading Symbols</label>
<div class="flex flex-wrap gap-2 mb-2">
<mat-chip *ngFor="let symbol of selectedSymbols" [removable]="true"
(removed)="removeSymbol(symbol)">
{{symbol}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</div>
<div class="flex gap-2">
<mat-form-field appearance="outline" class="flex-1">
<mat-label>Add Symbol</mat-label>
<input matInput #symbolInput placeholder="e.g., AAPL">
</mat-form-field>
<button type="button" mat-raised-button color="primary"
(click)="addSymbol(symbolInput.value); symbolInput.value = ''">
Add
</button>
</div>
<div class="mt-2">
<p class="text-sm text-gray-500 mb-1">Suggested symbols:</p>
<div class="flex flex-wrap gap-2">
<button type="button" *ngFor="let symbol of availableSymbols"
mat-stroked-button (click)="addSymbol(symbol)"
[disabled]="selectedSymbols.includes(symbol)">
{{symbol}}
</button>
</div>
</div>
</div>
<!-- Dynamic Strategy Parameters -->
<div *ngIf="strategyForm.get('type')?.value && Object.keys(parameters).length > 0">
<h3 class="text-lg font-semibold mb-2">Strategy Parameters</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field *ngFor="let param of parameters | keyvalue" appearance="outline">
<mat-label>{{param.key}}</mat-label>
<input matInput [value]="param.value"
(input)="updateParameter(param.key, $any($event.target).value)">
</mat-form-field>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-raised-button color="primary" type="submit"
[disabled]="strategyForm.invalid || selectedSymbols.length === 0">
{{isEditMode ? 'Update' : 'Create'}}
</button>
</mat-dialog-actions>
</form>

View file

@ -0,0 +1,182 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CommonModule } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { StrategyService, TradingStrategy } from '../../../services/strategy.service';
@Component({
selector: 'app-strategy-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatChipsModule,
MatIconModule,
MatAutocompleteModule,
],
templateUrl: './strategy-dialog.component.html',
styleUrl: './strategy-dialog.component.css',
})
export class StrategyDialogComponent implements OnInit {
strategyForm: FormGroup;
isEditMode: boolean = false;
strategyTypes: string[] = [];
availableSymbols: string[] = [
'AAPL',
'MSFT',
'GOOGL',
'AMZN',
'TSLA',
'META',
'NVDA',
'SPY',
'QQQ',
];
selectedSymbols: string[] = [];
separatorKeysCodes: number[] = [ENTER, COMMA];
parameters: Record<string, any> = {};
constructor(
private fb: FormBuilder,
private strategyService: StrategyService,
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
private dialogRef: MatDialogRef<StrategyDialogComponent>
) {
this.isEditMode = !!data;
this.strategyForm = this.fb.group({
name: ['', [Validators.required]],
description: [''],
type: ['', [Validators.required]],
// Dynamic parameters will be added based on strategy type
});
if (this.isEditMode && data) {
this.selectedSymbols = [...data.symbols];
this.strategyForm.patchValue({
name: data.name,
description: data.description,
type: data.type,
});
this.parameters = { ...data.parameters };
}
}
ngOnInit(): void {
// In a real implementation, fetch available strategy types from the API
this.loadStrategyTypes();
}
loadStrategyTypes(): void {
// In a real implementation, this would call the API
this.strategyService.getStrategyTypes().subscribe({
next: response => {
if (response.success) {
this.strategyTypes = response.data;
// If editing, load parameters
if (this.isEditMode && this.data) {
this.onStrategyTypeChange(this.data.type);
}
}
},
error: error => {
console.error('Error loading strategy types:', error);
// Fallback to hardcoded types
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
},
});
}
onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({
next: response => {
if (response.success) {
// If editing, merge default with existing
if (this.isEditMode && this.data) {
this.parameters = {
...response.data,
...this.data.parameters,
};
} else {
this.parameters = response.data;
}
}
},
error: error => {
console.error('Error loading parameters:', error);
// Fallback to empty parameters
this.parameters = {};
},
});
}
addSymbol(symbol: string): void {
if (!symbol || this.selectedSymbols.includes(symbol)) {
return;
}
this.selectedSymbols.push(symbol);
}
removeSymbol(symbol: string): void {
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
}
onSubmit(): void {
if (this.strategyForm.invalid || this.selectedSymbols.length === 0) {
return;
}
const formValue = this.strategyForm.value;
const strategy: Partial<TradingStrategy> = {
name: formValue.name,
description: formValue.description,
type: formValue.type,
symbols: this.selectedSymbols,
parameters: this.parameters,
};
if (this.isEditMode && this.data) {
this.strategyService.updateStrategy(this.data.id, strategy).subscribe({
next: response => {
if (response.success) {
this.dialogRef.close(true);
}
},
error: error => {
console.error('Error updating strategy:', error);
},
});
} else {
this.strategyService.createStrategy(strategy).subscribe({
next: response => {
if (response.success) {
this.dialogRef.close(true);
}
},
error: error => {
console.error('Error creating strategy:', error);
},
});
}
}
updateParameter(key: string, value: any): void {
this.parameters[key] = value;
}
}

View file

@ -0,0 +1 @@
/* Strategies specific styles */

View file

@ -0,0 +1,142 @@
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Trading Strategies</h1>
<p class="text-gray-600 mt-1">Configure and monitor your automated trading strategies</p>
</div>
<div class="flex gap-2">
<button mat-raised-button color="primary" (click)="openStrategyDialog()">
<mat-icon>add</mat-icon> New Strategy
</button>
<button mat-raised-button color="accent" (click)="openBacktestDialog()">
<mat-icon>science</mat-icon> New Backtest
</button>
</div>
</div>
<mat-card *ngIf="isLoading" class="p-4">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card>
<div *ngIf="!selectedStrategy; else strategyDetails">
<mat-card *ngIf="strategies.length > 0; else noStrategies" class="p-4">
<table mat-table [dataSource]="strategies" class="w-full">
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Strategy</th>
<td mat-cell *matCellDef="let strategy">
<div class="font-semibold">{{strategy.name}}</div>
<div class="text-xs text-gray-500">{{strategy.description}}</div>
</td>
</ng-container>
<!-- Type Column -->
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let strategy">{{strategy.type}}</td>
</ng-container>
<!-- Symbols Column -->
<ng-container matColumnDef="symbols">
<th mat-header-cell *matHeaderCellDef>Symbols</th>
<td mat-cell *matCellDef="let strategy">
<div class="flex flex-wrap gap-1 max-w-xs">
<mat-chip *ngFor="let symbol of strategy.symbols.slice(0, 3)">
{{symbol}}
</mat-chip>
<span *ngIf="strategy.symbols.length > 3" class="text-gray-500">
+{{strategy.symbols.length - 3}} more
</span>
</div>
</td>
</ng-container>
<!-- Status Column -->
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let strategy">
<div class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full"
[style.background-color]="getStatusColor(strategy.status)"></span>
{{strategy.status}}
</div>
</td>
</ng-container>
<!-- Performance Column -->
<ng-container matColumnDef="performance">
<th mat-header-cell *matHeaderCellDef>Performance</th>
<td mat-cell *matCellDef="let strategy">
<div class="flex flex-col">
<div class="flex justify-between">
<span class="text-xs text-gray-500">Return:</span>
<span [ngClass]="{'text-green-600': strategy.performance.totalReturn > 0,
'text-red-600': strategy.performance.totalReturn < 0}">
{{strategy.performance.totalReturn | percent:'1.2-2'}}
</span>
</div>
<div class="flex justify-between">
<span class="text-xs text-gray-500">Win Rate:</span>
<span>{{strategy.performance.winRate | percent:'1.0-0'}}</span>
</div>
</div>
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let strategy">
<div class="flex gap-2">
<button mat-icon-button color="primary" (click)="viewStrategyDetails(strategy)">
<mat-icon>visibility</mat-icon>
</button>
<button mat-icon-button [color]="strategy.status === 'ACTIVE' ? 'warn' : 'primary'"
(click)="toggleStrategyStatus(strategy)">
<mat-icon>{{strategy.status === 'ACTIVE' ? 'pause' : 'play_arrow'}}</mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="openStrategyDialog(strategy)">
<mat-icon>edit</mat-icon>
<span>Edit</span>
</button>
<button mat-menu-item (click)="openBacktestDialog(strategy)">
<mat-icon>science</mat-icon>
<span>Backtest</span>
</button>
</mat-menu>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-card>
<ng-template #noStrategies>
<mat-card class="p-6 flex flex-col items-center justify-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem; margin: 0 auto;">psychology</mat-icon>
<h3 class="text-xl font-semibold mt-4">No Strategies Yet</h3>
<p class="mb-4">Create your first trading strategy to get started</p>
<button mat-raised-button color="primary" (click)="openStrategyDialog()">
<mat-icon>add</mat-icon> Create Strategy
</button>
</div>
</mat-card>
</ng-template>
</div>
<ng-template #strategyDetails>
<div class="flex justify-between items-center mb-4">
<button mat-button (click)="selectedStrategy = null">
<mat-icon>arrow_back</mat-icon> Back to Strategies
</button>
</div>
<app-strategy-details [strategy]="selectedStrategy"></app-strategy-details>
</ng-template>
</div>

View file

@ -0,0 +1,154 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { StrategyService, TradingStrategy } from '../../services/strategy.service';
import { WebSocketService } from '../../services/websocket.service';
import { BacktestDialogComponent } from './dialogs/backtest-dialog.component';
import { StrategyDialogComponent } from './dialogs/strategy-dialog.component';
import { StrategyDetailsComponent } from './strategy-details/strategy-details.component';
@Component({
selector: 'app-strategies',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTabsModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatDialogModule,
MatMenuModule,
MatChipsModule,
MatProgressBarModule,
FormsModule,
ReactiveFormsModule,
StrategyDetailsComponent,
],
templateUrl: './strategies.component.html',
styleUrl: './strategies.component.css',
})
export class StrategiesComponent implements OnInit {
strategies: TradingStrategy[] = [];
displayedColumns: string[] = ['name', 'type', 'symbols', 'status', 'performance', 'actions'];
selectedStrategy: TradingStrategy | null = null;
isLoading = false;
constructor(
private strategyService: StrategyService,
private webSocketService: WebSocketService,
private dialog: MatDialog
) {}
ngOnInit(): void {
this.loadStrategies();
this.listenForStrategyUpdates();
}
loadStrategies(): void {
this.isLoading = true;
this.strategyService.getStrategies().subscribe({
next: response => {
if (response.success) {
this.strategies = response.data;
}
this.isLoading = false;
},
error: error => {
console.error('Error loading strategies:', error);
this.isLoading = false;
},
});
}
listenForStrategyUpdates(): void {
this.webSocketService.messages.subscribe(message => {
if (
message.type === 'STRATEGY_CREATED' ||
message.type === 'STRATEGY_UPDATED' ||
message.type === 'STRATEGY_STATUS_CHANGED'
) {
// Refresh the strategy list when changes occur
this.loadStrategies();
}
});
}
getStatusColor(status: string): string {
switch (status) {
case 'ACTIVE':
return 'green';
case 'PAUSED':
return 'orange';
case 'ERROR':
return 'red';
default:
return 'gray';
}
}
openStrategyDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px',
data: strategy || null,
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadStrategies();
}
});
}
openBacktestDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px',
data: strategy || null,
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Handle backtest result if needed
}
});
}
toggleStrategyStatus(strategy: TradingStrategy): void {
this.isLoading = true;
if (strategy.status === 'ACTIVE') {
this.strategyService.pauseStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(),
error: error => {
console.error('Error pausing strategy:', error);
this.isLoading = false;
},
});
} else {
this.strategyService.startStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(),
error: error => {
console.error('Error starting strategy:', error);
this.isLoading = false;
},
});
}
}
viewStrategyDetails(strategy: TradingStrategy): void {
this.selectedStrategy = strategy;
}
}

View file

@ -0,0 +1,16 @@
/* Strategy details specific styles */
table {
width: 100%;
border-collapse: collapse;
}
th {
font-weight: 600;
color: #4b5563;
font-size: 0.875rem;
border-bottom: 1px solid #e5e7eb;
}
td {
border-bottom: 1px solid #e5e7eb;
}

View file

@ -0,0 +1,249 @@
<div class="space-y-6" *ngIf="strategy">
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
<!-- Strategy Overview Card -->
<mat-card class="flex-1 p-4">
<div class="flex items-start justify-between">
<div>
<h2 class="text-xl font-bold">{{ strategy.name }}</h2>
<p class="text-gray-600 text-sm">{{ strategy.description }}</p>
</div>
<div class="flex items-center gap-2">
<button mat-raised-button color="primary" class="mr-2" (click)="openBacktestDialog()">
Run Backtest
</button>
<span
class="px-3 py-1 rounded-full text-xs font-semibold"
[style.background-color]="getStatusColor(strategy.status)"
style="color: white"
>
{{ strategy.status }}
</span>
</div>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 class="font-semibold text-sm text-gray-600">Type</h3>
<p>{{ strategy.type }}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Created</h3>
<p>{{ strategy.createdAt | date: 'medium' }}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Last Updated</h3>
<p>{{ strategy.updatedAt | date: 'medium' }}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Symbols</h3>
<div class="flex flex-wrap gap-1 mt-1">
<mat-chip *ngFor="let symbol of strategy.symbols">{{ symbol }}</mat-chip>
</div>
</div>
</div>
</mat-card>
<!-- Performance Summary Card -->
<mat-card class="md:w-1/3 p-4">
<h3 class="text-lg font-bold mb-3">Performance</h3>
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-sm text-gray-600">Return</p>
<p
class="text-xl font-semibold"
[ngClass]="{
'text-green-600': performance.totalReturn >= 0,
'text-red-600': performance.totalReturn < 0,
}"
>
{{ performance.totalReturn | percent: '1.2-2' }}
</p>
</div>
<div>
<p class="text-sm text-gray-600">Win Rate</p>
<p class="text-xl font-semibold">{{ performance.winRate | percent: '1.0-0' }}</p>
</div>
<div>
<p class="text-sm text-gray-600">Sharpe Ratio</p>
<p class="text-xl font-semibold">{{ performance.sharpeRatio | number: '1.2-2' }}</p>
</div>
<div>
<p class="text-sm text-gray-600">Max Drawdown</p>
<p class="text-xl font-semibold text-red-600">
{{ performance.maxDrawdown | percent: '1.2-2' }}
</p>
</div>
<div>
<p class="text-sm text-gray-600">Total Trades</p>
<p class="text-xl font-semibold">{{ performance.totalTrades }}</p>
</div>
<div>
<p class="text-sm text-gray-600">Sortino Ratio</p>
<p class="text-xl font-semibold">{{ performance.sortinoRatio | number: '1.2-2' }}</p>
</div>
</div>
<mat-divider class="my-4"></mat-divider>
<div class="flex justify-between mt-2">
<button
mat-button
color="primary"
*ngIf="strategy.status !== 'ACTIVE'"
(click)="activateStrategy()"
>
<mat-icon>play_arrow</mat-icon> Start
</button>
<button
mat-button
color="accent"
*ngIf="strategy.status === 'ACTIVE'"
(click)="pauseStrategy()"
>
<mat-icon>pause</mat-icon> Pause
</button>
<button
mat-button
color="warn"
*ngIf="strategy.status === 'ACTIVE'"
(click)="stopStrategy()"
>
<mat-icon>stop</mat-icon> Stop
</button>
<button mat-button (click)="openEditDialog()"><mat-icon>edit</mat-icon> Edit</button>
</div>
</mat-card>
</div>
<!-- Parameters Card -->
<mat-card class="p-4">
<h3 class="text-lg font-bold mb-3">Strategy Parameters</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div *ngFor="let param of strategy.parameters | keyvalue">
<p class="text-sm text-gray-600">{{ param.key }}</p>
<p class="font-semibold">{{ param.value }}</p>
</div>
</div>
</mat-card>
<!-- Backtest Results Section (only shown when a backtest has been run) -->
<div *ngIf="backtestResult" class="backtest-results space-y-6">
<h2 class="text-xl font-bold">Backtest Results</h2>
<!-- Performance Metrics Component -->
<app-performance-metrics [backtestResult]="backtestResult"></app-performance-metrics>
<!-- Equity Chart Component -->
<app-equity-chart [backtestResult]="backtestResult"></app-equity-chart>
<!-- Drawdown Chart Component -->
<app-drawdown-chart [backtestResult]="backtestResult"></app-drawdown-chart>
<!-- Trades Table Component -->
<app-trades-table [backtestResult]="backtestResult"></app-trades-table>
</div>
<!-- Tabs for Signals/Trades -->
<mat-card class="p-0">
<mat-tab-group>
<!-- Signals Tab -->
<mat-tab label="Recent Signals">
<div class="p-4">
<ng-container *ngIf="!isLoadingSignals; else loadingSignals">
<table class="min-w-full">
<thead>
<tr>
<th class="py-2 text-left">Time</th>
<th class="py-2 text-left">Symbol</th>
<th class="py-2 text-left">Action</th>
<th class="py-2 text-left">Price</th>
<th class="py-2 text-left">Quantity</th>
<th class="py-2 text-left">Confidence</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let signal of signals">
<td class="py-2">{{ signal.timestamp | date: 'short' }}</td>
<td class="py-2">{{ signal.symbol }}</td>
<td class="py-2">
<span
class="px-2 py-1 rounded text-xs font-semibold"
[style.background-color]="getSignalColor(signal.action)"
style="color: white"
>
{{ signal.action }}
</span>
</td>
<td class="py-2">${{ signal.price | number: '1.2-2' }}</td>
<td class="py-2">{{ signal.quantity }}</td>
<td class="py-2">{{ signal.confidence | percent: '1.0-0' }}</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-template #loadingSignals>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
</div>
</mat-tab>
<!-- Trades Tab -->
<mat-tab label="Recent Trades">
<div class="p-4">
<ng-container *ngIf="!isLoadingTrades; else loadingTrades">
<table class="min-w-full">
<thead>
<tr>
<th class="py-2 text-left">Symbol</th>
<th class="py-2 text-left">Entry</th>
<th class="py-2 text-left">Exit</th>
<th class="py-2 text-left">Quantity</th>
<th class="py-2 text-left">P&L</th>
<th class="py-2 text-left">P&L %</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let trade of trades">
<td class="py-2">{{ trade.symbol }}</td>
<td class="py-2">
${{ trade.entryPrice | number: '1.2-2' }} &#64;
{{ trade.entryTime | date: 'short' }}
</td>
<td class="py-2">
${{ trade.exitPrice | number: '1.2-2' }} &#64;
{{ trade.exitTime | date: 'short' }}
</td>
<td class="py-2">{{ trade.quantity }}</td>
<td
class="py-2"
[ngClass]="{ 'text-green-600': trade.pnl >= 0, 'text-red-600': trade.pnl < 0 }"
>
${{ trade.pnl | number: '1.2-2' }}
</td>
<td
class="py-2"
[ngClass]="{
'text-green-600': trade.pnlPercent >= 0,
'text-red-600': trade.pnlPercent < 0,
}"
>
{{ trade.pnlPercent | number: '1.2-2' }}%
</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-template #loadingTrades>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
</div>
</mat-tab>
</mat-tab-group>
</mat-card>
</div>
<mat-card class="p-6 flex items-center" *ngIf="!strategy">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem">psychology</mat-icon>
<p class="mb-4">No strategy selected</p>
</div>
</mat-card>

View file

@ -0,0 +1,415 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialog } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import {
BacktestResult,
StrategyService,
TradingStrategy,
} from '../../../services/strategy.service';
import { WebSocketService } from '../../../services/websocket.service';
import { DrawdownChartComponent } from '../components/drawdown-chart.component';
import { EquityChartComponent } from '../components/equity-chart.component';
import { PerformanceMetricsComponent } from '../components/performance-metrics.component';
import { TradesTableComponent } from '../components/trades-table.component';
import { BacktestDialogComponent } from '../dialogs/backtest-dialog.component';
import { StrategyDialogComponent } from '../dialogs/strategy-dialog.component';
@Component({
selector: 'app-strategy-details',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatTabsModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatChipsModule,
MatProgressBarModule,
MatDividerModule,
EquityChartComponent,
DrawdownChartComponent,
TradesTableComponent,
PerformanceMetricsComponent,
],
templateUrl: './strategy-details.component.html',
styleUrl: './strategy-details.component.css',
})
export class StrategyDetailsComponent implements OnChanges {
@Input() strategy: TradingStrategy | null = null;
signals: any[] = [];
trades: any[] = [];
performance: any = {};
isLoadingSignals = false;
isLoadingTrades = false;
backtestResult: BacktestResult | undefined;
constructor(
private strategyService: StrategyService,
private webSocketService: WebSocketService,
private dialog: MatDialog
) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['strategy'] && this.strategy) {
this.loadStrategyData();
this.listenForUpdates();
}
}
loadStrategyData(): void {
if (!this.strategy) {
return;
}
// In a real implementation, these would call API methods to fetch the data
this.loadSignals();
this.loadTrades();
this.loadPerformance();
}
loadSignals(): void {
if (!this.strategy) {
return;
}
this.isLoadingSignals = true;
// First check if we can get real signals from the API
this.strategyService.getStrategySignals(this.strategy.id).subscribe({
next: response => {
if (response.success && response.data && response.data.length > 0) {
this.signals = response.data;
} else {
// Fallback to mock data if no real signals available
this.signals = this.generateMockSignals();
}
this.isLoadingSignals = false;
},
error: error => {
console.error('Error loading signals', error);
// Fallback to mock data on error
this.signals = this.generateMockSignals();
this.isLoadingSignals = false;
},
});
}
loadTrades(): void {
if (!this.strategy) {
return;
}
this.isLoadingTrades = true;
// First check if we can get real trades from the API
this.strategyService.getStrategyTrades(this.strategy.id).subscribe({
next: response => {
if (response.success && response.data && response.data.length > 0) {
this.trades = response.data;
} else {
// Fallback to mock data if no real trades available
this.trades = this.generateMockTrades();
}
this.isLoadingTrades = false;
},
error: error => {
console.error('Error loading trades', error);
// Fallback to mock data on error
this.trades = this.generateMockTrades();
this.isLoadingTrades = false;
},
});
}
loadPerformance(): void {
// This would be an API call in a real implementation
this.performance = {
totalReturn: this.strategy?.performance.totalReturn || 0,
winRate: this.strategy?.performance.winRate || 0,
sharpeRatio: this.strategy?.performance.sharpeRatio || 0,
maxDrawdown: this.strategy?.performance.maxDrawdown || 0,
totalTrades: this.strategy?.performance.totalTrades || 0,
// Additional metrics that would come from the API
dailyReturn: 0.0012,
volatility: 0.008,
sortinoRatio: 1.2,
calmarRatio: 0.7,
};
}
listenForUpdates(): void {
if (!this.strategy) {
return;
}
// Subscribe to strategy signals
this.webSocketService.getStrategySignals(this.strategy.id).subscribe((signal: any) => {
// Add the new signal to the top of the list
this.signals = [signal, ...this.signals.slice(0, 9)]; // Keep only the latest 10 signals
});
// Subscribe to strategy trades
this.webSocketService.getStrategyTrades(this.strategy.id).subscribe((trade: any) => {
// Add the new trade to the top of the list
this.trades = [trade, ...this.trades.slice(0, 9)]; // Keep only the latest 10 trades
// Update performance metrics
this.updatePerformanceMetrics();
});
// Subscribe to strategy status updates
this.webSocketService.getStrategyUpdates().subscribe((update: any) => {
if (update.strategyId === this.strategy?.id) {
// Update strategy status if changed
if (update.status && this.strategy && this.strategy.status !== update.status) {
this.strategy.status = update.status;
}
// Update other fields if present
if (update.performance && this.strategy) {
this.strategy.performance = {
...this.strategy.performance,
...update.performance,
};
this.performance = {
...this.performance,
...update.performance,
};
}
}
});
console.log('WebSocket listeners for strategy updates initialized');
}
/**
* Update performance metrics when new trades come in
*/
private updatePerformanceMetrics(): void {
if (!this.strategy || this.trades.length === 0) {
return;
}
// Calculate basic metrics
const winningTrades = this.trades.filter(t => t.pnl > 0);
const losingTrades = this.trades.filter(t => t.pnl < 0);
const totalPnl = this.trades.reduce((sum, trade) => sum + trade.pnl, 0);
const winRate = winningTrades.length / this.trades.length;
// Update performance data
const currentPerformance = this.performance || {};
this.performance = {
...currentPerformance,
totalTrades: this.trades.length,
winRate: winRate,
winningTrades,
losingTrades,
totalReturn: (currentPerformance.totalReturn || 0) + totalPnl / 10000, // Approximate
};
// Update strategy performance as well
if (this.strategy && this.strategy.performance) {
this.strategy.performance = {
...this.strategy.performance,
totalTrades: this.trades.length,
winRate: winRate,
};
}
}
getStatusColor(status: string): string {
switch (status) {
case 'ACTIVE':
return 'green';
case 'PAUSED':
return 'orange';
case 'ERROR':
return 'red';
default:
return 'gray';
}
}
getSignalColor(action: string): string {
switch (action) {
case 'BUY':
return 'green';
case 'SELL':
return 'red';
default:
return 'gray';
}
}
/**
* Open the backtest dialog to run a backtest for this strategy
*/
openBacktestDialog(): void {
if (!this.strategy) {
return;
}
const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px',
data: this.strategy,
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Store the backtest result for visualization
this.backtestResult = result;
}
});
}
/**
* Open the strategy edit dialog
*/
openEditDialog(): void {
if (!this.strategy) {
return;
}
const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px',
data: this.strategy,
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Refresh strategy data after edit
this.loadStrategyData();
}
});
}
/**
* Start the strategy
*/
activateStrategy(): void {
if (!this.strategy) {
return;
}
this.strategyService.startStrategy(this.strategy.id).subscribe({
next: response => {
if (response.success) {
this.strategy!.status = 'ACTIVE';
}
},
error: error => {
console.error('Error starting strategy:', error);
},
});
}
/**
* Pause the strategy
*/
pauseStrategy(): void {
if (!this.strategy) {
return;
}
this.strategyService.pauseStrategy(this.strategy.id).subscribe({
next: response => {
if (response.success) {
this.strategy!.status = 'PAUSED';
}
},
error: error => {
console.error('Error pausing strategy:', error);
},
});
}
/**
* Stop the strategy
*/
stopStrategy(): void {
if (!this.strategy) {
return;
}
this.strategyService.stopStrategy(this.strategy.id).subscribe({
next: response => {
if (response.success) {
this.strategy!.status = 'INACTIVE';
}
},
error: error => {
console.error('Error stopping strategy:', error);
},
});
}
// Methods to generate mock data
private generateMockSignals(): any[] {
if (!this.strategy) {
return [];
}
const signals = [];
const actions = ['BUY', 'SELL', 'HOLD'];
const now = new Date();
for (let i = 0; i < 10; i++) {
const symbol =
this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const action = actions[Math.floor(Math.random() * actions.length)];
signals.push({
id: `sig_${i}`,
symbol,
action,
confidence: 0.7 + Math.random() * 0.3,
price: 100 + Math.random() * 50,
timestamp: new Date(now.getTime() - i * 1000 * 60 * 30), // 30 min intervals
quantity: Math.floor(10 + Math.random() * 90),
});
}
return signals;
}
private generateMockTrades(): any[] {
if (!this.strategy) {
return [];
}
const trades = [];
const now = new Date();
for (let i = 0; i < 10; i++) {
const symbol =
this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const entryPrice = 100 + Math.random() * 50;
const exitPrice = entryPrice * (1 + (Math.random() * 0.1 - 0.05)); // -5% to +5%
const quantity = Math.floor(10 + Math.random() * 90);
const pnl = (exitPrice - entryPrice) * quantity;
trades.push({
id: `trade_${i}`,
symbol,
entryPrice,
entryTime: new Date(now.getTime() - (i + 5) * 1000 * 60 * 60), // Hourly intervals
exitPrice,
exitTime: new Date(now.getTime() - i * 1000 * 60 * 60),
quantity,
pnl,
pnlPercent: ((exitPrice - entryPrice) / entryPrice) * 100,
});
}
return trades;
}
}

View file

@ -0,0 +1,104 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export interface RiskThresholds {
maxPositionSize: number;
maxDailyLoss: number;
maxPortfolioRisk: number;
volatilityLimit: number;
}
export interface RiskEvaluation {
symbol: string;
positionValue: number;
positionRisk: number;
violations: string[];
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
}
export interface MarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
@Injectable({
providedIn: 'root',
})
export class ApiService {
private readonly baseUrls = {
riskGuardian: 'http://localhost:3002',
strategyOrchestrator: 'http://localhost:3003',
marketDataGateway: 'http://localhost:3001',
};
constructor(private http: HttpClient) {}
// Risk Guardian API
getRiskThresholds(): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.get<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`
);
}
updateRiskThresholds(
thresholds: RiskThresholds
): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.put<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`,
thresholds
);
}
evaluateRisk(params: {
symbol: string;
quantity: number;
price: number;
portfolioValue: number;
}): Observable<{ success: boolean; data: RiskEvaluation }> {
return this.http.post<{ success: boolean; data: RiskEvaluation }>(
`${this.baseUrls.riskGuardian}/api/risk/evaluate`,
params
);
}
getRiskHistory(): Observable<{ success: boolean; data: RiskEvaluation[] }> {
return this.http.get<{ success: boolean; data: RiskEvaluation[] }>(
`${this.baseUrls.riskGuardian}/api/risk/history`
);
}
// Strategy Orchestrator API
getStrategies(): Observable<{ success: boolean; data: any[] }> {
return this.http.get<{ success: boolean; data: any[] }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`
);
}
createStrategy(strategy: any): Observable<{ success: boolean; data: any }> {
return this.http.post<{ success: boolean; data: any }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`,
strategy
);
}
// Market Data Gateway API
getMarketData(
symbols: string[] = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN']
): Observable<{ success: boolean; data: MarketData[] }> {
const symbolsParam = symbols.join(',');
return this.http.get<{ success: boolean; data: MarketData[] }>(
`${this.baseUrls.marketDataGateway}/api/market-data?symbols=${symbolsParam}`
);
}
// Health checks
checkServiceHealth(
service: 'riskGuardian' | 'strategyOrchestrator' | 'marketDataGateway'
): Observable<any> {
return this.http.get(`${this.baseUrls[service]}/health`);
}
}

View file

@ -0,0 +1,191 @@
import { inject, Injectable, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Subscription } from 'rxjs';
import { RiskAlert, WebSocketService } from './websocket.service';
export interface Notification {
id: string;
type: 'info' | 'warning' | 'error' | 'success';
title: string;
message: string;
timestamp: Date;
read: boolean;
}
@Injectable({
providedIn: 'root',
})
export class NotificationService {
private snackBar = inject(MatSnackBar);
private webSocketService = inject(WebSocketService);
private riskAlertsSubscription?: Subscription;
// Reactive state
public notifications = signal<Notification[]>([]);
public unreadCount = signal<number>(0);
constructor() {
this.initializeRiskAlerts();
}
private initializeRiskAlerts() {
// Subscribe to risk alerts from WebSocket
this.riskAlertsSubscription = this.webSocketService.getRiskAlerts().subscribe({
next: (alert: RiskAlert) => {
this.handleRiskAlert(alert);
},
error: err => {
console.error('Risk alert subscription error:', err);
},
});
}
private handleRiskAlert(alert: RiskAlert) {
const notification: Notification = {
id: alert.id,
type: this.mapSeverityToType(alert.severity),
title: `Risk Alert: ${alert.symbol}`,
message: alert.message,
timestamp: new Date(alert.timestamp),
read: false,
};
this.addNotification(notification);
this.showSnackBarAlert(notification);
}
private mapSeverityToType(severity: string): 'info' | 'warning' | 'error' | 'success' {
switch (severity) {
case 'HIGH':
return 'error';
case 'MEDIUM':
return 'warning';
case 'LOW':
return 'info';
default:
return 'info';
}
}
private showSnackBarAlert(notification: Notification) {
const actionText = notification.type === 'error' ? 'Review' : 'Dismiss';
const duration = notification.type === 'error' ? 10000 : 5000;
this.snackBar.open(`${notification.title}: ${notification.message}`, actionText, {
duration,
panelClass: [`snack-${notification.type}`],
});
}
// Public methods
addNotification(notification: Notification) {
const current = this.notifications();
const updated = [notification, ...current].slice(0, 50); // Keep only latest 50
this.notifications.set(updated);
this.updateUnreadCount();
}
markAsRead(notificationId: string) {
const current = this.notifications();
const updated = current.map(n => (n.id === notificationId ? { ...n, read: true } : n));
this.notifications.set(updated);
this.updateUnreadCount();
}
markAllAsRead() {
const current = this.notifications();
const updated = current.map(n => ({ ...n, read: true }));
this.notifications.set(updated);
this.updateUnreadCount();
}
clearNotification(notificationId: string) {
const current = this.notifications();
const updated = current.filter(n => n.id !== notificationId);
this.notifications.set(updated);
this.updateUnreadCount();
}
clearAllNotifications() {
this.notifications.set([]);
this.unreadCount.set(0);
}
private updateUnreadCount() {
const unread = this.notifications().filter(n => !n.read).length;
this.unreadCount.set(unread);
}
// Manual notification methods
showSuccess(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'success',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 3000,
panelClass: ['snack-success'],
});
}
showError(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'error',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 8000,
panelClass: ['snack-error'],
});
}
showWarning(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'warning',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 5000,
panelClass: ['snack-warning'],
});
}
showInfo(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'info',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 4000,
panelClass: ['snack-info'],
});
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
ngOnDestroy() {
this.riskAlertsSubscription?.unsubscribe();
}
}

View file

@ -0,0 +1,238 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export interface TradingStrategy {
id: string;
name: string;
description: string;
status: 'ACTIVE' | 'INACTIVE' | 'PAUSED' | 'ERROR';
type: string;
symbols: string[];
parameters: Record<string, any>;
performance: {
totalTrades: number;
winRate: number;
totalReturn: number;
sharpeRatio: number;
maxDrawdown: number;
};
createdAt: Date;
updatedAt: Date;
}
export interface BacktestRequest {
strategyType: string;
strategyParams: Record<string, any>;
symbols: string[];
startDate: Date | string;
endDate: Date | string;
initialCapital: number;
dataResolution: '1m' | '5m' | '15m' | '30m' | '1h' | '4h' | '1d';
commission: number;
slippage: number;
mode: 'event' | 'vector';
}
export interface BacktestResult {
strategyId: string;
startDate: Date;
endDate: Date;
duration: number;
initialCapital: number;
finalCapital: number;
totalReturn: number;
annualizedReturn: number;
sharpeRatio: number;
maxDrawdown: number;
maxDrawdownDuration: number;
winRate: number;
totalTrades: number;
winningTrades: number;
losingTrades: number;
averageWinningTrade: number;
averageLosingTrade: number;
profitFactor: number;
dailyReturns: Array<{ date: Date; return: number }>;
trades: Array<{
symbol: string;
entryTime: Date;
entryPrice: number;
exitTime: Date;
exitPrice: number;
quantity: number;
pnl: number;
pnlPercent: number;
}>;
// Advanced metrics
sortinoRatio?: number;
calmarRatio?: number;
omegaRatio?: number;
cagr?: number;
volatility?: number;
ulcerIndex?: number;
}
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
@Injectable({
providedIn: 'root',
})
export class StrategyService {
private apiBaseUrl = '/api'; // Will be proxied to the correct backend endpoint
constructor(private http: HttpClient) {}
// Strategy Management
getStrategies(): Observable<ApiResponse<TradingStrategy[]>> {
return this.http.get<ApiResponse<TradingStrategy[]>>(`${this.apiBaseUrl}/strategies`);
}
getStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.get<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}`);
}
createStrategy(strategy: Partial<TradingStrategy>): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies`, strategy);
}
updateStrategy(
id: string,
updates: Partial<TradingStrategy>
): Observable<ApiResponse<TradingStrategy>> {
return this.http.put<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}`,
updates
);
}
startStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/start`,
{}
);
}
stopStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/stop`,
{}
);
}
pauseStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/pause`,
{}
);
}
// Backtest Management
getStrategyTypes(): Observable<ApiResponse<string[]>> {
return this.http.get<ApiResponse<string[]>>(`${this.apiBaseUrl}/strategy-types`);
}
getStrategyParameters(type: string): Observable<ApiResponse<Record<string, any>>> {
return this.http.get<ApiResponse<Record<string, any>>>(
`${this.apiBaseUrl}/strategy-parameters/${type}`
);
}
runBacktest(request: BacktestRequest): Observable<ApiResponse<BacktestResult>> {
return this.http.post<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest`, request);
}
getBacktestResult(id: string): Observable<ApiResponse<BacktestResult>> {
return this.http.get<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest/${id}`);
}
optimizeStrategy(
baseRequest: BacktestRequest,
parameterGrid: Record<string, any[]>
): Observable<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>> {
return this.http.post<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>>(
`${this.apiBaseUrl}/backtest/optimize`,
{ baseRequest, parameterGrid }
);
}
// Strategy Signals and Trades
getStrategySignals(strategyId: string): Observable<
ApiResponse<
Array<{
id: string;
strategyId: string;
symbol: string;
action: string;
price: number;
quantity: number;
timestamp: Date;
confidence: number;
metadata?: any;
}>
>
> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/signals`);
}
getStrategyTrades(strategyId: string): Observable<
ApiResponse<
Array<{
id: string;
strategyId: string;
symbol: string;
entryPrice: number;
entryTime: Date;
exitPrice: number;
exitTime: Date;
quantity: number;
pnl: number;
pnlPercent: number;
}>
>
> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/trades`);
}
// Helper methods for common transformations
formatBacktestRequest(formData: any): BacktestRequest {
// Handle date formatting and parameter conversion
return {
...formData,
startDate:
formData.startDate instanceof Date ? formData.startDate.toISOString() : formData.startDate,
endDate: formData.endDate instanceof Date ? formData.endDate.toISOString() : formData.endDate,
strategyParams: this.convertParameterTypes(formData.strategyType, formData.strategyParams),
};
}
private convertParameterTypes(
strategyType: string,
params: Record<string, any>
): Record<string, any> {
// Convert string parameters to correct types based on strategy requirements
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(params)) {
if (typeof value === 'string') {
// Try to convert to number if it looks like a number
if (!isNaN(Number(value))) {
result[key] = Number(value);
} else if (value.toLowerCase() === 'true') {
result[key] = true;
} else if (value.toLowerCase() === 'false') {
result[key] = false;
} else {
result[key] = value;
}
} else {
result[key] = value;
}
}
return result;
}
}

View file

@ -0,0 +1,215 @@
import { Injectable, signal } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
export interface WebSocketMessage {
type: string;
data: any;
timestamp: string;
}
export interface MarketDataUpdate {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
export interface RiskAlert {
id: string;
symbol: string;
alertType: 'POSITION_LIMIT' | 'DAILY_LOSS' | 'VOLATILITY' | 'PORTFOLIO_RISK';
message: string;
severity: 'LOW' | 'MEDIUM' | 'HIGH';
timestamp: string;
}
@Injectable({
providedIn: 'root',
})
export class WebSocketService {
private readonly WS_ENDPOINTS = {
marketData: 'ws://localhost:3001/ws',
riskGuardian: 'ws://localhost:3002/ws',
strategyOrchestrator: 'ws://localhost:3003/ws',
};
private connections = new Map<string, WebSocket>();
private messageSubjects = new Map<string, Subject<WebSocketMessage>>();
// Connection status signals
public isConnected = signal<boolean>(false);
public connectionStatus = signal<{ [key: string]: boolean }>({
marketData: false,
riskGuardian: false,
strategyOrchestrator: false,
});
constructor() {
this.initializeConnections();
}
private initializeConnections() {
// Initialize WebSocket connections for all services
Object.entries(this.WS_ENDPOINTS).forEach(([service, url]) => {
this.connect(service, url);
});
}
private connect(serviceName: string, url: string) {
try {
const ws = new WebSocket(url);
const messageSubject = new Subject<WebSocketMessage>();
ws.onopen = () => {
console.log(`Connected to ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, true);
};
ws.onmessage = event => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
messageSubject.next(message);
} catch (error) {
console.error(`Failed to parse WebSocket message from ${serviceName}:`, error);
}
};
ws.onclose = () => {
console.log(`Disconnected from ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, false);
// Attempt to reconnect after 5 seconds
setTimeout(() => {
this.connect(serviceName, url);
}, 5000);
};
ws.onerror = error => {
console.error(`WebSocket error for ${serviceName}:`, error);
this.updateConnectionStatus(serviceName, false);
};
this.connections.set(serviceName, ws);
this.messageSubjects.set(serviceName, messageSubject);
} catch (error) {
console.error(`Failed to connect to ${serviceName} WebSocket:`, error);
this.updateConnectionStatus(serviceName, false);
}
}
private updateConnectionStatus(serviceName: string, isConnected: boolean) {
const currentStatus = this.connectionStatus();
const newStatus = { ...currentStatus, [serviceName]: isConnected };
this.connectionStatus.set(newStatus);
// Update overall connection status
const overallConnected = Object.values(newStatus).some(status => status);
this.isConnected.set(overallConnected);
}
// Market Data Updates
getMarketDataUpdates(): Observable<MarketDataUpdate> {
const subject = this.messageSubjects.get('marketData');
if (!subject) {
throw new Error('Market data WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'market_data_update'),
map(message => message.data as MarketDataUpdate)
);
}
// Risk Alerts
getRiskAlerts(): Observable<RiskAlert> {
const subject = this.messageSubjects.get('riskGuardian');
if (!subject) {
throw new Error('Risk Guardian WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'risk_alert'),
map(message => message.data as RiskAlert)
);
}
// Strategy Updates
getStrategyUpdates(): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'strategy_update'),
map(message => message.data)
);
}
// Strategy Signals
getStrategySignals(strategyId?: string): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(
message =>
message.type === 'strategy_signal' &&
(!strategyId || message.data.strategyId === strategyId)
),
map(message => message.data)
);
}
// Strategy Trades
getStrategyTrades(strategyId?: string): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(
message =>
message.type === 'strategy_trade' &&
(!strategyId || message.data.strategyId === strategyId)
),
map(message => message.data)
);
}
// All strategy-related messages, useful for components that need all types
getAllStrategyMessages(): Observable<WebSocketMessage> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(filter(message => message.type.startsWith('strategy_')));
}
// Send messages
sendMessage(serviceName: string, message: any) {
const ws = this.connections.get(serviceName);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
} else {
console.warn(`Cannot send message to ${serviceName}: WebSocket not connected`);
}
}
// Cleanup
disconnect() {
this.connections.forEach((ws, _serviceName) => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
this.connections.clear();
this.messageSubjects.clear();
}
}

View file

View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Trading Dashboard</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="preconnect" href="https://fonts.gstatic.com">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View file

@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig).catch(err => console.error(err));

View file

@ -0,0 +1,89 @@
@import "tailwindcss";
/* Custom base styles */
html, body {
height: 100%;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f9fafb;
}
/* Angular Material integration styles */
.mat-sidenav-container {
background-color: transparent;
}
.mat-sidenav {
border-radius: 0;
}
.mat-toolbar {
background-color: white;
color: #374151;
}
.mat-mdc-button.w-full {
width: 100%;
text-align: left;
justify-content: flex-start;
}
.mat-mdc-card {
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
.mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #e5e7eb;
}
.mat-mdc-chip.chip-green {
background-color: #dcfce7 !important;
color: #166534 !important;
}
.mat-mdc-chip.chip-blue {
background-color: #dbeafe !important;
color: #1e40af !important;
}
.mat-mdc-table {
border-radius: 8px;
overflow: hidden;
}
.mat-mdc-header-row {
background-color: #f9fafb;
}
.mat-mdc-row:hover {
background-color: #f9fafb;
}
/* Dark mode overrides */
.dark .mat-toolbar {
background-color: #1f2937;
color: #f9fafb;
}
.dark .mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #4b5563;
}
.dark .mat-mdc-header-row {
background-color: #1f2937;
}
.dark .mat-mdc-row:hover {
background-color: #374151;
}
.dark .mat-mdc-card {
background-color: #1f2937;
color: #f9fafb;
}
.dark .mat-mdc-table {
background-color: #1f2937;
color: #f9fafb;
}

View file

View file

@ -0,0 +1,52 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}",
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
danger: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
},
},
},
},
plugins: [],
}

View file

@ -0,0 +1,11 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.spec.ts"]
}

View file

@ -0,0 +1,32 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../tsconfig.json",
"compileOnSave": false,
"compilerOptions": {
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View file

@ -0,0 +1,10 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jasmine"]
},
"include": ["src/**/*.ts"]
}

View file

@ -1,28 +1,33 @@
{
"name": "@stock-bot/web-api",
"name": "@stock-bot/data-service",
"version": "1.0.0",
"description": "REST API service for stock bot web application",
"description": "Combined data ingestion and historical data service",
"main": "dist/index.js",
"type": "module",
"scripts": {
"dev": "bun --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist --target node --external chromium-bidi --external electron --external playwright --external playwright-core",
"build": "bun build src/index.ts --outdir dist --target node",
"start": "bun dist/index.js",
"test": "bun test",
"clean": "rm -rf dist"
},
"dependencies": {
"@stock-bot/cache": "*",
"@stock-bot/config": "*",
"@stock-bot/stock-config": "*",
"@stock-bot/event-bus": "*",
"@stock-bot/http": "*",
"@stock-bot/logger": "*",
"@stock-bot/mongodb": "*",
"@stock-bot/postgres": "*",
"@stock-bot/mongodb-client": "*",
"@stock-bot/questdb-client": "*",
"@stock-bot/shutdown": "*",
"@stock-bot/handlers": "*",
"@stock-bot/di": "*",
"hono": "^4.0.0"
"@stock-bot/types": "*",
"bullmq": "^5.53.2",
"hono": "^4.0.0",
"p-limit": "^6.2.0",
"ws": "^8.0.0"
},
"devDependencies": {
"@types/ws": "^8.0.0",
"typescript": "^5.0.0"
}
}

View file

@ -0,0 +1,112 @@
/**
* Data Service - Combined live and historical data ingestion with queue-based architecture
*/
import { Hono } from 'hono';
import { Browser } from '@stock-bot/browser';
import { loadEnvVariables } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
import { Shutdown } from '@stock-bot/shutdown';
import { initializeIBResources } from './providers/ib.tasks';
import { initializeProxyResources } from './providers/proxy.tasks';
import { queueManager } from './services/queue.service';
import { initializeBatchCache } from './utils/batch-helpers';
import { healthRoutes, marketDataRoutes, proxyRoutes, queueRoutes, testRoutes } from './routes';
// Load environment variables
loadEnvVariables();
const app = new Hono();
const logger = getLogger('data-service');
const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002');
let server: any = null;
// Initialize shutdown manager with 15 second timeout
const shutdown = Shutdown.getInstance({ timeout: 15000 });
// Register all routes
app.route('', healthRoutes);
app.route('', queueRoutes);
app.route('', marketDataRoutes);
app.route('', proxyRoutes);
app.route('', testRoutes);
// Initialize services
async function initializeServices() {
logger.info('Initializing data service...');
try {
// Initialize browser resources
logger.info('Starting browser resources initialization...');
await Browser.initialize();
logger.info('Browser resources initialized');
// Initialize batch cache FIRST - before queue service
logger.info('Starting batch cache initialization...');
await initializeBatchCache();
logger.info('Batch cache initialized');
// Initialize proxy cache - before queue service
logger.info('Starting proxy cache initialization...');
await initializeProxyResources(true); // Wait for cache during startup
logger.info('Proxy cache initialized');
// Initialize proxy cache - before queue service
logger.info('Starting proxy cache initialization...');
await initializeIBResources(true); // Wait for cache during startup
logger.info('Proxy cache initialized');
// Initialize queue service (Redis connections should be ready now)
logger.info('Starting queue service initialization...');
await queueManager.initialize();
logger.info('Queue service initialized');
logger.info('All services initialized successfully');
} catch (error) {
logger.error('Failed to initialize services', { error });
throw error;
}
}
// Start server
async function startServer() {
await initializeServices();
// Start the HTTP server using Bun's native serve
server = Bun.serve({
port: PORT,
fetch: app.fetch,
development: process.env.NODE_ENV === 'development',
});
logger.info(`Data Service started on port ${PORT}`);
}
// Register shutdown handlers
shutdown.onShutdown(async () => {
if (server) {
logger.info('Stopping HTTP server...');
try {
server.stop();
logger.info('HTTP server stopped successfully');
} catch (error) {
logger.error('Error stopping HTTP server', { error });
}
}
});
shutdown.onShutdown(async () => {
logger.info('Shutting down queue manager...');
try {
await queueManager.shutdown();
logger.info('Queue manager shut down successfully');
} catch (error) {
logger.error('Error shutting down queue manager', { error });
throw error; // Re-throw to mark shutdown as failed
}
});
// Start the application
startServer().catch(error => {
logger.error('Failed to start server', { error });
process.exit(1);
});
logger.info('Data service startup initiated with graceful shutdown handlers');

View file

@ -0,0 +1,32 @@
import { getLogger } from '@stock-bot/logger';
import { ProviderConfig } from '../services/provider-registry.service';
const logger = getLogger('ib-provider');
export const ibProvider: ProviderConfig = {
name: 'ib',
operations: {
'ib-symbol-summary': async () => {
const { ibTasks } = await import('./ib.tasks');
logger.info('Fetching symbol summary from IB');
const total = await ibTasks.fetchSymbolSummary();
logger.info('Fetched symbol summary from IB', {
count: total,
});
return total;
},
},
scheduledJobs: [
{
type: 'ib-symbol-summary',
operation: 'ib-symbol-summary',
payload: {},
// should remove and just run at the same time so app restarts dont keeping adding same jobs
cronPattern: '*/2 * * * *',
priority: 5,
immediately: true, // Don't run immediately during startup to avoid conflicts
description: 'Fetch and validate proxy list from sources',
},
],
};

View file

@ -0,0 +1,152 @@
import { Browser } from '@stock-bot/browser';
import { getLogger } from '@stock-bot/logger';
// Shared instances (module-scoped, not global)
let isInitialized = false; // Track if resources are initialized
let logger: ReturnType<typeof getLogger>;
// let cache: CacheProvider;
export async function initializeIBResources(waitForCache = false): Promise<void> {
// Skip if already initialized
if (isInitialized) {
return;
}
logger = getLogger('proxy-tasks');
// cache = createCache({
// keyPrefix: 'proxy:',
// ttl: PROXY_CONFIG.CACHE_TTL,
// enableMetrics: true,
// });
// httpClient = new HttpClient({ timeout: 15000 }, logger);
// if (waitForCache) {
// // logger.info('Initializing proxy cache...');
// // await cache.waitForReady(10000);
// // logger.info('Proxy cache initialized successfully');
// logger.info('Proxy tasks initialized');
// } else {
// logger.info('Proxy tasks initialized (fallback mode)');
// }
isInitialized = true;
}
export async function fetchSymbolSummary(): Promise<number> {
try {
await Browser.initialize({ headless: true, timeout: 10000, blockResources: false });
logger.info('✅ Browser initialized');
const { page, contextId } = await Browser.createPageWithProxy(
'https://www.interactivebrokers.com/en/trading/products-exchanges.php#/',
'http://doimvbnb-US-rotate:w5fpiwrb9895@p.webshare.io:80'
);
logger.info('✅ Page created with proxy');
let summaryData: any = null; // Initialize summaryData to store API response
let eventCount = 0;
page.onNetworkEvent(event => {
if (event.url.includes('/webrest/search/product-types/summary')) {
console.log(`🎯 Found summary API call: ${event.type} ${event.url}`);
if (event.type === 'response' && event.responseData) {
console.log(`📊 Summary API Response Data: ${event.responseData}`);
try {
summaryData = JSON.parse(event.responseData) as any;
const totalCount = summaryData[0].totalCount;
console.log('📊 Summary API Response:', JSON.stringify(summaryData, null, 2));
console.log(`🔢 Total symbols found: ${totalCount || 'Unknown'}`);
} catch (e) {
console.log('📊 Raw Summary Response:', event.responseData);
}
}
}
eventCount++;
logger.info(`📡 Event ${eventCount}: ${event.type} ${event.url}`);
});
logger.info('⏳ Waiting for page load...');
await page.waitForLoadState('domcontentloaded', { timeout: 20000 });
logger.info('✅ Page loaded');
// RIGHT HERE - Interact with the page to find Stocks checkbox and Apply button
logger.info('🔍 Looking for Products tab...');
// Wait for the page to fully load
await page.waitForTimeout(20000);
// First, click on the Products tab
const productsTab = page.locator('#productSearchTab[role="tab"][href="#products"]');
await productsTab.waitFor({ timeout: 20000 });
logger.info('✅ Found Products tab');
logger.info('🖱️ Clicking Products tab...');
await productsTab.click();
logger.info('✅ Products tab clicked');
// Wait for the tab content to load
await page.waitForTimeout(5000);
// Click on the Asset Classes accordion to expand it
logger.info('🔍 Looking for Asset Classes accordion...');
const assetClassesAccordion = page.locator(
'#products .accordion-item #acc-products .accordion_btn:has-text("Asset Classes")'
);
await assetClassesAccordion.waitFor({ timeout: 10000 });
logger.info('✅ Found Asset Classes accordion');
logger.info('🖱️ Clicking Asset Classes accordion...');
await assetClassesAccordion.click();
logger.info('✅ Asset Classes accordion clicked');
// Wait for the accordion content to expand
await page.waitForTimeout(2000);
logger.info('🔍 Looking for Stocks checkbox...');
// Find the span with class "fs-7 checkbox-text" and inner text containing "Stocks"
const stocksSpan = page.locator('span.fs-7.checkbox-text:has-text("Stocks")');
await stocksSpan.waitFor({ timeout: 10000 });
logger.info('✅ Found Stocks span');
// Find the checkbox by looking in the same parent container
const parentContainer = stocksSpan.locator('..');
const checkbox = parentContainer.locator('input[type="checkbox"]');
if ((await checkbox.count()) > 0) {
logger.info('📋 Clicking Stocks checkbox...');
await checkbox.first().check();
logger.info('✅ Stocks checkbox checked');
} else {
logger.info('⚠️ Could not find checkbox near Stocks text');
}
// Wait a moment for any UI updates
await page.waitForTimeout(1000);
// Find and click the nearest Apply button
logger.info('🔍 Looking for Apply button...');
const applyButton = page.locator(
'button:has-text("Apply"), input[type="submit"][value*="Apply"], input[type="button"][value*="Apply"]'
);
if ((await applyButton.count()) > 0) {
logger.info('🎯 Clicking Apply button...');
await applyButton.first().click();
logger.info('✅ Apply button clicked');
// Wait for any network requests triggered by the Apply button
await page.waitForTimeout(2000);
} else {
logger.info('⚠️ Could not find Apply button');
}
return 0;
} catch (error) {
logger.error('Failed to fetch IB symbol summary', { error });
return 0;
}
}
// Optional: Export a convenience object that groups related tasks
export const ibTasks = {
fetchSymbolSummary,
};

View file

@ -0,0 +1,98 @@
import { ProxyInfo } from 'libs/http/src/types';
import { getLogger } from '@stock-bot/logger';
import { ProviderConfig } from '../services/provider-registry.service';
// Create logger for this provider
const logger = getLogger('proxy-provider');
// This will run at the same time each day as when the app started
const getEvery24HourCron = (): string => {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
return `${minutes} ${hours} * * *`; // Every day at startup time
};
export const proxyProvider: ProviderConfig = {
name: 'proxy-provider',
operations: {
'fetch-and-check': async (_payload: { sources?: string[] }) => {
const { proxyService } = await import('./proxy.tasks');
const { queueManager } = await import('../services/queue.service');
const { processItems } = await import('../utils/batch-helpers');
const proxies = await proxyService.fetchProxiesFromSources();
if (proxies.length === 0) {
return { proxiesFetched: 0, jobsCreated: 0 };
}
// Use generic function with routing parameters
const result = await processItems(
proxies,
(proxy, index) => ({
proxy,
index,
source: 'batch-processing',
}),
queueManager,
{
totalDelayHours: 12, //parseFloat(process.env.PROXY_VALIDATION_HOURS || '1'),
batchSize: parseInt(process.env.PROXY_BATCH_SIZE || '200'),
useBatching: process.env.PROXY_DIRECT_MODE !== 'true',
provider: 'proxy-provider',
operation: 'check-proxy',
}
);
return result;
},
'process-batch-items': async (payload: any) => {
// Process a batch using the simplified batch helpers
const { processBatchJob } = await import('../utils/batch-helpers');
const { queueManager } = await import('../services/queue.service');
return await processBatchJob(payload, queueManager);
},
'check-proxy': async (payload: {
proxy: ProxyInfo;
source?: string;
batchIndex?: number;
itemIndex?: number;
total?: number;
}) => {
const { checkProxy } = await import('./proxy.tasks');
try {
const result = await checkProxy(payload.proxy);
logger.debug('Proxy validated', {
proxy: `${payload.proxy.host}:${payload.proxy.port}`,
isWorking: result.isWorking,
responseTime: result.responseTime,
});
return { result, proxy: payload.proxy };
} catch (error) {
logger.warn('Proxy validation failed', {
proxy: `${payload.proxy.host}:${payload.proxy.port}`,
error: error instanceof Error ? error.message : String(error),
});
return { result: { isWorking: false, error: String(error) }, proxy: payload.proxy };
}
},
},
scheduledJobs: [
// {
// type: 'proxy-maintenance',
// operation: 'fetch-and-check',
// payload: {},
// // should remove and just run at the same time so app restarts dont keeping adding same jobs
// cronPattern: getEvery24HourCron(),
// priority: 5,
// immediately: true, // Don't run immediately during startup to avoid conflicts
// description: 'Fetch and validate proxy list from sources',
// },
],
};

View file

@ -0,0 +1,574 @@
import { createCache, type CacheProvider } from '@stock-bot/cache';
import { HttpClient, ProxyInfo } from '@stock-bot/http';
import { getLogger } from '@stock-bot/logger';
// Type definitions
export interface ProxySource {
id: string;
url: string;
protocol: string;
working?: number; // Optional, used for stats
total?: number; // Optional, used for stats
percentWorking?: number; // Optional, used for stats
lastChecked?: Date; // Optional, used for stats
}
// Shared configuration and utilities
const PROXY_CONFIG = {
CACHE_KEY: 'active',
CACHE_STATS_KEY: 'stats',
CACHE_TTL: 86400, // 24 hours
CHECK_TIMEOUT: 7000,
CHECK_IP: '99.246.102.205',
CHECK_URL: 'https://proxy-detection.stare.gg/?api_key=bd406bf53ddc6abe1d9de5907830a955',
PROXY_SOURCES: [
{
id: 'prxchk',
url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt',
protocol: 'http',
},
{
id: 'casals',
url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/http',
protocol: 'http',
},
{
id: 'sunny9577',
url: 'https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/proxies.txt',
protocol: 'http',
},
{
id: 'themiralay',
url: 'https://raw.githubusercontent.com/themiralay/Proxy-List-World/refs/heads/master/data.txt',
protocol: 'http',
},
{
id: 'casa-ls',
url: 'https://raw.githubusercontent.com/casa-ls/proxy-list/refs/heads/main/http',
protocol: 'http',
},
{
id: 'databay',
url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/http.txt',
protocol: 'http',
},
{
id: 'speedx',
url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt',
protocol: 'http',
},
{
id: 'monosans',
url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt',
protocol: 'http',
},
{
id: 'murong',
url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt',
protocol: 'http',
},
{
id: 'vakhov-fresh',
url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/http.txt',
protocol: 'http',
},
{
id: 'kangproxy',
url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/http/http.txt',
protocol: 'http',
},
{
id: 'gfpcom',
url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/http.txt',
protocol: 'http',
},
{
id: 'dpangestuw',
url: 'https://raw.githubusercontent.com/dpangestuw/Free-Proxy/refs/heads/main/http_proxies.txt',
protocol: 'http',
},
{
id: 'gitrecon',
url: 'https://raw.githubusercontent.com/gitrecon1455/fresh-proxy-list/refs/heads/main/proxylist.txt',
protocol: 'http',
},
{
id: 'vakhov-master',
url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/http.txt',
protocol: 'http',
},
{
id: 'breaking-tech',
url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/http.txt',
protocol: 'http',
},
{
id: 'ercindedeoglu',
url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt',
protocol: 'http',
},
{
id: 'tuanminpay',
url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/http.txt',
protocol: 'http',
},
{
id: 'r00tee-https',
url: 'https://raw.githubusercontent.com/r00tee/Proxy-List/refs/heads/main/Https.txt',
protocol: 'https',
},
{
id: 'ercindedeoglu-https',
url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt',
protocol: 'https',
},
{
id: 'vakhov-fresh-https',
url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/https.txt',
protocol: 'https',
},
{
id: 'databay-https',
url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/https.txt',
protocol: 'https',
},
{
id: 'kangproxy-https',
url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/https/https.txt',
protocol: 'https',
},
{
id: 'zloi-user-https',
url: 'https://raw.githubusercontent.com/zloi-user/hideip.me/refs/heads/master/https.txt',
protocol: 'https',
},
{
id: 'gfpcom-https',
url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/https.txt',
protocol: 'https',
},
],
};
// Shared instances (module-scoped, not global)
let isInitialized = false; // Track if resources are initialized
let logger: ReturnType<typeof getLogger>;
let cache: CacheProvider;
let httpClient: HttpClient;
let proxyStats: ProxySource[] = PROXY_CONFIG.PROXY_SOURCES.map(source => ({
id: source.id,
total: 0,
working: 0,
lastChecked: new Date(),
protocol: source.protocol,
url: source.url,
}));
/**
* Initialize proxy resources (cache and shared dependencies)
* This should be called before any proxy operations
* @param waitForCache - Whether to wait for cache readiness (default: false for fallback mode)
*/
export async function initializeProxyResources(waitForCache = false): Promise<void> {
// Skip if already initialized
if (isInitialized) {
return;
}
logger = getLogger('proxy-tasks');
cache = createCache({
keyPrefix: 'proxy:',
ttl: PROXY_CONFIG.CACHE_TTL,
enableMetrics: true,
});
httpClient = new HttpClient({ timeout: 10000 }, logger);
if (waitForCache) {
logger.info('Initializing proxy cache...');
await cache.waitForReady(10000);
logger.info('Proxy cache initialized successfully');
logger.info('Proxy tasks initialized');
} else {
logger.info('Proxy tasks initialized (fallback mode)');
}
isInitialized = true;
}
// make a function that takes in source id and a boolean success and updates the proxyStats array
async function updateProxyStats(sourceId: string, success: boolean) {
const source = proxyStats.find(s => s.id === sourceId);
if (source !== undefined) {
if (typeof source.working !== 'number') {
source.working = 0;
}
if (typeof source.total !== 'number') {
source.total = 0;
}
source.total += 1;
if (success) {
source.working += 1;
}
source.percentWorking = (source.working / source.total) * 100;
source.lastChecked = new Date();
await cache.set(`${PROXY_CONFIG.CACHE_STATS_KEY}:${source.id}`, source, PROXY_CONFIG.CACHE_TTL);
return source;
} else {
logger.warn(`Unknown proxy source: ${sourceId}`);
}
}
// make a function that resets proxyStats
async function resetProxyStats(): Promise<void> {
proxyStats = PROXY_CONFIG.PROXY_SOURCES.map(source => ({
id: source.id,
total: 0,
working: 0,
lastChecked: new Date(),
protocol: source.protocol,
url: source.url,
}));
for (const source of proxyStats) {
await cache.set(`${PROXY_CONFIG.CACHE_STATS_KEY}:${source.id}`, source, PROXY_CONFIG.CACHE_TTL);
}
return Promise.resolve();
}
/**
* Update proxy data in cache with working/total stats and average response time
* @param proxy - The proxy to update
* @param isWorking - Whether the proxy is currently working
*/
async function updateProxyInCache(proxy: ProxyInfo, isWorking: boolean): Promise<void> {
const cacheKey = `${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`;
try {
const existing: any = await cache.get(cacheKey);
// For failed proxies, only update if they already exist
if (!isWorking && !existing) {
logger.debug('Proxy not in cache, skipping failed update', {
proxy: `${proxy.host}:${proxy.port}`,
});
return;
}
// Calculate new average response time if we have a response time
let newAverageResponseTime = existing?.averageResponseTime;
if (proxy.responseTime !== undefined) {
const existingAvg = existing?.averageResponseTime || 0;
const existingTotal = existing?.total || 0;
// Calculate weighted average: (existing_avg * existing_count + new_response) / (existing_count + 1)
newAverageResponseTime =
existingTotal > 0
? (existingAvg * existingTotal + proxy.responseTime) / (existingTotal + 1)
: proxy.responseTime;
}
// Build updated proxy data
const updated = {
...existing,
...proxy, // Keep latest proxy info
total: (existing?.total || 0) + 1,
working: isWorking ? (existing?.working || 0) + 1 : existing?.working || 0,
isWorking,
lastChecked: new Date(),
// Add firstSeen only for new entries
...(existing ? {} : { firstSeen: new Date() }),
// Update average response time if we calculated a new one
...(newAverageResponseTime !== undefined
? { averageResponseTime: newAverageResponseTime }
: {}),
};
// Calculate success rate
updated.successRate = updated.total > 0 ? (updated.working / updated.total) * 100 : 0;
// Save to cache: reset TTL for working proxies, keep existing TTL for failed ones
const cacheOptions = isWorking ? PROXY_CONFIG.CACHE_TTL : undefined;
await cache.set(cacheKey, updated, cacheOptions);
logger.debug(`Updated ${isWorking ? 'working' : 'failed'} proxy in cache`, {
proxy: `${proxy.host}:${proxy.port}`,
working: updated.working,
total: updated.total,
successRate: updated.successRate.toFixed(1) + '%',
avgResponseTime: updated.averageResponseTime
? `${updated.averageResponseTime.toFixed(0)}ms`
: 'N/A',
});
} catch (error) {
logger.error('Failed to update proxy in cache', {
proxy: `${proxy.host}:${proxy.port}`,
error: error instanceof Error ? error.message : String(error),
});
}
}
// Individual task functions
export async function queueProxyFetch(): Promise<string> {
const { queueManager } = await import('../services/queue.service');
const job = await queueManager.addJob({
type: 'proxy-fetch',
provider: 'proxy-service',
operation: 'fetch-and-check',
payload: {},
priority: 5,
});
const jobId = job.id || 'unknown';
logger.info('Proxy fetch job queued', { jobId });
return jobId;
}
export async function queueProxyCheck(proxies: ProxyInfo[]): Promise<string> {
const { queueManager } = await import('../services/queue.service');
const job = await queueManager.addJob({
type: 'proxy-check',
provider: 'proxy-service',
operation: 'check-specific',
payload: { proxies },
priority: 3,
});
const jobId = job.id || 'unknown';
logger.info('Proxy check job queued', { jobId, count: proxies.length });
return jobId;
}
export async function fetchProxiesFromSources(): Promise<ProxyInfo[]> {
await resetProxyStats();
const fetchPromises = PROXY_CONFIG.PROXY_SOURCES.map(source => fetchProxiesFromSource(source));
const results = await Promise.all(fetchPromises);
let allProxies: ProxyInfo[] = results.flat();
allProxies = removeDuplicateProxies(allProxies);
return allProxies;
}
export async function fetchProxiesFromSource(source: ProxySource): Promise<ProxyInfo[]> {
const allProxies: ProxyInfo[] = [];
try {
logger.info(`Fetching proxies from ${source.url}`);
const response = await httpClient.get(source.url, {
timeout: 10000,
});
if (response.status !== 200) {
logger.warn(`Failed to fetch from ${source.url}: ${response.status}`);
return [];
}
const text = response.data;
const lines = text.split('\n').filter((line: string) => line.trim());
for (const line of lines) {
let trimmed = line.trim();
trimmed = cleanProxyUrl(trimmed);
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
// Parse formats like "host:port" or "host:port:user:pass"
const parts = trimmed.split(':');
if (parts.length >= 2) {
const proxy: ProxyInfo = {
source: source.id,
protocol: source.protocol as 'http' | 'https' | 'socks4' | 'socks5',
host: parts[0],
port: parseInt(parts[1]),
};
if (!isNaN(proxy.port) && proxy.host) {
allProxies.push(proxy);
}
}
}
logger.info(`Parsed ${allProxies.length} proxies from ${source.url}`);
} catch (error) {
logger.error(`Error fetching proxies from ${source.url}`, error);
return [];
}
return allProxies;
}
/**
* Check if a proxy is working
*/
export async function checkProxy(proxy: ProxyInfo): Promise<ProxyInfo> {
let success = false;
logger.debug(`Checking Proxy:`, {
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
});
try {
// Test the proxy
const response = await httpClient.get(PROXY_CONFIG.CHECK_URL, {
proxy,
timeout: PROXY_CONFIG.CHECK_TIMEOUT,
});
const isWorking = response.status >= 200 && response.status < 300;
const result: ProxyInfo = {
...proxy,
isWorking,
lastChecked: new Date(),
responseTime: response.responseTime,
};
if (isWorking && !JSON.stringify(response.data).includes(PROXY_CONFIG.CHECK_IP)) {
success = true;
await updateProxyInCache(result, true);
} else {
await updateProxyInCache(result, false);
}
if (proxy.source) {
await updateProxyStats(proxy.source, success);
}
logger.debug('Proxy check completed', {
host: proxy.host,
port: proxy.port,
isWorking,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const result: ProxyInfo = {
...proxy,
isWorking: false,
error: errorMessage,
lastChecked: new Date(),
};
// Update cache for failed proxy (increment total, don't update TTL)
await updateProxyInCache(result, false);
if (proxy.source) {
await updateProxyStats(proxy.source, success);
}
logger.debug('Proxy check failed', {
host: proxy.host,
port: proxy.port,
error: errorMessage,
});
return result;
}
}
/**
* Get a random active proxy from the cache
* @param protocol - Optional protocol filter ('http' | 'https' | 'socks4' | 'socks5')
* @param minSuccessRate - Minimum success rate percentage (default: 50)
* @returns A random working proxy or null if none found
*/
export async function getRandomActiveProxy(
protocol?: 'http' | 'https' | 'socks4' | 'socks5',
minSuccessRate: number = 50
): Promise<ProxyInfo | null> {
try {
// Get all active proxy keys from cache
const pattern = protocol
? `${PROXY_CONFIG.CACHE_KEY}:${protocol}://*`
: `${PROXY_CONFIG.CACHE_KEY}:*`;
const keys = await cache.keys(pattern);
if (keys.length === 0) {
logger.debug('No active proxies found in cache', { pattern });
return null;
}
// Shuffle the keys for randomness
const shuffledKeys = keys.sort(() => Math.random() - 0.5);
// Find a working proxy that meets the criteria
for (const key of shuffledKeys) {
try {
const proxyData: ProxyInfo | null = await cache.get(key);
if (
proxyData &&
proxyData.isWorking &&
(!proxyData.successRate || proxyData.successRate >= minSuccessRate)
) {
logger.debug('Random active proxy selected', {
proxy: `${proxyData.host}:${proxyData.port}`,
protocol: proxyData.protocol,
successRate: proxyData.successRate?.toFixed(1) + '%',
avgResponseTime: proxyData.averageResponseTime
? `${proxyData.averageResponseTime.toFixed(0)}ms`
: 'N/A',
});
return proxyData;
}
} catch (error) {
logger.debug('Error reading proxy from cache', { key, error: (error as Error).message });
continue;
}
}
logger.debug('No working proxies found meeting criteria', {
protocol,
minSuccessRate,
keysChecked: shuffledKeys.length,
});
return null;
} catch (error) {
logger.error('Error getting random active proxy', {
error: error instanceof Error ? error.message : String(error),
protocol,
minSuccessRate,
});
return null;
}
}
// Utility functions
function cleanProxyUrl(url: string): string {
return url
.replace(/^https?:\/\//, '')
.replace(/^0+/, '')
.replace(/:0+(\d)/g, ':$1');
}
function removeDuplicateProxies(proxies: ProxyInfo[]): ProxyInfo[] {
const seen = new Set<string>();
const unique: ProxyInfo[] = [];
for (const proxy of proxies) {
const key = `${proxy.protocol}://${proxy.host}:${proxy.port}`;
if (!seen.has(key)) {
seen.add(key);
unique.push(proxy);
}
}
return unique;
}
// Optional: Export a convenience object that groups related tasks
export const proxyTasks = {
queueProxyFetch,
queueProxyCheck,
fetchProxiesFromSources,
fetchProxiesFromSource,
checkProxy,
};
// Export singleton instance for backward compatibility (optional)
// Remove this if you want to fully move to the task-based approach
export const proxyService = proxyTasks;

View file

@ -0,0 +1,182 @@
import { getLogger } from '@stock-bot/logger';
import { ProviderConfig } from '../services/provider-registry.service';
const logger = getLogger('qm-provider');
export const qmProvider: ProviderConfig = {
name: 'qm',
operations: {
'live-data': async (payload: { symbol: string; fields?: string[] }) => {
logger.info('Fetching live data from qm', { symbol: payload.symbol });
// Simulate qm API call
const mockData = {
symbol: payload.symbol,
price: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000),
change: (Math.random() - 0.5) * 20,
changePercent: (Math.random() - 0.5) * 5,
timestamp: new Date().toISOString(),
source: 'qm',
fields: payload.fields || ['price', 'volume', 'change'],
};
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200));
return mockData;
},
'historical-data': async (payload: {
symbol: string;
from: Date;
to: Date;
interval?: string;
fields?: string[];
}) => {
logger.info('Fetching historical data from qm', {
symbol: payload.symbol,
from: payload.from,
to: payload.to,
interval: payload.interval || '1d',
});
// Generate mock historical data
const days = Math.ceil(
(payload.to.getTime() - payload.from.getTime()) / (1000 * 60 * 60 * 24)
);
const data = [];
for (let i = 0; i < Math.min(days, 100); i++) {
const date = new Date(payload.from.getTime() + i * 24 * 60 * 60 * 1000);
data.push({
date: date.toISOString().split('T')[0],
open: Math.random() * 1000 + 100,
high: Math.random() * 1000 + 100,
low: Math.random() * 1000 + 100,
close: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000),
source: 'qm',
});
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300));
return {
symbol: payload.symbol,
interval: payload.interval || '1d',
data,
source: 'qm',
totalRecords: data.length,
};
},
'batch-quotes': async (payload: { symbols: string[]; fields?: string[] }) => {
logger.info('Fetching batch quotes from qm', {
symbols: payload.symbols,
count: payload.symbols.length,
});
const quotes = payload.symbols.map(symbol => ({
symbol,
price: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000),
change: (Math.random() - 0.5) * 20,
timestamp: new Date().toISOString(),
source: 'qm',
}));
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 200));
return {
quotes,
source: 'qm',
timestamp: new Date().toISOString(),
totalSymbols: payload.symbols.length,
};
},
'company-profile': async (payload: { symbol: string }) => {
logger.info('Fetching company profile from qm', { symbol: payload.symbol });
// Simulate company profile data
const profile = {
symbol: payload.symbol,
companyName: `${payload.symbol} Corporation`,
sector: 'Technology',
industry: 'Software',
description: `${payload.symbol} is a leading technology company.`,
marketCap: Math.floor(Math.random() * 1000000000000),
employees: Math.floor(Math.random() * 100000),
website: `https://www.${payload.symbol.toLowerCase()}.com`,
source: 'qm',
};
await new Promise(resolve => setTimeout(resolve, 150 + Math.random() * 100));
return profile;
},
'options-chain': async (payload: { symbol: string; expiration?: string }) => {
logger.info('Fetching options chain from qm', {
symbol: payload.symbol,
expiration: payload.expiration,
});
// Generate mock options data
const strikes = Array.from({ length: 20 }, (_, i) => 100 + i * 5);
const calls = strikes.map(strike => ({
strike,
bid: Math.random() * 10,
ask: Math.random() * 10 + 0.5,
volume: Math.floor(Math.random() * 1000),
openInterest: Math.floor(Math.random() * 5000),
}));
const puts = strikes.map(strike => ({
strike,
bid: Math.random() * 10,
ask: Math.random() * 10 + 0.5,
volume: Math.floor(Math.random() * 1000),
openInterest: Math.floor(Math.random() * 5000),
}));
await new Promise(resolve => setTimeout(resolve, 400 + Math.random() * 300));
return {
symbol: payload.symbol,
expiration:
payload.expiration ||
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
calls,
puts,
source: 'qm',
};
},
},
scheduledJobs: [
// {
// type: 'qm-premium-refresh',
// operation: 'batch-quotes',
// payload: { symbols: ['AAPL', 'GOOGL', 'MSFT'] },
// cronPattern: '*/2 * * * *', // Every 2 minutes
// priority: 7,
// description: 'Refresh premium quotes with detailed market data'
// },
// {
// type: 'qm-options-update',
// operation: 'options-chain',
// payload: { symbol: 'SPY' },
// cronPattern: '*/10 * * * *', // Every 10 minutes
// priority: 5,
// description: 'Update options chain data for SPY ETF'
// },
// {
// type: 'qm-profiles',
// operation: 'company-profile',
// payload: { symbol: 'AAPL' },
// cronPattern: '0 9 * * 1-5', // Weekdays at 9 AM
// priority: 3,
// description: 'Update company profile data'
// }
],
};

View file

@ -0,0 +1,254 @@
import { getLogger } from '@stock-bot/logger';
import { ProviderConfig } from '../services/provider-registry.service';
const logger = getLogger('yahoo-provider');
export const yahooProvider: ProviderConfig = {
name: 'yahoo-finance',
operations: {
'live-data': async (payload: { symbol: string; modules?: string[] }) => {
logger.info('Fetching live data from Yahoo Finance', { symbol: payload.symbol });
// Simulate Yahoo Finance API call
const mockData = {
symbol: payload.symbol,
regularMarketPrice: Math.random() * 1000 + 100,
regularMarketVolume: Math.floor(Math.random() * 1000000),
regularMarketChange: (Math.random() - 0.5) * 20,
regularMarketChangePercent: (Math.random() - 0.5) * 5,
preMarketPrice: Math.random() * 1000 + 100,
postMarketPrice: Math.random() * 1000 + 100,
marketCap: Math.floor(Math.random() * 1000000000000),
peRatio: Math.random() * 50 + 5,
dividendYield: Math.random() * 0.1,
fiftyTwoWeekHigh: Math.random() * 1200 + 100,
fiftyTwoWeekLow: Math.random() * 800 + 50,
timestamp: Date.now() / 1000,
source: 'yahoo-finance',
modules: payload.modules || ['price', 'summaryDetail'],
};
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 150 + Math.random() * 250));
return mockData;
},
'historical-data': async (payload: {
symbol: string;
period1: number;
period2: number;
interval?: string;
events?: string;
}) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Fetching historical data from Yahoo Finance', {
symbol: payload.symbol,
period1: payload.period1,
period2: payload.period2,
interval: payload.interval || '1d',
});
// Generate mock historical data
const days = Math.ceil((payload.period2 - payload.period1) / (24 * 60 * 60));
const data = [];
for (let i = 0; i < Math.min(days, 100); i++) {
const timestamp = payload.period1 + i * 24 * 60 * 60;
data.push({
timestamp,
date: new Date(timestamp * 1000).toISOString().split('T')[0],
open: Math.random() * 1000 + 100,
high: Math.random() * 1000 + 100,
low: Math.random() * 1000 + 100,
close: Math.random() * 1000 + 100,
adjClose: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000),
source: 'yahoo-finance',
});
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 250 + Math.random() * 350));
return {
symbol: payload.symbol,
interval: payload.interval || '1d',
timestamps: data.map(d => d.timestamp),
indicators: {
quote: [
{
open: data.map(d => d.open),
high: data.map(d => d.high),
low: data.map(d => d.low),
close: data.map(d => d.close),
volume: data.map(d => d.volume),
},
],
adjclose: [
{
adjclose: data.map(d => d.adjClose),
},
],
},
source: 'yahoo-finance',
totalRecords: data.length,
};
},
search: async (payload: { query: string; quotesCount?: number; newsCount?: number }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Searching Yahoo Finance', { query: payload.query });
// Generate mock search results
const quotes = Array.from({ length: payload.quotesCount || 5 }, (_, i) => ({
symbol: `${payload.query.toUpperCase()}${i}`,
shortname: `${payload.query} Company ${i}`,
longname: `${payload.query} Corporation ${i}`,
exchDisp: 'NASDAQ',
typeDisp: 'Equity',
source: 'yahoo-finance',
}));
const news = Array.from({ length: payload.newsCount || 3 }, (_, i) => ({
uuid: `news-${i}-${Date.now()}`,
title: `${payload.query} News Article ${i}`,
publisher: 'Financial News',
providerPublishTime: Date.now() - i * 3600000,
type: 'STORY',
source: 'yahoo-finance',
}));
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 200));
return {
quotes,
news,
totalQuotes: quotes.length,
totalNews: news.length,
source: 'yahoo-finance',
};
},
financials: async (payload: { symbol: string; type?: 'income' | 'balance' | 'cash' }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Fetching financials from Yahoo Finance', {
symbol: payload.symbol,
type: payload.type || 'income',
});
// Generate mock financial data
const financials = {
symbol: payload.symbol,
type: payload.type || 'income',
currency: 'USD',
annual: Array.from({ length: 4 }, (_, i) => ({
fiscalYear: 2024 - i,
revenue: Math.floor(Math.random() * 100000000000),
netIncome: Math.floor(Math.random() * 10000000000),
totalAssets: Math.floor(Math.random() * 500000000000),
totalDebt: Math.floor(Math.random() * 50000000000),
})),
quarterly: Array.from({ length: 4 }, (_, i) => ({
fiscalQuarter: `Q${4 - i} 2024`,
revenue: Math.floor(Math.random() * 25000000000),
netIncome: Math.floor(Math.random() * 2500000000),
})),
source: 'yahoo-finance',
};
await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 200));
return financials;
},
earnings: async (payload: { symbol: string; period?: 'annual' | 'quarterly' }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Fetching earnings from Yahoo Finance', {
symbol: payload.symbol,
period: payload.period || 'quarterly',
});
// Generate mock earnings data
const earnings = {
symbol: payload.symbol,
period: payload.period || 'quarterly',
earnings: Array.from({ length: 8 }, (_, i) => ({
quarter: `Q${(i % 4) + 1} ${2024 - Math.floor(i / 4)}`,
epsEstimate: Math.random() * 5,
epsActual: Math.random() * 5,
revenueEstimate: Math.floor(Math.random() * 50000000000),
revenueActual: Math.floor(Math.random() * 50000000000),
surprise: (Math.random() - 0.5) * 2,
})),
source: 'yahoo-finance',
};
await new Promise(resolve => setTimeout(resolve, 250 + Math.random() * 150));
return earnings;
},
recommendations: async (payload: { symbol: string }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Fetching recommendations from Yahoo Finance', { symbol: payload.symbol });
// Generate mock recommendations
const recommendations = {
symbol: payload.symbol,
current: {
strongBuy: Math.floor(Math.random() * 10),
buy: Math.floor(Math.random() * 15),
hold: Math.floor(Math.random() * 20),
sell: Math.floor(Math.random() * 5),
strongSell: Math.floor(Math.random() * 3),
},
trend: Array.from({ length: 4 }, (_, i) => ({
period: `${i}m`,
strongBuy: Math.floor(Math.random() * 10),
buy: Math.floor(Math.random() * 15),
hold: Math.floor(Math.random() * 20),
sell: Math.floor(Math.random() * 5),
strongSell: Math.floor(Math.random() * 3),
})),
source: 'yahoo-finance',
};
await new Promise(resolve => setTimeout(resolve, 180 + Math.random() * 120));
return recommendations;
},
},
scheduledJobs: [
// {
// type: 'yahoo-market-refresh',
// operation: 'live-data',
// payload: { symbol: 'AAPL' },
// cronPattern: '*/1 * * * *', // Every minute
// priority: 8,
// description: 'Refresh Apple stock price from Yahoo Finance'
// },
// {
// type: 'yahoo-sp500-update',
// operation: 'live-data',
// payload: { symbol: 'SPY' },
// cronPattern: '*/2 * * * *', // Every 2 minutes
// priority: 9,
// description: 'Update S&P 500 ETF price'
// },
// {
// type: 'yahoo-earnings-check',
// operation: 'earnings',
// payload: { symbol: 'AAPL' },
// cronPattern: '0 16 * * 1-5', // Weekdays at 4 PM (market close)
// priority: 6,
// description: 'Check earnings data for Apple'
// }
],
};

View file

@ -0,0 +1,20 @@
/**
* Health check routes
*/
import { Hono } from 'hono';
import { queueManager } from '../services/queue.service';
export const healthRoutes = new Hono();
// Health check endpoint
healthRoutes.get('/health', c => {
return c.json({
service: 'data-service',
status: 'healthy',
timestamp: new Date().toISOString(),
queue: {
status: 'running',
workers: queueManager.getWorkerCount(),
},
});
});

View file

@ -0,0 +1,8 @@
/**
* Routes index - exports all route modules
*/
export { healthRoutes } from './health.routes';
export { queueRoutes } from './queue.routes';
export { marketDataRoutes } from './market-data.routes';
export { proxyRoutes } from './proxy.routes';
export { testRoutes } from './test.routes';

View file

@ -0,0 +1,74 @@
/**
* Market data routes
*/
import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger';
import { queueManager } from '../services/queue.service';
const logger = getLogger('market-data-routes');
export const marketDataRoutes = new Hono();
// Market data endpoints
marketDataRoutes.get('/api/live/:symbol', async c => {
const symbol = c.req.param('symbol');
logger.info('Live data request', { symbol });
try {
// Queue job for live data using Yahoo provider
const job = await queueManager.addJob({
type: 'market-data-live',
service: 'market-data',
provider: 'yahoo-finance',
operation: 'live-data',
payload: { symbol },
});
return c.json({
status: 'success',
message: 'Live data job queued',
jobId: job.id,
symbol,
});
} catch (error) {
logger.error('Failed to queue live data job', { symbol, error });
return c.json({ status: 'error', message: 'Failed to queue live data job' }, 500);
}
});
marketDataRoutes.get('/api/historical/:symbol', async c => {
const symbol = c.req.param('symbol');
const from = c.req.query('from');
const to = c.req.query('to');
logger.info('Historical data request', { symbol, from, to });
try {
const fromDate = from ? new Date(from) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
const toDate = to ? new Date(to) : new Date(); // Now
// Queue job for historical data using Yahoo provider
const job = await queueManager.addJob({
type: 'market-data-historical',
service: 'market-data',
provider: 'yahoo-finance',
operation: 'historical-data',
payload: {
symbol,
from: fromDate.toISOString(),
to: toDate.toISOString(),
},
});
return c.json({
status: 'success',
message: 'Historical data job queued',
jobId: job.id,
symbol,
from: fromDate,
to: toDate,
});
} catch (error) {
logger.error('Failed to queue historical data job', { symbol, from, to, error });
return c.json({ status: 'error', message: 'Failed to queue historical data job' }, 500);
}
});

View file

@ -0,0 +1,76 @@
/**
* Proxy management routes
*/
import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger';
import { queueManager } from '../services/queue.service';
const logger = getLogger('proxy-routes');
export const proxyRoutes = new Hono();
// Proxy management endpoints
proxyRoutes.post('/api/proxy/fetch', async c => {
try {
const job = await queueManager.addJob({
type: 'proxy-fetch',
provider: 'proxy-provider',
operation: 'fetch-and-check',
payload: {},
priority: 5,
});
return c.json({
status: 'success',
jobId: job.id,
message: 'Proxy fetch job queued',
});
} catch (error) {
logger.error('Failed to queue proxy fetch', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy fetch' }, 500);
}
});
proxyRoutes.post('/api/proxy/check', async c => {
try {
const { proxies } = await c.req.json();
const job = await queueManager.addJob({
type: 'proxy-check',
provider: 'proxy-provider',
operation: 'check-specific',
payload: { proxies },
priority: 8,
});
return c.json({
status: 'success',
jobId: job.id,
message: `Proxy check job queued for ${proxies.length} proxies`,
});
} catch (error) {
logger.error('Failed to queue proxy check', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy check' }, 500);
}
});
// Get proxy stats via queue
proxyRoutes.get('/api/proxy/stats', async c => {
try {
const job = await queueManager.addJob({
type: 'proxy-stats',
provider: 'proxy-provider',
operation: 'get-stats',
payload: {},
priority: 3,
});
return c.json({
status: 'success',
jobId: job.id,
message: 'Proxy stats job queued',
});
} catch (error) {
logger.error('Failed to queue proxy stats', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy stats' }, 500);
}
});

View file

@ -0,0 +1,71 @@
/**
* Queue management routes
*/
import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger';
import { queueManager } from '../services/queue.service';
const logger = getLogger('queue-routes');
export const queueRoutes = new Hono();
// Queue management endpoints
queueRoutes.get('/api/queue/status', async c => {
try {
const status = await queueManager.getQueueStatus();
return c.json({ status: 'success', data: status });
} catch (error) {
logger.error('Failed to get queue status', { error });
return c.json({ status: 'error', message: 'Failed to get queue status' }, 500);
}
});
queueRoutes.post('/api/queue/job', async c => {
try {
const jobData = await c.req.json();
const job = await queueManager.addJob(jobData);
return c.json({ status: 'success', jobId: job.id });
} catch (error) {
logger.error('Failed to add job', { error });
return c.json({ status: 'error', message: 'Failed to add job' }, 500);
}
});
// Provider registry endpoints
queueRoutes.get('/api/providers', async c => {
try {
const { providerRegistry } = await import('../services/provider-registry.service');
const providers = providerRegistry.getProviders();
return c.json({ status: 'success', providers });
} catch (error) {
logger.error('Failed to get providers', { error });
return c.json({ status: 'error', message: 'Failed to get providers' }, 500);
}
});
// Add new endpoint to see scheduled jobs
queueRoutes.get('/api/scheduled-jobs', async c => {
try {
const { providerRegistry } = await import('../services/provider-registry.service');
const jobs = providerRegistry.getAllScheduledJobs();
return c.json({
status: 'success',
count: jobs.length,
jobs,
});
} catch (error) {
logger.error('Failed to get scheduled jobs info', { error });
return c.json({ status: 'error', message: 'Failed to get scheduled jobs' }, 500);
}
});
queueRoutes.post('/api/queue/drain', async c => {
try {
await queueManager.drainQueue();
const status = await queueManager.getQueueStatus();
return c.json({ status: 'success', message: 'Queue drained', queueStatus: status });
} catch (error) {
logger.error('Failed to drain queue', { error });
return c.json({ status: 'error', message: 'Failed to drain queue' }, 500);
}
});

View file

@ -0,0 +1,87 @@
/**
* Test and development routes for batch processing
*/
import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger';
import { queueManager } from '../services/queue.service';
const logger = getLogger('test-routes');
export const testRoutes = new Hono();
// Test endpoint for new functional batch processing
testRoutes.post('/api/test/batch-symbols', async c => {
try {
const { symbols, useBatching = false, totalDelayHours = 1 } = await c.req.json();
const { processItems } = await import('../utils/batch-helpers');
if (!symbols || !Array.isArray(symbols)) {
return c.json({ status: 'error', message: 'symbols array is required' }, 400);
}
const result = await processItems(
symbols,
(symbol, index) => ({
symbol,
index,
timestamp: new Date().toISOString(),
}),
queueManager,
{
totalDelayHours,
useBatching,
batchSize: 10,
priority: 1,
provider: 'test-provider',
operation: 'live-data',
}
);
return c.json({
status: 'success',
message: 'Batch processing started',
result,
});
} catch (error) {
logger.error('Failed to start batch symbol processing', { error });
return c.json({ status: 'error', message: 'Failed to start batch processing' }, 500);
}
});
testRoutes.post('/api/test/batch-custom', async c => {
try {
const { items, useBatching = false, totalDelayHours = 0.5 } = await c.req.json();
const { processItems } = await import('../utils/batch-helpers');
if (!items || !Array.isArray(items)) {
return c.json({ status: 'error', message: 'items array is required' }, 400);
}
const result = await processItems(
items,
(item, index) => ({
originalItem: item,
processIndex: index,
timestamp: new Date().toISOString(),
}),
queueManager,
{
totalDelayHours,
useBatching,
batchSize: 5,
priority: 1,
provider: 'test-provider',
operation: 'custom-test',
}
);
return c.json({
status: 'success',
message: 'Custom batch processing started',
result,
});
} catch (error) {
logger.error('Failed to start custom batch processing', { error });
return c.json({ status: 'error', message: 'Failed to start custom batch processing' }, 500);
}
});

View file

@ -0,0 +1,135 @@
import { getLogger } from '@stock-bot/logger';
export interface JobHandler {
(payload: any): Promise<any>;
}
export interface JobData {
type?: string;
provider: string;
operation: string;
payload: any;
priority?: number;
immediately?: boolean;
}
export interface ScheduledJob {
type: string;
operation: string;
payload: any;
cronPattern: string;
priority?: number;
description?: string;
immediately?: boolean;
}
export interface ProviderConfig {
name: string;
operations: Record<string, JobHandler>;
scheduledJobs?: ScheduledJob[];
}
export interface ProviderRegistry {
registerProvider: (config: ProviderConfig) => void;
getHandler: (provider: string, operation: string) => JobHandler | null;
getAllScheduledJobs: () => Array<{ provider: string; job: ScheduledJob }>;
getProviders: () => Array<{ key: string; config: ProviderConfig }>;
hasProvider: (provider: string) => boolean;
clear: () => void;
}
/**
* Create a new provider registry instance
*/
export function createProviderRegistry(): ProviderRegistry {
const logger = getLogger('provider-registry');
const providers = new Map<string, ProviderConfig>();
/**
* Register a provider with its operations
*/
function registerProvider(config: ProviderConfig): void {
providers.set(config.name, config);
logger.info(`Registered provider: ${config.name}`, {
operations: Object.keys(config.operations),
scheduledJobs: config.scheduledJobs?.length || 0,
});
}
/**
* Get a job handler for a specific provider and operation
*/
function getHandler(provider: string, operation: string): JobHandler | null {
const providerConfig = providers.get(provider);
if (!providerConfig) {
logger.warn(`Provider not found: ${provider}`);
return null;
}
const handler = providerConfig.operations[operation];
if (!handler) {
logger.warn(`Operation not found: ${operation} in provider ${provider}`);
return null;
}
return handler;
}
/**
* Get all scheduled jobs from all providers
*/
function getAllScheduledJobs(): Array<{ provider: string; job: ScheduledJob }> {
const allJobs: Array<{ provider: string; job: ScheduledJob }> = [];
for (const [, config] of providers) {
if (config.scheduledJobs) {
for (const job of config.scheduledJobs) {
allJobs.push({
provider: config.name,
job,
});
}
}
}
return allJobs;
}
/**
* Get all registered providers with their configurations
*/
function getProviders(): Array<{ key: string; config: ProviderConfig }> {
return Array.from(providers.entries()).map(([key, config]) => ({
key,
config,
}));
}
/**
* Check if a provider exists
*/
function hasProvider(provider: string): boolean {
return providers.has(provider);
}
/**
* Clear all providers (useful for testing)
*/
function clear(): void {
providers.clear();
logger.info('All providers cleared');
}
return {
registerProvider,
getHandler,
getAllScheduledJobs,
getProviders,
hasProvider,
clear,
};
}
// Create the default shared registry instance
export const providerRegistry = createProviderRegistry();

View file

@ -0,0 +1,419 @@
import { Queue, QueueEvents, Worker, type Job } from 'bullmq';
import { getLogger } from '@stock-bot/logger';
import { providerRegistry, type JobData } from './provider-registry.service';
export class QueueService {
private logger = getLogger('queue-service');
private queue!: Queue;
private workers: Worker[] = [];
private queueEvents!: QueueEvents;
private config = {
workers: parseInt(process.env.WORKER_COUNT || '5'),
concurrency: parseInt(process.env.WORKER_CONCURRENCY || '20'),
redis: {
host: process.env.DRAGONFLY_HOST || 'localhost',
port: parseInt(process.env.DRAGONFLY_PORT || '6379'),
},
};
private get isInitialized() {
return !!this.queue;
}
constructor() {
// Don't initialize in constructor to allow for proper async initialization
}
async initialize() {
if (this.isInitialized) {
this.logger.warn('Queue service already initialized');
return;
}
this.logger.info('Initializing queue service...');
try {
// Step 1: Register providers
await this.registerProviders();
// Step 2: Setup queue and workers
const connection = this.getConnection();
const queueName = '{data-service-queue}';
this.queue = new Queue(queueName, {
connection,
defaultJobOptions: {
removeOnComplete: 10,
removeOnFail: 5,
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
},
});
this.queueEvents = new QueueEvents(queueName, { connection });
// Step 3: Create workers
const { workerCount, totalConcurrency } = this.createWorkers(queueName, connection);
// Step 4: Wait for readiness (parallel)
await Promise.all([
this.queue.waitUntilReady(),
this.queueEvents.waitUntilReady(),
...this.workers.map(worker => worker.waitUntilReady()),
]);
// Step 5: Setup events and scheduled tasks
this.setupQueueEvents();
await this.setupScheduledTasks();
this.logger.info('Queue service initialized successfully', {
workers: workerCount,
totalConcurrency,
});
} catch (error) {
this.logger.error('Failed to initialize queue service', { error });
throw error;
}
}
private getConnection() {
return {
...this.config.redis,
maxRetriesPerRequest: null,
retryDelayOnFailover: 100,
lazyConnect: false,
};
}
private createWorkers(queueName: string, connection: any) {
for (let i = 0; i < this.config.workers; i++) {
const worker = new Worker(queueName, this.processJob.bind(this), {
connection: { ...connection },
concurrency: this.config.concurrency,
maxStalledCount: 1,
stalledInterval: 30000,
});
// Setup events inline
worker.on('ready', () => this.logger.info(`Worker ${i + 1} ready`));
worker.on('error', error => this.logger.error(`Worker ${i + 1} error`, { error }));
this.workers.push(worker);
}
return {
workerCount: this.config.workers,
totalConcurrency: this.config.workers * this.config.concurrency,
};
}
private setupQueueEvents() {
// Add comprehensive logging to see job flow
this.queueEvents.on('added', job => {
this.logger.debug('Job added to queue', {
id: job.jobId,
});
});
this.queueEvents.on('waiting', job => {
this.logger.debug('Job moved to waiting', {
id: job.jobId,
});
});
this.queueEvents.on('active', job => {
this.logger.debug('Job became active', {
id: job.jobId,
});
});
this.queueEvents.on('delayed', job => {
this.logger.debug('Job delayed', {
id: job.jobId,
delay: job.delay,
});
});
this.queueEvents.on('completed', job => {
this.logger.debug('Job completed', {
id: job.jobId,
});
});
this.queueEvents.on('failed', (job, error) => {
this.logger.debug('Job failed', {
id: job.jobId,
error: String(error),
});
});
}
private async registerProviders() {
this.logger.info('Registering providers...');
try {
// Define providers to register
const providers = [
{ module: '../providers/proxy.provider', export: 'proxyProvider' },
{ module: '../providers/ib.provider', export: 'ibProvider' },
// { module: '../providers/yahoo.provider', export: 'yahooProvider' },
];
// Import and register all providers
for (const { module, export: exportName } of providers) {
const providerModule = await import(module);
providerRegistry.registerProvider(providerModule[exportName]);
}
this.logger.info('All providers registered successfully');
} catch (error) {
this.logger.error('Failed to register providers', { error });
throw error;
}
}
private async processJob(job: Job) {
const { provider, operation, payload }: JobData = job.data;
this.logger.info('Processing job', {
id: job.id,
provider,
operation,
payloadKeys: Object.keys(payload || {}),
});
try {
let result;
if (operation === 'process-batch-items') {
// Special handling for batch processing - requires 2 parameters
const { processBatchJob } = await import('../utils/batch-helpers');
result = await processBatchJob(payload, this);
} else {
// Regular handler lookup - requires 1 parameter
const handler = providerRegistry.getHandler(provider, operation);
if (!handler) {
throw new Error(`No handler found for ${provider}:${operation}`);
}
result = await handler(payload);
}
this.logger.info('Job completed successfully', {
id: job.id,
provider,
operation,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('Job failed', {
id: job.id,
provider,
operation,
error: errorMessage,
});
throw error;
}
}
async addBulk(jobs: any[]): Promise<any[]> {
return await this.queue.addBulk(jobs);
}
private getTotalConcurrency() {
return this.workers.reduce((total, worker) => total + (worker.opts.concurrency || 1), 0);
}
private async setupScheduledTasks() {
const allScheduledJobs = providerRegistry.getAllScheduledJobs();
if (allScheduledJobs.length === 0) {
this.logger.warn('No scheduled jobs found in providers');
return;
}
this.logger.info('Setting up scheduled tasks...', { count: allScheduledJobs.length });
// Use Promise.allSettled for parallel processing + better error handling
const results = await Promise.allSettled(
allScheduledJobs.map(async ({ provider, job }) => {
await this.addRecurringJob(
{
type: job.type,
provider,
operation: job.operation,
payload: job.payload,
priority: job.priority,
immediately: job.immediately || false,
},
job.cronPattern
);
return { provider, operation: job.operation };
})
);
// Log results
const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
failed.forEach((result, index) => {
const { provider, job } = allScheduledJobs[index];
this.logger.error('Failed to register scheduled job', {
provider,
operation: job.operation,
error: result.reason,
});
});
}
this.logger.info('Scheduled tasks setup complete', {
successful: successful.length,
failed: failed.length,
});
}
private async addJobInternal(jobData: JobData, options: any = {}) {
if (!this.isInitialized) {
throw new Error('Queue service not initialized');
}
const jobType = jobData.type || `${jobData.provider}-${jobData.operation}`;
return this.queue.add(jobType, jobData, {
priority: jobData.priority || undefined,
removeOnComplete: 10,
removeOnFail: 5,
...options,
});
}
async addJob(jobData: JobData, options?: any) {
return this.addJobInternal(jobData, options);
}
async addRecurringJob(jobData: JobData, cronPattern: string, options?: any) {
const jobKey = `recurring-${jobData.provider}-${jobData.operation}`;
return this.addJobInternal(jobData, {
repeat: {
pattern: cronPattern,
tz: 'UTC',
immediately: jobData.immediately || false,
},
jobId: jobKey,
removeOnComplete: 1,
removeOnFail: 1,
attempts: 2,
backoff: {
type: 'fixed',
delay: 5000,
},
...options,
});
}
async getJobStats() {
if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.');
}
const [waiting, active, completed, failed, delayed] = await Promise.all([
this.queue.getWaiting(),
this.queue.getActive(),
this.queue.getCompleted(),
this.queue.getFailed(),
this.queue.getDelayed(),
]);
return {
waiting: waiting.length,
active: active.length,
completed: completed.length,
failed: failed.length,
delayed: delayed.length,
};
}
async drainQueue() {
if (this.isInitialized) {
await this.queue.drain();
}
}
async getQueueStatus() {
if (!this.isInitialized) {
throw new Error('Queue service not initialized');
}
const stats = await this.getJobStats();
return {
...stats,
workers: this.workers.length,
concurrency: this.getTotalConcurrency(),
};
}
async shutdown() {
if (!this.isInitialized) {
this.logger.warn('Queue service not initialized, nothing to shutdown');
return;
}
this.logger.info('Shutting down queue service gracefully...');
try {
// Step 1: Stop accepting new jobs and wait for current jobs to finish
this.logger.debug('Closing workers gracefully...');
const workerClosePromises = this.workers.map(async (worker, index) => {
this.logger.debug(`Closing worker ${index + 1}/${this.workers.length}`);
try {
// Wait for current jobs to finish, then close
await Promise.race([
worker.close(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Worker ${index + 1} close timeout`)), 5000)
),
]);
this.logger.debug(`Worker ${index + 1} closed successfully`);
} catch (error) {
this.logger.error(`Failed to close worker ${index + 1}`, { error });
// Force close if graceful close fails
await worker.close(true);
}
});
await Promise.allSettled(workerClosePromises);
this.logger.debug('All workers closed');
// Step 2: Close queue and events with timeout protection
this.logger.debug('Closing queue and events...');
await Promise.allSettled([
Promise.race([
this.queue.close(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Queue close timeout')), 3000)
),
]).catch(error => this.logger.error('Queue close error', { error })),
Promise.race([
this.queueEvents.close(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('QueueEvents close timeout')), 3000)
),
]).catch(error => this.logger.error('QueueEvents close error', { error })),
]);
this.logger.info('Queue service shutdown completed successfully');
} catch (error) {
this.logger.error('Error during queue service shutdown', { error });
// Force close everything as last resort
try {
await Promise.allSettled([
...this.workers.map(worker => worker.close(true)),
this.queue.close(),
this.queueEvents.close(),
]);
} catch (forceCloseError) {
this.logger.error('Force close also failed', { error: forceCloseError });
}
throw error;
}
}
}
export const queueManager = new QueueService();

View file

@ -0,0 +1,364 @@
import { CacheProvider, createCache } from '@stock-bot/cache';
import { getLogger } from '@stock-bot/logger';
import type { QueueService } from '../services/queue.service';
const logger = getLogger('batch-helpers');
// Simple interfaces
export interface ProcessOptions {
totalDelayHours: number;
batchSize?: number;
priority?: number;
useBatching?: boolean;
retries?: number;
ttl?: number;
removeOnComplete?: number;
removeOnFail?: number;
// Job routing information
provider?: string;
operation?: string;
}
export interface BatchResult {
jobsCreated: number;
mode: 'direct' | 'batch';
totalItems: number;
batchesCreated?: number;
duration: number;
}
// Cache instance for payload storage
let cacheProvider: CacheProvider | null = null;
function getCache(): CacheProvider {
if (!cacheProvider) {
cacheProvider = createCache({
keyPrefix: 'batch:',
ttl: 86400, // 24 hours default
enableMetrics: true,
});
}
return cacheProvider;
}
/**
* Initialize the batch cache before any batch operations
* This should be called during application startup
*/
export async function initializeBatchCache(): Promise<void> {
logger.info('Initializing batch cache...');
const cache = getCache();
await cache.waitForReady(10000);
logger.info('Batch cache initialized successfully');
}
/**
* Main function - processes items either directly or in batches
*/
export async function processItems<T>(
items: T[],
processor: (item: T, index: number) => any,
queue: QueueService,
options: ProcessOptions
): Promise<BatchResult> {
const startTime = Date.now();
if (items.length === 0) {
return {
jobsCreated: 0,
mode: 'direct',
totalItems: 0,
duration: 0,
};
}
logger.info('Starting batch processing', {
totalItems: items.length,
mode: options.useBatching ? 'batch' : 'direct',
batchSize: options.batchSize,
totalDelayHours: options.totalDelayHours,
});
try {
const result = options.useBatching
? await processBatched(items, processor, queue, options)
: await processDirect(items, processor, queue, options);
const duration = Date.now() - startTime;
logger.info('Batch processing completed', {
...result,
duration: `${(duration / 1000).toFixed(1)}s`,
});
return { ...result, duration };
} catch (error) {
logger.error('Batch processing failed', error);
throw error;
}
}
/**
* Process items directly - each item becomes a separate job
*/
async function processDirect<T>(
items: T[],
processor: (item: T, index: number) => any,
queue: QueueService,
options: ProcessOptions
): Promise<Omit<BatchResult, 'duration'>> {
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000;
const delayPerItem = totalDelayMs / items.length;
logger.info('Creating direct jobs', {
totalItems: items.length,
delayPerItem: `${(delayPerItem / 1000).toFixed(1)}s`,
});
const jobs = items.map((item, index) => ({
name: 'process-item',
data: {
type: 'process-item',
provider: options.provider || 'generic',
operation: options.operation || 'process-item',
payload: processor(item, index),
priority: options.priority || undefined,
},
opts: {
delay: index * delayPerItem,
priority: options.priority || undefined,
attempts: options.retries || 3,
removeOnComplete: options.removeOnComplete || 10,
removeOnFail: options.removeOnFail || 5,
},
}));
const createdJobs = await addJobsInChunks(queue, jobs);
return {
totalItems: items.length,
jobsCreated: createdJobs.length,
mode: 'direct',
};
}
/**
* Process items in batches - groups of items are stored and processed together
*/
async function processBatched<T>(
items: T[],
processor: (item: T, index: number) => any,
queue: QueueService,
options: ProcessOptions
): Promise<Omit<BatchResult, 'duration'>> {
const batchSize = options.batchSize || 100;
const batches = createBatches(items, batchSize);
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000;
const delayPerBatch = totalDelayMs / batches.length;
logger.info('Creating batch jobs', {
totalItems: items.length,
batchSize,
totalBatches: batches.length,
delayPerBatch: `${(delayPerBatch / 1000 / 60).toFixed(2)} minutes`,
});
const batchJobs = await Promise.all(
batches.map(async (batch, batchIndex) => {
const payloadKey = await storePayload(batch, processor, options);
return {
name: 'process-batch',
data: {
type: 'process-batch',
provider: options.provider || 'generic',
operation: 'process-batch-items',
payload: {
payloadKey,
batchIndex,
totalBatches: batches.length,
itemCount: batch.length,
},
priority: options.priority || undefined,
},
opts: {
delay: batchIndex * delayPerBatch,
priority: options.priority || undefined,
attempts: options.retries || 3,
removeOnComplete: options.removeOnComplete || 10,
removeOnFail: options.removeOnFail || 5,
},
};
})
);
const createdJobs = await addJobsInChunks(queue, batchJobs);
return {
totalItems: items.length,
jobsCreated: createdJobs.length,
batchesCreated: batches.length,
mode: 'batch',
};
}
/**
* Process a batch job - loads payload from cache and creates individual jobs
*/
export async function processBatchJob(jobData: any, queue: QueueService): Promise<any> {
const { payloadKey, batchIndex, totalBatches, itemCount } = jobData;
logger.debug('Processing batch job', {
batchIndex,
totalBatches,
itemCount,
});
try {
const payload = await loadPayload(payloadKey);
if (!payload || !payload.items || !payload.processorStr) {
logger.error('Invalid payload data', { payloadKey, payload });
throw new Error(`Invalid payload data for key: ${payloadKey}`);
}
const { items, processorStr, options } = payload;
// Deserialize the processor function
const processor = new Function('return ' + processorStr)();
const jobs = items.map((item: any, index: number) => ({
name: 'process-item',
data: {
type: 'process-item',
provider: options.provider || 'generic',
operation: options.operation || 'generic',
payload: processor(item, index),
priority: options.priority || undefined,
},
opts: {
delay: index * (options.delayPerItem || 1000),
priority: options.priority || undefined,
attempts: options.retries || 3,
},
}));
const createdJobs = await addJobsInChunks(queue, jobs);
// Cleanup payload after successful processing
await cleanupPayload(payloadKey);
return {
batchIndex,
itemsProcessed: items.length,
jobsCreated: createdJobs.length,
};
} catch (error) {
logger.error('Batch job processing failed', { batchIndex, error });
throw error;
}
}
// Helper functions
function createBatches<T>(items: T[], batchSize: number): T[][] {
const batches: T[][] = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}
async function storePayload<T>(
items: T[],
processor: (item: T, index: number) => any,
options: ProcessOptions
): Promise<string> {
const cache = getCache();
// Create more specific key: batch:provider:operation:payload_timestamp_random
const timestamp = Date.now();
const randomId = Math.random().toString(36).substr(2, 9);
const provider = options.provider || 'generic';
const operation = options.operation || 'generic';
const key = `${provider}:${operation}:payload_${timestamp}_${randomId}`;
const payload = {
items,
processorStr: processor.toString(),
options: {
delayPerItem: 1000,
priority: options.priority || undefined,
retries: options.retries || 3,
// Store routing information for later use
provider: options.provider || 'generic',
operation: options.operation || 'generic',
},
createdAt: Date.now(),
};
logger.debug('Storing batch payload', {
key,
itemCount: items.length,
});
await cache.set(key, payload, options.ttl || 86400);
logger.debug('Stored batch payload successfully', {
key,
itemCount: items.length,
});
return key;
}
async function loadPayload(key: string): Promise<any> {
const cache = getCache();
logger.debug('Loading batch payload', { key });
const data = await cache.get(key);
if (!data) {
logger.error('Payload not found in cache', { key });
throw new Error(`Payload not found: ${key}`);
}
logger.debug('Loaded batch payload successfully', { key });
return data;
}
async function cleanupPayload(key: string): Promise<void> {
try {
const cache = getCache();
await cache.del(key);
logger.debug('Cleaned up payload', { key });
} catch (error) {
logger.warn('Failed to cleanup payload', { key, error });
}
}
async function addJobsInChunks(queue: QueueService, jobs: any[], chunkSize = 100): Promise<any[]> {
const allCreatedJobs = [];
for (let i = 0; i < jobs.length; i += chunkSize) {
const chunk = jobs.slice(i, i + chunkSize);
try {
const createdJobs = await queue.addBulk(chunk);
allCreatedJobs.push(...createdJobs);
// Small delay between chunks to avoid overwhelming Redis
if (i + chunkSize < jobs.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
logger.error('Failed to add job chunk', {
startIndex: i,
chunkSize: chunk.length,
error,
});
}
}
return allCreatedJobs;
}

Some files were not shown because too many files have changed in this diff Show more