linxus fs fixes

This commit is contained in:
Boki 2025-06-09 22:55:51 -04:00
parent ac23b70146
commit 0b7846fe67
292 changed files with 41947 additions and 41947 deletions

View file

@ -1,60 +1,60 @@
# Node.js # Node.js
node_modules/ node_modules/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# Build outputs # Build outputs
dist/ dist/
build/ build/
.turbo/ .turbo/
.next/ .next/
# Environment files # Environment files
.env .env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
*.swo *.swo
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Logs # Logs
logs/ logs/
*.log *.log
# Git # Git
.git/ .git/
.gitignore .gitignore
# Documentation # Documentation
README.md README.md
DOCKER.md DOCKER.md
docs/ docs/
# Docker # Docker
Dockerfile* Dockerfile*
docker-compose* docker-compose*
.dockerignore .dockerignore
# Cache # Cache
.cache/ .cache/
.temp/ .temp/
.tmp/ .tmp/
# Test coverage # Test coverage
coverage/ coverage/
.nyc_output/ .nyc_output/
# Misc # Misc
*.tgz *.tgz
*.tar.gz *.tar.gz

224
.gitignore vendored
View file

@ -1,112 +1,112 @@
# Dependencies # Dependencies
node_modules/ node_modules/
.pnp .pnp
.pnp.js .pnp.js
# Production builds # Production builds
dist/ dist/
build/ build/
.next/ .next/
*.js.map *.js.map
*.d.ts *.d.ts
# Environment variables # Environment variables
.env .env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
# Logs # Logs
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
bun-debug.log* bun-debug.log*
bun-error.log* bun-error.log*
*.log *.log
# Runtime data # Runtime data
pids pids
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
*.tsbuildinfo *.tsbuildinfo
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage/ coverage/
*.lcov *.lcov
# Dependency directories # Dependency directories
.pnpm-store/ .pnpm-store/
# Optional npm cache directory # Optional npm cache directory
.npm .npm
# Optional eslint cache # Optional eslint cache
.eslintcache .eslintcache
# Optional stylelint cache # Optional stylelint cache
.stylelintcache .stylelintcache
# Microbundle cache # Microbundle cache
.rpt2_cache/ .rpt2_cache/
.rts2_cache_cjs/ .rts2_cache_cjs/
.rts2_cache_es/ .rts2_cache_es/
.rts2_cache_umd/ .rts2_cache_umd/
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
# Output of 'npm pack' # Output of 'npm pack'
*.tgz *.tgz
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)
.cache .cache
.parcel-cache .parcel-cache
# Next.js build output # Next.js build output
.next .next
# Nuxt.js build / generate output # Nuxt.js build / generate output
.nuxt .nuxt
# Storybook build outputs # Storybook build outputs
.out .out
.storybook-out .storybook-out
# Temporary folders # Temporary folders
tmp/ tmp/
temp/ temp/
# Editor directories and files # Editor directories and files
.idea/ .idea/
*.swp *.swp
*.swo *.swo
*~ *~
# OS generated files # OS generated files
.DS_Store .DS_Store
.DS_Store? .DS_Store?
._* ._*
.Spotlight-V100 .Spotlight-V100
.Trashes .Trashes
ehthumbs.db ehthumbs.db
Thumbs.db Thumbs.db
# Trading bot specific # Trading bot specific
.data/ .data/
.backtest-results/ .backtest-results/
.logs/ .logs/
.old/ .old/
.mongo/ .mongo/
.chat/ .chat/
*.db *.db
*.sqlite *.sqlite
*.sqlite3 *.sqlite3
# Turbo # Turbo
.turbo .turbo

View file

@ -1,19 +1,19 @@
stages: stages:
- build - build
- test - test
- deploy - deploy
- review - review
- dast - dast
- staging - staging
- canary - canary
- production - production
- incremental rollout 10% - incremental rollout 10%
- incremental rollout 25% - incremental rollout 25%
- incremental rollout 50% - incremental rollout 50%
- incremental rollout 100% - incremental rollout 100%
- performance - performance
- cleanup - cleanup
sast: sast:
stage: test stage: test
include: include:
- template: Auto-DevOps.gitlab-ci.yml - template: Auto-DevOps.gitlab-ci.yml

46
.vscode/settings.json vendored
View file

@ -1,24 +1,24 @@
{ {
"yaml.schemas": { "yaml.schemas": {
"https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json": [ "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json": [
"docker-compose*.yml", "docker-compose*.yml",
"docker-compose*.yaml" "docker-compose*.yaml"
], ],
"https://json.schemastore.org/grafana-dashboard-5.x.json": [ "https://json.schemastore.org/grafana-dashboard-5.x.json": [
"monitoring/grafana/provisioning/datasources/*.yml" "monitoring/grafana/provisioning/datasources/*.yml"
], ],
"https://json.schemastore.org/kustomization.json": [ "https://json.schemastore.org/kustomization.json": [
"k8s/**/kustomization.yml" "k8s/**/kustomization.yml"
] ]
}, },
"yaml.customTags": [ "yaml.customTags": [
"!datasources", "!datasources",
"!dashboard", "!dashboard",
"!notification", "!notification",
"!template" "!template"
], ],
"yaml.validate": true, "yaml.validate": true,
"yaml.completion": true, "yaml.completion": true,
"yaml.hover": true, "yaml.hover": true,
"yaml.format.enable": true "yaml.format.enable": true
} }

38
.vscode/tasks.json vendored
View file

@ -1,19 +1,19 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "Start Data Service", "label": "Start Data Service",
"type": "shell", "type": "shell",
"command": "bun", "command": "bun",
"args": [ "args": [
"run", "run",
"dev" "dev"
], ],
"group": "build", "group": "build",
"isBackground": true, "isBackground": true,
"problemMatcher": [ "problemMatcher": [
"$tsc" "$tsc"
] ]
} }
] ]
} }

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,59 +1,59 @@
# TradingDashboard # TradingDashboard
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.0. This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.0.
## Development server ## Development server
To start a local development server, run: To start a local development server, run:
```bash ```bash
ng serve 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. 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 ## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run: Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash ```bash
ng generate component component-name ng generate component component-name
``` ```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash ```bash
ng generate --help ng generate --help
``` ```
## Building ## Building
To build the project run: To build the project run:
```bash ```bash
ng build 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. 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 ## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash ```bash
ng test ng test
``` ```
## Running end-to-end tests ## Running end-to-end tests
For end-to-end (e2e) testing, run: For end-to-end (e2e) testing, run:
```bash ```bash
ng e2e ng e2e
``` ```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources ## 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. 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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,16 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Trading Dashboard</title> <title>Trading Dashboard</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <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/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 href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="preconnect" href="https://fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.gstatic.com">
</head> </head>
<body class="mat-typography"> <body class="mat-typography">
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,258 +1,258 @@
/** /**
* Data Service - Combined live and historical data ingestion with queue-based architecture * Data Service - Combined live and historical data ingestion with queue-based architecture
*/ */
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { loadEnvVariables } from '@stock-bot/config'; import { loadEnvVariables } from '@stock-bot/config';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { queueManager } from './services/queue.service'; import { queueManager } from './services/queue.service';
// Load environment variables // Load environment variables
loadEnvVariables(); loadEnvVariables();
const app = new Hono(); const app = new Hono();
const logger = getLogger('data-service'); const logger = getLogger('data-service');
const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002'); const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002');
// Health check endpoint // Health check endpoint
app.get('/health', (c) => { app.get('/health', (c) => {
return c.json({ return c.json({
service: 'data-service', service: 'data-service',
status: 'healthy', status: 'healthy',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
queue: { queue: {
status: 'running', status: 'running',
workers: queueManager.getWorkerCount() workers: queueManager.getWorkerCount()
} }
}); });
}); });
// Queue management endpoints // Queue management endpoints
app.get('/api/queue/status', async (c) => { app.get('/api/queue/status', async (c) => {
try { try {
const status = await queueManager.getQueueStatus(); const status = await queueManager.getQueueStatus();
return c.json({ status: 'success', data: status }); return c.json({ status: 'success', data: status });
} catch (error) { } catch (error) {
logger.error('Failed to get queue status', { error }); logger.error('Failed to get queue status', { error });
return c.json({ status: 'error', message: 'Failed to get queue status' }, 500); return c.json({ status: 'error', message: 'Failed to get queue status' }, 500);
} }
}); });
app.post('/api/queue/job', async (c) => { app.post('/api/queue/job', async (c) => {
try { try {
const jobData = await c.req.json(); const jobData = await c.req.json();
const job = await queueManager.addJob(jobData); const job = await queueManager.addJob(jobData);
return c.json({ status: 'success', jobId: job.id }); return c.json({ status: 'success', jobId: job.id });
} catch (error) { } catch (error) {
logger.error('Failed to add job', { error }); logger.error('Failed to add job', { error });
return c.json({ status: 'error', message: 'Failed to add job' }, 500); return c.json({ status: 'error', message: 'Failed to add job' }, 500);
} }
}); });
// Market data endpoints // Market data endpoints
app.get('/api/live/:symbol', async (c) => { app.get('/api/live/:symbol', async (c) => {
const symbol = c.req.param('symbol'); const symbol = c.req.param('symbol');
logger.info('Live data request', { symbol }); logger.info('Live data request', { symbol });
try { // Queue job for live data using Yahoo provider try { // Queue job for live data using Yahoo provider
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'market-data-live', type: 'market-data-live',
service: 'market-data', service: 'market-data',
provider: 'yahoo-finance', provider: 'yahoo-finance',
operation: 'live-data', operation: 'live-data',
payload: { symbol } payload: { symbol }
}); });
return c.json({ return c.json({
status: 'success', status: 'success',
message: 'Live data job queued', message: 'Live data job queued',
jobId: job.id, jobId: job.id,
symbol symbol
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue live data job', { symbol, error }); logger.error('Failed to queue live data job', { symbol, error });
return c.json({ status: 'error', message: 'Failed to queue live data job' }, 500); return c.json({ status: 'error', message: 'Failed to queue live data job' }, 500);
} }
}); });
app.get('/api/historical/:symbol', async (c) => { app.get('/api/historical/:symbol', async (c) => {
const symbol = c.req.param('symbol'); const symbol = c.req.param('symbol');
const from = c.req.query('from'); const from = c.req.query('from');
const to = c.req.query('to'); const to = c.req.query('to');
logger.info('Historical data request', { symbol, from, to }); logger.info('Historical data request', { symbol, from, to });
try { try {
const fromDate = from ? new Date(from) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago 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 const toDate = to ? new Date(to) : new Date(); // Now
// Queue job for historical data using Yahoo provider // Queue job for historical data using Yahoo provider
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'market-data-historical', type: 'market-data-historical',
service: 'market-data', service: 'market-data',
provider: 'yahoo-finance', provider: 'yahoo-finance',
operation: 'historical-data', operation: 'historical-data',
payload: { payload: {
symbol, symbol,
from: fromDate.toISOString(), from: fromDate.toISOString(),
to: toDate.toISOString() to: toDate.toISOString()
} }
}); return c.json({ }); return c.json({
status: 'success', status: 'success',
message: 'Historical data job queued', message: 'Historical data job queued',
jobId: job.id, jobId: job.id,
symbol, symbol,
from: fromDate, from: fromDate,
to: toDate to: toDate
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue historical data job', { symbol, from, to, 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); } return c.json({ status: 'error', message: 'Failed to queue historical data job' }, 500); }
}); });
// Proxy management endpoints // Proxy management endpoints
app.post('/api/proxy/fetch', async (c) => { app.post('/api/proxy/fetch', async (c) => {
try { try {
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'proxy-fetch', type: 'proxy-fetch',
service: 'proxy', service: 'proxy',
provider: 'proxy-service', provider: 'proxy-service',
operation: 'fetch-and-check', operation: 'fetch-and-check',
payload: {}, payload: {},
priority: 5 priority: 5
}); });
return c.json({ return c.json({
status: 'success', status: 'success',
jobId: job.id, jobId: job.id,
message: 'Proxy fetch job queued' message: 'Proxy fetch job queued'
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue proxy fetch', { error }); logger.error('Failed to queue proxy fetch', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy fetch' }, 500); return c.json({ status: 'error', message: 'Failed to queue proxy fetch' }, 500);
} }
}); });
app.post('/api/proxy/check', async (c) => { app.post('/api/proxy/check', async (c) => {
try { try {
const { proxies } = await c.req.json(); const { proxies } = await c.req.json();
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'proxy-check', type: 'proxy-check',
service: 'proxy', service: 'proxy',
provider: 'proxy-service', provider: 'proxy-service',
operation: 'check-specific', operation: 'check-specific',
payload: { proxies }, payload: { proxies },
priority: 8 priority: 8
}); });
return c.json({ return c.json({
status: 'success', status: 'success',
jobId: job.id, jobId: job.id,
message: `Proxy check job queued for ${proxies.length} proxies` message: `Proxy check job queued for ${proxies.length} proxies`
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue proxy check', { error }); logger.error('Failed to queue proxy check', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy check' }, 500); return c.json({ status: 'error', message: 'Failed to queue proxy check' }, 500);
} }
}); });
// Get proxy stats via queue // Get proxy stats via queue
app.get('/api/proxy/stats', async (c) => { app.get('/api/proxy/stats', async (c) => {
try { try {
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'proxy-stats', type: 'proxy-stats',
service: 'proxy', service: 'proxy',
provider: 'proxy-service', provider: 'proxy-service',
operation: 'get-stats', operation: 'get-stats',
payload: {}, payload: {},
priority: 3 priority: 3
}); });
return c.json({ return c.json({
status: 'success', status: 'success',
jobId: job.id, jobId: job.id,
message: 'Proxy stats job queued' message: 'Proxy stats job queued'
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue proxy stats', { error }); logger.error('Failed to queue proxy stats', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy stats' }, 500); return c.json({ status: 'error', message: 'Failed to queue proxy stats' }, 500);
} }
}); });
// Provider registry endpoints // Provider registry endpoints
app.get('/api/providers', async (c) => { app.get('/api/providers', async (c) => {
try { try {
const providers = queueManager.getRegisteredProviders(); const providers = queueManager.getRegisteredProviders();
return c.json({ status: 'success', providers }); return c.json({ status: 'success', providers });
} catch (error) { } catch (error) {
logger.error('Failed to get providers', { error }); logger.error('Failed to get providers', { error });
return c.json({ status: 'error', message: 'Failed to get providers' }, 500); return c.json({ status: 'error', message: 'Failed to get providers' }, 500);
} }
}); });
// Add new endpoint to see scheduled jobs // Add new endpoint to see scheduled jobs
app.get('/api/scheduled-jobs', async (c) => { app.get('/api/scheduled-jobs', async (c) => {
try { try {
const jobs = queueManager.getScheduledJobsInfo(); const jobs = queueManager.getScheduledJobsInfo();
return c.json({ return c.json({
status: 'success', status: 'success',
count: jobs.length, count: jobs.length,
jobs jobs
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get scheduled jobs info', { error }); logger.error('Failed to get scheduled jobs info', { error });
return c.json({ status: 'error', message: 'Failed to get scheduled jobs' }, 500); return c.json({ status: 'error', message: 'Failed to get scheduled jobs' }, 500);
} }
}); });
// Initialize services // Initialize services
async function initializeServices() { async function initializeServices() {
logger.info('Initializing data service...'); logger.info('Initializing data service...');
try { try {
// Initialize queue service // Initialize queue service
await queueManager.initialize(); await queueManager.initialize();
logger.info('Queue service initialized'); logger.info('Queue service initialized');
logger.info('All services initialized successfully'); logger.info('All services initialized successfully');
} catch (error) { } catch (error) {
logger.error('Failed to initialize services', { error }); logger.error('Failed to initialize services', { error });
throw error; throw error;
} }
} }
// Start server // Start server
async function startServer() { async function startServer() {
await initializeServices(); await initializeServices();
serve({ serve({
fetch: app.fetch, fetch: app.fetch,
port: PORT, port: PORT,
}); });
logger.info(`Data Service started on port ${PORT}`); logger.info(`Data Service started on port ${PORT}`);
logger.info('Available endpoints:'); logger.info('Available endpoints:');
logger.info(' GET /health - Health check'); logger.info(' GET /health - Health check');
logger.info(' GET /api/queue/status - Queue status'); logger.info(' GET /api/queue/status - Queue status');
logger.info(' POST /api/queue/job - Add job to queue'); logger.info(' POST /api/queue/job - Add job to queue');
logger.info(' GET /api/live/:symbol - Live market data'); logger.info(' GET /api/live/:symbol - Live market data');
logger.info(' GET /api/historical/:symbol - Historical market data'); logger.info(' GET /api/historical/:symbol - Historical market data');
logger.info(' POST /api/proxy/fetch - Queue proxy fetch'); logger.info(' POST /api/proxy/fetch - Queue proxy fetch');
logger.info(' POST /api/proxy/check - Queue proxy check'); logger.info(' POST /api/proxy/check - Queue proxy check');
logger.info(' GET /api/providers - List registered providers'); logger.info(' GET /api/providers - List registered providers');
} }
// Graceful shutdown // Graceful shutdown
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
logger.info('Received SIGINT, shutting down gracefully...'); logger.info('Received SIGINT, shutting down gracefully...');
await queueManager.shutdown(); await queueManager.shutdown();
process.exit(0); process.exit(0);
}); });
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {
logger.info('Received SIGTERM, shutting down gracefully...'); logger.info('Received SIGTERM, shutting down gracefully...');
await queueManager.shutdown(); await queueManager.shutdown();
process.exit(0); process.exit(0);
}); });
startServer().catch(error => { startServer().catch(error => {
logger.error('Failed to start server', { error }); logger.error('Failed to start server', { error });
process.exit(1); process.exit(1);
}); });

View file

@ -1,140 +1,140 @@
import { ProxyInfo } from 'libs/http/src/types'; import { ProxyInfo } from 'libs/http/src/types';
import { ProviderConfig } from '../services/provider-registry.service'; import { ProviderConfig } from '../services/provider-registry.service';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { BatchProcessor } from '../utils/batch-processor'; import { BatchProcessor } from '../utils/batch-processor';
// Create logger for this provider // Create logger for this provider
const logger = getLogger('proxy-provider'); const logger = getLogger('proxy-provider');
// This will run at the same time each day as when the app started // This will run at the same time each day as when the app started
const getEvery24HourCron = (): string => { const getEvery24HourCron = (): string => {
const now = new Date(); const now = new Date();
const hours = now.getHours(); const hours = now.getHours();
const minutes = now.getMinutes(); const minutes = now.getMinutes();
return `${minutes} ${hours} * * *`; // Every day at startup time return `${minutes} ${hours} * * *`; // Every day at startup time
}; };
export const proxyProvider: ProviderConfig = { export const proxyProvider: ProviderConfig = {
name: 'proxy-service', name: 'proxy-service',
service: 'proxy', service: 'proxy',
operations: { operations: {
'fetch-and-check': async (payload: { sources?: string[] }) => { 'fetch-and-check': async (payload: { sources?: string[] }) => {
const { proxyService } = await import('./proxy.tasks'); const { proxyService } = await import('./proxy.tasks');
const { queueManager } = await import('../services/queue.service'); const { queueManager } = await import('../services/queue.service');
const proxies = await proxyService.fetchProxiesFromSources(); const proxies = await proxyService.fetchProxiesFromSources();
if (proxies.length === 0) { if (proxies.length === 0) {
return { proxiesFetched: 0, jobsCreated: 0 }; return { proxiesFetched: 0, jobsCreated: 0 };
} }
const batchProcessor = new BatchProcessor(queueManager); const batchProcessor = new BatchProcessor(queueManager);
// Simplified configuration // Simplified configuration
const result = await batchProcessor.processItems({ const result = await batchProcessor.processItems({
items: proxies, items: proxies,
batchSize: parseInt(process.env.PROXY_BATCH_SIZE || '200'), batchSize: parseInt(process.env.PROXY_BATCH_SIZE || '200'),
totalDelayMs: parseInt(process.env.PROXY_VALIDATION_HOURS || '4') * 60 * 60 * 1000 , totalDelayMs: parseInt(process.env.PROXY_VALIDATION_HOURS || '4') * 60 * 60 * 1000 ,
jobNamePrefix: 'proxy', jobNamePrefix: 'proxy',
operation: 'check-proxy', operation: 'check-proxy',
service: 'proxy', service: 'proxy',
provider: 'proxy-service', provider: 'proxy-service',
priority: 2, priority: 2,
useBatching: process.env.PROXY_DIRECT_MODE !== 'true', // Simple boolean flag useBatching: process.env.PROXY_DIRECT_MODE !== 'true', // Simple boolean flag
createJobData: (proxy: ProxyInfo) => ({ createJobData: (proxy: ProxyInfo) => ({
proxy, proxy,
source: 'fetch-and-check' source: 'fetch-and-check'
}), }),
removeOnComplete: 5, removeOnComplete: 5,
removeOnFail: 3 removeOnFail: 3
}); });
return { return {
proxiesFetched: result.totalItems, proxiesFetched: result.totalItems,
...result ...result
}; };
}, },
'process-proxy-batch': async (payload: any) => { 'process-proxy-batch': async (payload: any) => {
// Process a batch of proxies - uses the fetch-and-check JobNamePrefix process-(proxy)-batch // Process a batch of proxies - uses the fetch-and-check JobNamePrefix process-(proxy)-batch
const { queueManager } = await import('../services/queue.service'); const { queueManager } = await import('../services/queue.service');
const batchProcessor = new BatchProcessor(queueManager); const batchProcessor = new BatchProcessor(queueManager);
return await batchProcessor.processBatch( return await batchProcessor.processBatch(
payload, payload,
(proxy: ProxyInfo) => ({ (proxy: ProxyInfo) => ({
proxy, proxy,
source: payload.config?.source || 'batch-processing' source: payload.config?.source || 'batch-processing'
}) })
); );
}, },
'check-proxy': async (payload: { 'check-proxy': async (payload: {
proxy: ProxyInfo, proxy: ProxyInfo,
source?: string, source?: string,
batchIndex?: number, batchIndex?: number,
itemIndex?: number, itemIndex?: number,
total?: number total?: number
}) => { }) => {
const { checkProxy } = await import('./proxy.tasks'); const { checkProxy } = await import('./proxy.tasks');
try { try {
const result = await checkProxy(payload.proxy); const result = await checkProxy(payload.proxy);
logger.debug('Proxy validated', { logger.debug('Proxy validated', {
proxy: `${payload.proxy.host}:${payload.proxy.port}`, proxy: `${payload.proxy.host}:${payload.proxy.port}`,
isWorking: result.isWorking, isWorking: result.isWorking,
responseTime: result.responseTime, responseTime: result.responseTime,
batchIndex: payload.batchIndex batchIndex: payload.batchIndex
}); });
return { return {
result, result,
proxy: payload.proxy, proxy: payload.proxy,
// Only include batch info if it exists (for batch mode) // Only include batch info if it exists (for batch mode)
...(payload.batchIndex !== undefined && { ...(payload.batchIndex !== undefined && {
batchInfo: { batchInfo: {
batchIndex: payload.batchIndex, batchIndex: payload.batchIndex,
itemIndex: payload.itemIndex, itemIndex: payload.itemIndex,
total: payload.total, total: payload.total,
source: payload.source source: payload.source
} }
}) })
}; };
} catch (error) { } catch (error) {
logger.warn('Proxy validation failed', { logger.warn('Proxy validation failed', {
proxy: `${payload.proxy.host}:${payload.proxy.port}`, proxy: `${payload.proxy.host}:${payload.proxy.port}`,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
batchIndex: payload.batchIndex batchIndex: payload.batchIndex
}); });
return { return {
result: { isWorking: false, error: String(error) }, result: { isWorking: false, error: String(error) },
proxy: payload.proxy, proxy: payload.proxy,
// Only include batch info if it exists (for batch mode) // Only include batch info if it exists (for batch mode)
...(payload.batchIndex !== undefined && { ...(payload.batchIndex !== undefined && {
batchInfo: { batchInfo: {
batchIndex: payload.batchIndex, batchIndex: payload.batchIndex,
itemIndex: payload.itemIndex, itemIndex: payload.itemIndex,
total: payload.total, total: payload.total,
source: payload.source source: payload.source
} }
}) })
}; };
} }
} }
}, },
scheduledJobs: [ scheduledJobs: [
{ {
type: 'proxy-maintenance', type: 'proxy-maintenance',
operation: 'fetch-and-check', operation: 'fetch-and-check',
payload: {}, payload: {},
// should remove and just run at the same time so app restarts dont keeping adding same jobs // should remove and just run at the same time so app restarts dont keeping adding same jobs
cronPattern: getEvery24HourCron(), cronPattern: getEvery24HourCron(),
priority: 5, priority: 5,
immediately: true, immediately: true,
description: 'Fetch and validate proxy list from sources' description: 'Fetch and validate proxy list from sources'
} }
] ]
}; };

View file

@ -1,264 +1,264 @@
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import createCache, { type CacheProvider } from '@stock-bot/cache'; import createCache, { type CacheProvider } from '@stock-bot/cache';
import { HttpClient, ProxyInfo } from '@stock-bot/http'; import { HttpClient, ProxyInfo } from '@stock-bot/http';
import pLimit from 'p-limit'; import pLimit from 'p-limit';
// Shared configuration and utilities // Shared configuration and utilities
const PROXY_CONFIG = { const PROXY_CONFIG = {
CACHE_KEY: 'proxy', CACHE_KEY: 'proxy',
CACHE_TTL: 86400, // 24 hours CACHE_TTL: 86400, // 24 hours
CHECK_TIMEOUT: 7000, CHECK_TIMEOUT: 7000,
CHECK_IP: '99.246.102.205', CHECK_IP: '99.246.102.205',
CHECK_URL: 'https://proxy-detection.stare.gg/?api_key=bd406bf53ddc6abe1d9de5907830a955', CHECK_URL: 'https://proxy-detection.stare.gg/?api_key=bd406bf53ddc6abe1d9de5907830a955',
CONCURRENCY_LIMIT: 100, CONCURRENCY_LIMIT: 100,
PROXY_SOURCES: [ PROXY_SOURCES: [
{url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/http',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/http',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/proxies.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/proxies.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/http/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/http/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/http.txt', protocol: 'http' }, {url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/http.txt', protocol: 'http' },
{url: 'https://raw.githubusercontent.com/dpangestuw/Free-Proxy/refs/heads/main/http_proxies.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/dpangestuw/Free-Proxy/refs/heads/main/http_proxies.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/gitrecon1455/fresh-proxy-list/refs/heads/main/proxylist.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/gitrecon1455/fresh-proxy-list/refs/heads/main/proxylist.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/themiralay/Proxy-List-World/refs/heads/master/data.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/themiralay/Proxy-List-World/refs/heads/master/data.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/casa-ls/proxy-list/refs/heads/main/http',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/casa-ls/proxy-list/refs/heads/main/http',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/http.txt', protocol: 'http' }, {url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/http.txt', protocol: 'http' },
{url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt',protocol: 'http', },
{url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/http.txt',protocol: 'http', }, {url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/http.txt',protocol: 'http', },
// {url: 'https://raw.githubusercontent.com/r00tee/Proxy-List/refs/heads/main/Https.txt',protocol: 'https', }, // {url: 'https://raw.githubusercontent.com/r00tee/Proxy-List/refs/heads/main/Https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt',protocol: 'https', }, // {url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/https.txt', protocol: 'https' }, // {url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/https.txt', protocol: 'https' },
// {url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/https.txt',protocol: 'https', }, // {url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/https/https.txt',protocol: 'https', }, // {url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/https/https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/zloi-user/hideip.me/refs/heads/master/https.txt',protocol: 'https', }, // {url: 'https://raw.githubusercontent.com/zloi-user/hideip.me/refs/heads/master/https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/https.txt',protocol: 'https', }, // {url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/https.txt',protocol: 'https', },
] ]
}; };
// Shared instances (module-scoped, not global) // Shared instances (module-scoped, not global)
let logger: ReturnType<typeof getLogger>; let logger: ReturnType<typeof getLogger>;
let cache: CacheProvider; let cache: CacheProvider;
let httpClient: HttpClient; let httpClient: HttpClient;
let concurrencyLimit: ReturnType<typeof pLimit>; let concurrencyLimit: ReturnType<typeof pLimit>;
// Initialize shared resources // Initialize shared resources
function initializeSharedResources() { function initializeSharedResources() {
if (!logger) { if (!logger) {
logger = getLogger('proxy-tasks'); logger = getLogger('proxy-tasks');
cache = createCache('hybrid'); cache = createCache('hybrid');
httpClient = new HttpClient({ timeout: 10000 }, logger); httpClient = new HttpClient({ timeout: 10000 }, logger);
concurrencyLimit = pLimit(PROXY_CONFIG.CONCURRENCY_LIMIT); concurrencyLimit = pLimit(PROXY_CONFIG.CONCURRENCY_LIMIT);
logger.info('Proxy tasks initialized'); logger.info('Proxy tasks initialized');
} }
} }
// Individual task functions // Individual task functions
export async function queueProxyFetch(): Promise<string> { export async function queueProxyFetch(): Promise<string> {
initializeSharedResources(); initializeSharedResources();
const { queueManager } = await import('../services/queue.service'); const { queueManager } = await import('../services/queue.service');
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'proxy-fetch', type: 'proxy-fetch',
service: 'proxy', service: 'proxy',
provider: 'proxy-service', provider: 'proxy-service',
operation: 'fetch-and-check', operation: 'fetch-and-check',
payload: {}, payload: {},
priority: 5 priority: 5
}); });
const jobId = job.id || 'unknown'; const jobId = job.id || 'unknown';
logger.info('Proxy fetch job queued', { jobId }); logger.info('Proxy fetch job queued', { jobId });
return jobId; return jobId;
} }
export async function queueProxyCheck(proxies: ProxyInfo[]): Promise<string> { export async function queueProxyCheck(proxies: ProxyInfo[]): Promise<string> {
initializeSharedResources(); initializeSharedResources();
const { queueManager } = await import('../services/queue.service'); const { queueManager } = await import('../services/queue.service');
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'proxy-check', type: 'proxy-check',
service: 'proxy', service: 'proxy',
provider: 'proxy-service', provider: 'proxy-service',
operation: 'check-specific', operation: 'check-specific',
payload: { proxies }, payload: { proxies },
priority: 3 priority: 3
}); });
const jobId = job.id || 'unknown'; const jobId = job.id || 'unknown';
logger.info('Proxy check job queued', { jobId, count: proxies.length }); logger.info('Proxy check job queued', { jobId, count: proxies.length });
return jobId; return jobId;
} }
export async function fetchProxiesFromSources(): Promise<ProxyInfo[]> { export async function fetchProxiesFromSources(): Promise<ProxyInfo[]> {
initializeSharedResources(); initializeSharedResources();
const sources = PROXY_CONFIG.PROXY_SOURCES.map(source => const sources = PROXY_CONFIG.PROXY_SOURCES.map(source =>
concurrencyLimit(() => fetchProxiesFromSource(source)) concurrencyLimit(() => fetchProxiesFromSource(source))
); );
const result = await Promise.all(sources); const result = await Promise.all(sources);
let allProxies: ProxyInfo[] = result.flat(); let allProxies: ProxyInfo[] = result.flat();
allProxies = removeDuplicateProxies(allProxies); allProxies = removeDuplicateProxies(allProxies);
// await checkProxies(allProxies); // await checkProxies(allProxies);
return allProxies; return allProxies;
} }
export async function fetchProxiesFromSource(source: { url: string; protocol: string }): Promise<ProxyInfo[]> { export async function fetchProxiesFromSource(source: { url: string; protocol: string }): Promise<ProxyInfo[]> {
initializeSharedResources(); initializeSharedResources();
const allProxies: ProxyInfo[] = []; const allProxies: ProxyInfo[] = [];
try { try {
logger.info(`Fetching proxies from ${source.url}`); logger.info(`Fetching proxies from ${source.url}`);
const response = await httpClient.get(source.url, { const response = await httpClient.get(source.url, {
timeout: 10000 timeout: 10000
}); });
if (response.status !== 200) { if (response.status !== 200) {
logger.warn(`Failed to fetch from ${source.url}: ${response.status}`); logger.warn(`Failed to fetch from ${source.url}: ${response.status}`);
return []; return [];
} }
const text = response.data; const text = response.data;
const lines = text.split('\n').filter((line: string) => line.trim()); const lines = text.split('\n').filter((line: string) => line.trim());
for (const line of lines) { for (const line of lines) {
let trimmed = line.trim(); let trimmed = line.trim();
trimmed = cleanProxyUrl(trimmed); trimmed = cleanProxyUrl(trimmed);
if (!trimmed || trimmed.startsWith('#')) continue; if (!trimmed || trimmed.startsWith('#')) continue;
// Parse formats like "host:port" or "host:port:user:pass" // Parse formats like "host:port" or "host:port:user:pass"
const parts = trimmed.split(':'); const parts = trimmed.split(':');
if (parts.length >= 2) { if (parts.length >= 2) {
const proxy: ProxyInfo = { const proxy: ProxyInfo = {
protocol: source.protocol as 'http' | 'https' | 'socks4' | 'socks5', protocol: source.protocol as 'http' | 'https' | 'socks4' | 'socks5',
host: parts[0], host: parts[0],
port: parseInt(parts[1]) port: parseInt(parts[1])
}; };
if (!isNaN(proxy.port) && proxy.host) { if (!isNaN(proxy.port) && proxy.host) {
allProxies.push(proxy); allProxies.push(proxy);
} }
} }
} }
logger.info(`Parsed ${allProxies.length} proxies from ${source.url}`); logger.info(`Parsed ${allProxies.length} proxies from ${source.url}`);
} catch (error) { } catch (error) {
logger.error(`Error fetching proxies from ${source.url}`, error); logger.error(`Error fetching proxies from ${source.url}`, error);
return []; return [];
} }
return allProxies; return allProxies;
} }
/** /**
* Check if a proxy is working * Check if a proxy is working
*/ */
export async function checkProxy(proxy: ProxyInfo): Promise<ProxyInfo> { export async function checkProxy(proxy: ProxyInfo): Promise<ProxyInfo> {
initializeSharedResources(); initializeSharedResources();
let success = false; let success = false;
logger.debug(`Checking Proxy:`, { logger.debug(`Checking Proxy:`, {
protocol: proxy.protocol, protocol: proxy.protocol,
host: proxy.host, host: proxy.host,
port: proxy.port, port: proxy.port,
}); });
try { try {
// Test the proxy // Test the proxy
const response = await httpClient.get(PROXY_CONFIG.CHECK_URL, { const response = await httpClient.get(PROXY_CONFIG.CHECK_URL, {
proxy, proxy,
timeout: PROXY_CONFIG.CHECK_TIMEOUT timeout: PROXY_CONFIG.CHECK_TIMEOUT
}); });
const isWorking = response.status >= 200 && response.status < 300; const isWorking = response.status >= 200 && response.status < 300;
const result: ProxyInfo = { const result: ProxyInfo = {
...proxy, ...proxy,
isWorking, isWorking,
checkedAt: new Date(), checkedAt: new Date(),
responseTime: response.responseTime, responseTime: response.responseTime,
}; };
if (isWorking && !JSON.stringify(response.data).includes(PROXY_CONFIG.CHECK_IP)) { if (isWorking && !JSON.stringify(response.data).includes(PROXY_CONFIG.CHECK_IP)) {
success = true; success = true;
await cache.set(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`, result, PROXY_CONFIG.CACHE_TTL); await cache.set(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`, result, PROXY_CONFIG.CACHE_TTL);
} else { } else {
await cache.del(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`); await cache.del(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`);
} }
logger.debug('Proxy check completed', { logger.debug('Proxy check completed', {
host: proxy.host, host: proxy.host,
port: proxy.port, port: proxy.port,
isWorking, isWorking,
}); });
return result; return result;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
const result: ProxyInfo = { const result: ProxyInfo = {
...proxy, ...proxy,
isWorking: false, isWorking: false,
error: errorMessage, error: errorMessage,
checkedAt: new Date() checkedAt: new Date()
}; };
// If the proxy check failed, remove it from cache - success is here cause i think abort signal fails sometimes // If the proxy check failed, remove it from cache - success is here cause i think abort signal fails sometimes
if (!success) { if (!success) {
await cache.del(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`); await cache.del(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`);
} }
logger.debug('Proxy check failed', { logger.debug('Proxy check failed', {
host: proxy.host, host: proxy.host,
port: proxy.port, port: proxy.port,
error: errorMessage error: errorMessage
}); });
return result; return result;
} }
} }
// Utility functions // Utility functions
function cleanProxyUrl(url: string): string { function cleanProxyUrl(url: string): string {
return url return url
.replace(/^https?:\/\//, '') .replace(/^https?:\/\//, '')
.replace(/^0+/, '') .replace(/^0+/, '')
.replace(/:0+(\d)/g, ':$1'); .replace(/:0+(\d)/g, ':$1');
} }
function removeDuplicateProxies(proxies: ProxyInfo[]): ProxyInfo[] { function removeDuplicateProxies(proxies: ProxyInfo[]): ProxyInfo[] {
const seen = new Set<string>(); const seen = new Set<string>();
const unique: ProxyInfo[] = []; const unique: ProxyInfo[] = [];
for (const proxy of proxies) { for (const proxy of proxies) {
const key = `${proxy.protocol}://${proxy.host}:${proxy.port}`; const key = `${proxy.protocol}://${proxy.host}:${proxy.port}`;
if (!seen.has(key)) { if (!seen.has(key)) {
seen.add(key); seen.add(key);
unique.push(proxy); unique.push(proxy);
} }
} }
return unique; return unique;
} }
// Optional: Export a convenience object that groups related tasks // Optional: Export a convenience object that groups related tasks
export const proxyTasks = { export const proxyTasks = {
queueProxyFetch, queueProxyFetch,
queueProxyCheck, queueProxyCheck,
fetchProxiesFromSources, fetchProxiesFromSources,
fetchProxiesFromSource, fetchProxiesFromSource,
checkProxy, checkProxy,
}; };
// Export singleton instance for backward compatibility (optional) // Export singleton instance for backward compatibility (optional)
// Remove this if you want to fully move to the task-based approach // Remove this if you want to fully move to the task-based approach
export const proxyService = proxyTasks; export const proxyService = proxyTasks;

View file

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

View file

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

View file

@ -1,24 +1,24 @@
import { proxyService } from './providers/proxy.tasks'; import { proxyService } from './providers/proxy.tasks';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
// Initialize logger for the demo // Initialize logger for the demo
const logger = getLogger('proxy-demo'); const logger = getLogger('proxy-demo');
console.log('🔧 Starting proxy demo...'); console.log('🔧 Starting proxy demo...');
/** /**
* Example: Custom proxy source with enhanced logging * Example: Custom proxy source with enhanced logging
*/ */
async function demonstrateCustomProxySource() { async function demonstrateCustomProxySource() {
console.log('🔧 Demonstrating!'); console.log('🔧 Demonstrating!');
logger.info('🔧 Demonstrating custom proxy source...'); logger.info('🔧 Demonstrating custom proxy source...');
try { try {
console.log('🔧 Demonstrating 1'); console.log('🔧 Demonstrating 1');
await proxyService.fetchProxiesFromSources(); await proxyService.fetchProxiesFromSources();
console.log('🔧 Demonstrating custom proxy source is DONE!'); console.log('🔧 Demonstrating custom proxy source is DONE!');
} catch (error) { } catch (error) {
logger.error('❌ Custom source scraping failed',{ logger.error('❌ Custom source scraping failed',{
error: error error: error
}); });
} }
} }
demonstrateCustomProxySource() demonstrateCustomProxySource()

View file

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

View file

@ -1,478 +1,478 @@
import { Queue, Worker, QueueEvents } from 'bullmq'; import { Queue, Worker, QueueEvents } from 'bullmq';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { providerRegistry } from './provider-registry.service'; import { providerRegistry } from './provider-registry.service';
export interface JobData { export interface JobData {
type: string; type: string;
service: string; service: string;
provider: string; provider: string;
operation: string; operation: string;
payload: any; payload: any;
priority?: number; priority?: number;
immediately?: boolean; immediately?: boolean;
} }
export class QueueService { export class QueueService {
private logger = getLogger('queue-service'); private logger = getLogger('queue-service');
private queue!: Queue; private queue!: Queue;
private workers: Worker[] = []; private workers: Worker[] = [];
private queueEvents!: QueueEvents; private queueEvents!: QueueEvents;
private isInitialized = false; private isInitialized = false;
constructor() { constructor() {
// Don't initialize in constructor to allow for proper async initialization // Don't initialize in constructor to allow for proper async initialization
} }
async initialize() { async initialize() {
if (this.isInitialized) { if (this.isInitialized) {
this.logger.warn('Queue service already initialized'); this.logger.warn('Queue service already initialized');
return; return;
} }
this.logger.info('Initializing queue service...'); this.logger.info('Initializing queue service...');
// Register all providers first // Register all providers first
await this.registerProviders(); await this.registerProviders();
const connection = { const connection = {
host: process.env.DRAGONFLY_HOST || 'localhost', host: process.env.DRAGONFLY_HOST || 'localhost',
port: parseInt(process.env.DRAGONFLY_PORT || '6379'), port: parseInt(process.env.DRAGONFLY_PORT || '6379'),
// Add these Redis-specific options to fix the undeclared key issue // Add these Redis-specific options to fix the undeclared key issue
maxRetriesPerRequest: null, maxRetriesPerRequest: null,
retryDelayOnFailover: 100, retryDelayOnFailover: 100,
enableReadyCheck: false, enableReadyCheck: false,
lazyConnect: true, lazyConnect: true,
// Disable Redis Cluster mode if you're using standalone Redis/Dragonfly // Disable Redis Cluster mode if you're using standalone Redis/Dragonfly
enableOfflineQueue: false enableOfflineQueue: false
}; };
// Worker configuration // Worker configuration
const workerCount = parseInt(process.env.WORKER_COUNT || '5'); const workerCount = parseInt(process.env.WORKER_COUNT || '5');
const concurrencyPerWorker = parseInt(process.env.WORKER_CONCURRENCY || '20'); const concurrencyPerWorker = parseInt(process.env.WORKER_CONCURRENCY || '20');
this.logger.info('Connecting to Redis/Dragonfly', connection); this.logger.info('Connecting to Redis/Dragonfly', connection);
try { try {
this.queue = new Queue('{data-service-queue}', { this.queue = new Queue('{data-service-queue}', {
connection, connection,
defaultJobOptions: { defaultJobOptions: {
removeOnComplete: 10, removeOnComplete: 10,
removeOnFail: 5, removeOnFail: 5,
attempts: 3, attempts: 3,
backoff: { backoff: {
type: 'exponential', type: 'exponential',
delay: 1000, delay: 1000,
} }
} }
}); });
// Create multiple workers // Create multiple workers
for (let i = 0; i < workerCount; i++) { for (let i = 0; i < workerCount; i++) {
const worker = new Worker( const worker = new Worker(
'{data-service-queue}', '{data-service-queue}',
this.processJob.bind(this), this.processJob.bind(this),
{ {
connection: { ...connection }, // Each worker gets its own connection connection: { ...connection }, // Each worker gets its own connection
concurrency: concurrencyPerWorker, concurrency: concurrencyPerWorker,
maxStalledCount: 1, maxStalledCount: 1,
stalledInterval: 30000, stalledInterval: 30000,
} }
); );
// Add worker-specific logging // Add worker-specific logging
worker.on('ready', () => { worker.on('ready', () => {
this.logger.info(`Worker ${i + 1} ready`, { workerId: i + 1 }); this.logger.info(`Worker ${i + 1} ready`, { workerId: i + 1 });
}); });
worker.on('error', (error) => { worker.on('error', (error) => {
this.logger.error(`Worker ${i + 1} error`, { workerId: i + 1, error }); this.logger.error(`Worker ${i + 1} error`, { workerId: i + 1, error });
}); });
this.workers.push(worker); this.workers.push(worker);
} }
this.queueEvents = new QueueEvents('{data-service-queue}', { connection }); // Test connection this.queueEvents = new QueueEvents('{data-service-queue}', { connection }); // Test connection
// Wait for all workers to be ready // Wait for all workers to be ready
await this.queue.waitUntilReady(); await this.queue.waitUntilReady();
await Promise.all(this.workers.map(worker => worker.waitUntilReady())); await Promise.all(this.workers.map(worker => worker.waitUntilReady()));
await this.queueEvents.waitUntilReady(); await this.queueEvents.waitUntilReady();
this.setupEventListeners(); this.setupEventListeners();
this.isInitialized = true; this.isInitialized = true;
this.logger.info('Queue service initialized successfully'); this.logger.info('Queue service initialized successfully');
await this.setupScheduledTasks(); await this.setupScheduledTasks();
} catch (error) { } catch (error) {
this.logger.error('Failed to initialize queue service', { error }); this.logger.error('Failed to initialize queue service', { error });
throw error; throw error;
} }
} }
// Update getTotalConcurrency method // Update getTotalConcurrency method
getTotalConcurrency() { getTotalConcurrency() {
if (!this.isInitialized) { if (!this.isInitialized) {
return 0; return 0;
} }
return this.workers.reduce((total, worker) => { return this.workers.reduce((total, worker) => {
return total + (worker.opts.concurrency || 1); return total + (worker.opts.concurrency || 1);
}, 0); }, 0);
} }
private async registerProviders() { private async registerProviders() {
this.logger.info('Registering providers...'); this.logger.info('Registering providers...');
try { try {
// Import and register all providers // Import and register all providers
const { proxyProvider } = await import('../providers/proxy.provider'); const { proxyProvider } = await import('../providers/proxy.provider');
const { quotemediaProvider } = await import('../providers/quotemedia.provider'); const { quotemediaProvider } = await import('../providers/quotemedia.provider');
const { yahooProvider } = await import('../providers/yahoo.provider'); const { yahooProvider } = await import('../providers/yahoo.provider');
providerRegistry.registerProvider(proxyProvider); providerRegistry.registerProvider(proxyProvider);
providerRegistry.registerProvider(quotemediaProvider); providerRegistry.registerProvider(quotemediaProvider);
providerRegistry.registerProvider(yahooProvider); providerRegistry.registerProvider(yahooProvider);
this.logger.info('All providers registered successfully'); this.logger.info('All providers registered successfully');
} catch (error) { } catch (error) {
this.logger.error('Failed to register providers', { error }); this.logger.error('Failed to register providers', { error });
throw error; throw error;
} }
} }
private async processJob(job: any) { private async processJob(job: any) {
const { service, provider, operation, payload }: JobData = job.data; const { service, provider, operation, payload }: JobData = job.data;
this.logger.info('Processing job', { this.logger.info('Processing job', {
id: job.id, id: job.id,
service, service,
provider, provider,
operation, operation,
payloadKeys: Object.keys(payload || {}) payloadKeys: Object.keys(payload || {})
}); });
try { try {
// Get handler from registry // Get handler from registry
const handler = providerRegistry.getHandler(service, provider, operation); const handler = providerRegistry.getHandler(service, provider, operation);
if (!handler) { if (!handler) {
throw new Error(`No handler found for ${service}:${provider}:${operation}`); throw new Error(`No handler found for ${service}:${provider}:${operation}`);
} }
// Execute the handler // Execute the handler
const result = await handler(payload); const result = await handler(payload);
this.logger.info('Job completed successfully', { this.logger.info('Job completed successfully', {
id: job.id, id: job.id,
service, service,
provider, provider,
operation operation
}); });
return result; return result;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('Job failed', { this.logger.error('Job failed', {
id: job.id, id: job.id,
service, service,
provider, provider,
operation, operation,
error: errorMessage error: errorMessage
}); });
throw error; throw error;
} }
} }
async addBulk(jobs: any[]) : Promise<any[]> { async addBulk(jobs: any[]) : Promise<any[]> {
return await this.queue.addBulk(jobs) return await this.queue.addBulk(jobs)
} }
private setupEventListeners() { private setupEventListeners() {
this.queueEvents.on('completed', (job) => { this.queueEvents.on('completed', (job) => {
this.logger.info('Job completed', { id: job.jobId }); this.logger.info('Job completed', { id: job.jobId });
}); });
this.queueEvents.on('failed', (job) => { this.queueEvents.on('failed', (job) => {
this.logger.error('Job failed', { id: job.jobId, error: job.failedReason }); this.logger.error('Job failed', { id: job.jobId, error: job.failedReason });
}); });
// Note: Worker-specific events are already set up during worker creation // Note: Worker-specific events are already set up during worker creation
// No need for additional progress events since we handle them per-worker // No need for additional progress events since we handle them per-worker
} }
private async setupScheduledTasks() { private async setupScheduledTasks() {
try { try {
this.logger.info('Setting up scheduled tasks from providers...'); this.logger.info('Setting up scheduled tasks from providers...');
// Get all scheduled jobs from all providers // Get all scheduled jobs from all providers
const allScheduledJobs = providerRegistry.getAllScheduledJobs(); const allScheduledJobs = providerRegistry.getAllScheduledJobs();
if (allScheduledJobs.length === 0) { if (allScheduledJobs.length === 0) {
this.logger.warn('No scheduled jobs found in providers'); this.logger.warn('No scheduled jobs found in providers');
return; return;
} }
// Get existing repeatable jobs for comparison // Get existing repeatable jobs for comparison
const existingJobs = await this.queue.getRepeatableJobs(); const existingJobs = await this.queue.getRepeatableJobs();
this.logger.info(`Found ${existingJobs.length} existing repeatable jobs`); this.logger.info(`Found ${existingJobs.length} existing repeatable jobs`);
let successCount = 0; let successCount = 0;
let failureCount = 0; let failureCount = 0;
let updatedCount = 0; let updatedCount = 0;
let newCount = 0; let newCount = 0;
// Process each scheduled job // Process each scheduled job
for (const { service, provider, job } of allScheduledJobs) { for (const { service, provider, job } of allScheduledJobs) {
try { try {
const jobKey = `${service}-${provider}-${job.operation}`; const jobKey = `${service}-${provider}-${job.operation}`;
// Check if this job already exists // Check if this job already exists
const existingJob = existingJobs.find(existing => const existingJob = existingJobs.find(existing =>
existing.key?.includes(jobKey) || existing.name === job.type existing.key?.includes(jobKey) || existing.name === job.type
); );
if (existingJob) { if (existingJob) {
// Check if the job needs updating (different cron pattern or config) // Check if the job needs updating (different cron pattern or config)
const needsUpdate = existingJob.pattern !== job.cronPattern; const needsUpdate = existingJob.pattern !== job.cronPattern;
if (needsUpdate) { if (needsUpdate) {
this.logger.info('Job configuration changed, updating', { this.logger.info('Job configuration changed, updating', {
jobKey, jobKey,
oldPattern: existingJob.pattern, oldPattern: existingJob.pattern,
newPattern: job.cronPattern newPattern: job.cronPattern
}); });
updatedCount++; updatedCount++;
} else { } else {
this.logger.debug('Job unchanged, skipping', { jobKey }); this.logger.debug('Job unchanged, skipping', { jobKey });
successCount++; successCount++;
continue; continue;
} }
} else { } else {
newCount++; newCount++;
} }
// Add delay between job registrations // Add delay between job registrations
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
await this.addRecurringJob({ await this.addRecurringJob({
type: job.type, type: job.type,
service: service, service: service,
provider: provider, provider: provider,
operation: job.operation, operation: job.operation,
payload: job.payload, payload: job.payload,
priority: job.priority, priority: job.priority,
immediately: job.immediately || false immediately: job.immediately || false
}, job.cronPattern); }, job.cronPattern);
this.logger.info('Scheduled job registered', { this.logger.info('Scheduled job registered', {
type: job.type, type: job.type,
service, service,
provider, provider,
operation: job.operation, operation: job.operation,
cronPattern: job.cronPattern, cronPattern: job.cronPattern,
description: job.description, description: job.description,
immediately: job.immediately || false immediately: job.immediately || false
}); });
successCount++; successCount++;
} catch (error) { } catch (error) {
this.logger.error('Failed to register scheduled job', { this.logger.error('Failed to register scheduled job', {
type: job.type, type: job.type,
service, service,
provider, provider,
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
}); });
failureCount++; failureCount++;
} }
} }
this.logger.info(`Scheduled tasks setup complete`, { this.logger.info(`Scheduled tasks setup complete`, {
total: allScheduledJobs.length, total: allScheduledJobs.length,
successful: successCount, successful: successCount,
failed: failureCount, failed: failureCount,
updated: updatedCount, updated: updatedCount,
new: newCount new: newCount
}); });
} catch (error) { } catch (error) {
this.logger.error('Failed to setup scheduled tasks', error); this.logger.error('Failed to setup scheduled tasks', error);
} }
} }
async addJob(jobData: JobData, options?: any) { async addJob(jobData: JobData, options?: any) {
if (!this.isInitialized) { if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.'); throw new Error('Queue service not initialized. Call initialize() first.');
} }
return this.queue.add(jobData.type, jobData, { return this.queue.add(jobData.type, jobData, {
priority: jobData.priority || 0, priority: jobData.priority || 0,
removeOnComplete: 10, removeOnComplete: 10,
removeOnFail: 5, removeOnFail: 5,
...options ...options
}); });
} }
async addRecurringJob(jobData: JobData, cronPattern: string, options?: any) { async addRecurringJob(jobData: JobData, cronPattern: string, options?: any) {
if (!this.isInitialized) { if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.'); throw new Error('Queue service not initialized. Call initialize() first.');
} }
try { try {
// Create a unique job key for this specific job // Create a unique job key for this specific job
const jobKey = `${jobData.service}-${jobData.provider}-${jobData.operation}`; const jobKey = `${jobData.service}-${jobData.provider}-${jobData.operation}`;
// Get all existing repeatable jobs // Get all existing repeatable jobs
const existingJobs = await this.queue.getRepeatableJobs(); const existingJobs = await this.queue.getRepeatableJobs();
// Find and remove the existing job with the same key if it exists // Find and remove the existing job with the same key if it exists
const existingJob = existingJobs.find(job => { const existingJob = existingJobs.find(job => {
// Check if this is the same job by comparing key components // Check if this is the same job by comparing key components
return job.key?.includes(jobKey) || job.name === jobData.type; return job.key?.includes(jobKey) || job.name === jobData.type;
}); });
if (existingJob) { if (existingJob) {
this.logger.info('Updating existing recurring job', { this.logger.info('Updating existing recurring job', {
jobKey, jobKey,
existingPattern: existingJob.pattern, existingPattern: existingJob.pattern,
newPattern: cronPattern newPattern: cronPattern
}); });
// Remove the existing job // Remove the existing job
await this.queue.removeRepeatableByKey(existingJob.key); await this.queue.removeRepeatableByKey(existingJob.key);
// Small delay to ensure cleanup is complete // Small delay to ensure cleanup is complete
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
} else { } else {
this.logger.info('Creating new recurring job', { jobKey, cronPattern }); this.logger.info('Creating new recurring job', { jobKey, cronPattern });
} }
// Add the new/updated recurring job // Add the new/updated recurring job
const job = await this.queue.add(jobData.type, jobData, { const job = await this.queue.add(jobData.type, jobData, {
repeat: { repeat: {
pattern: cronPattern, pattern: cronPattern,
tz: 'UTC', tz: 'UTC',
immediately: jobData.immediately || false, immediately: jobData.immediately || false,
}, },
// Use a consistent jobId for this specific recurring job // Use a consistent jobId for this specific recurring job
jobId: `recurring-${jobKey}`, jobId: `recurring-${jobKey}`,
removeOnComplete: 1, removeOnComplete: 1,
removeOnFail: 1, removeOnFail: 1,
attempts: 2, attempts: 2,
backoff: { backoff: {
type: 'fixed', type: 'fixed',
delay: 5000 delay: 5000
}, },
...options ...options
}); });
this.logger.info('Recurring job added/updated successfully', { this.logger.info('Recurring job added/updated successfully', {
jobKey, jobKey,
type: jobData.type, type: jobData.type,
cronPattern, cronPattern,
immediately: jobData.immediately || false immediately: jobData.immediately || false
}); });
return job; return job;
} catch (error) { } catch (error) {
this.logger.error('Failed to add/update recurring job', { this.logger.error('Failed to add/update recurring job', {
jobData, jobData,
cronPattern, cronPattern,
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
}); });
throw error; throw error;
} }
} }
async getJobStats() { async getJobStats() {
if (!this.isInitialized) { if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.'); throw new Error('Queue service not initialized. Call initialize() first.');
} }
const [waiting, active, completed, failed, delayed] = await Promise.all([ const [waiting, active, completed, failed, delayed] = await Promise.all([
this.queue.getWaiting(), this.queue.getWaiting(),
this.queue.getActive(), this.queue.getActive(),
this.queue.getCompleted(), this.queue.getCompleted(),
this.queue.getFailed(), this.queue.getFailed(),
this.queue.getDelayed() this.queue.getDelayed()
]); ]);
return { return {
waiting: waiting.length, waiting: waiting.length,
active: active.length, active: active.length,
completed: completed.length, completed: completed.length,
failed: failed.length, failed: failed.length,
delayed: delayed.length delayed: delayed.length
}; };
} }
async drainQueue() { async drainQueue() {
if (!this.isInitialized) { if (!this.isInitialized) {
await this.queue.drain() await this.queue.drain()
} }
} }
async getQueueStatus() { async getQueueStatus() {
if (!this.isInitialized) { if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.'); throw new Error('Queue service not initialized. Call initialize() first.');
} }
const stats = await this.getJobStats(); const stats = await this.getJobStats();
return { return {
...stats, ...stats,
workers: this.getWorkerCount(), workers: this.getWorkerCount(),
totalConcurrency: this.getTotalConcurrency(), totalConcurrency: this.getTotalConcurrency(),
queue: this.queue.name, queue: this.queue.name,
connection: { connection: {
host: process.env.DRAGONFLY_HOST || 'localhost', host: process.env.DRAGONFLY_HOST || 'localhost',
port: parseInt(process.env.DRAGONFLY_PORT || '6379') port: parseInt(process.env.DRAGONFLY_PORT || '6379')
} }
}; };
} }
getWorkerCount() { getWorkerCount() {
if (!this.isInitialized) { if (!this.isInitialized) {
return 0; return 0;
} }
return this.workers.length; return this.workers.length;
} }
getRegisteredProviders() { getRegisteredProviders() {
return providerRegistry.getProviders().map(({ key, config }) => ({ return providerRegistry.getProviders().map(({ key, config }) => ({
key, key,
name: config.name, name: config.name,
service: config.service, service: config.service,
operations: Object.keys(config.operations), operations: Object.keys(config.operations),
scheduledJobs: config.scheduledJobs?.length || 0 scheduledJobs: config.scheduledJobs?.length || 0
})); }));
} }
getScheduledJobsInfo() { getScheduledJobsInfo() {
return providerRegistry.getAllScheduledJobs().map(({ service, provider, job }) => ({ return providerRegistry.getAllScheduledJobs().map(({ service, provider, job }) => ({
id: `${service}-${provider}-${job.type}`, id: `${service}-${provider}-${job.type}`,
service, service,
provider, provider,
type: job.type, type: job.type,
operation: job.operation, operation: job.operation,
cronPattern: job.cronPattern, cronPattern: job.cronPattern,
priority: job.priority, priority: job.priority,
description: job.description, description: job.description,
immediately: job.immediately || false immediately: job.immediately || false
})); }));
} }
async shutdown() { async shutdown() {
if (!this.isInitialized) { if (!this.isInitialized) {
this.logger.warn('Queue service not initialized, nothing to shutdown'); this.logger.warn('Queue service not initialized, nothing to shutdown');
return; return;
} }
this.logger.info('Shutting down queue service'); this.logger.info('Shutting down queue service');
// Close all workers // Close all workers
this.logger.info(`Closing ${this.workers.length} workers...`); this.logger.info(`Closing ${this.workers.length} workers...`);
await Promise.all(this.workers.map((worker, index) => { await Promise.all(this.workers.map((worker, index) => {
this.logger.debug(`Closing worker ${index + 1}`); this.logger.debug(`Closing worker ${index + 1}`);
return worker.close(); return worker.close();
})); }));
await this.queue.close(); await this.queue.close();
await this.queueEvents.close(); await this.queueEvents.close();
this.isInitialized = false; this.isInitialized = false;
this.logger.info('Queue service shutdown complete'); this.logger.info('Queue service shutdown complete');
} }
} }
export const queueManager = new QueueService(); export const queueManager = new QueueService();

View file

@ -1,293 +1,293 @@
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
export interface BatchConfig<T> { export interface BatchConfig<T> {
items: T[]; items: T[];
batchSize?: number; // Optional - only used for batch mode batchSize?: number; // Optional - only used for batch mode
totalDelayMs: number; totalDelayMs: number;
jobNamePrefix: string; jobNamePrefix: string;
operation: string; operation: string;
service: string; service: string;
provider: string; provider: string;
priority?: number; priority?: number;
createJobData: (item: T, index: number) => any; createJobData: (item: T, index: number) => any;
removeOnComplete?: number; removeOnComplete?: number;
removeOnFail?: number; removeOnFail?: number;
useBatching?: boolean; // Simple flag to choose mode useBatching?: boolean; // Simple flag to choose mode
} }
const logger = getLogger('batch-processor'); const logger = getLogger('batch-processor');
export class BatchProcessor { export class BatchProcessor {
constructor(private queueManager: any) {} constructor(private queueManager: any) {}
/** /**
* Unified method that handles both direct and batch approaches * Unified method that handles both direct and batch approaches
*/ */
async processItems<T>(config: BatchConfig<T>) { async processItems<T>(config: BatchConfig<T>) {
const { items, useBatching = false } = config; const { items, useBatching = false } = config;
if (items.length === 0) { if (items.length === 0) {
return { totalItems: 0, jobsCreated: 0 }; return { totalItems: 0, jobsCreated: 0 };
} }
if (useBatching) { if (useBatching) {
return await this.createBatchJobs(config); return await this.createBatchJobs(config);
} else { } else {
return await this.createDirectJobs(config); return await this.createDirectJobs(config);
} }
} }
private async createDirectJobs<T>(config: BatchConfig<T>) { private async createDirectJobs<T>(config: BatchConfig<T>) {
const { const {
items, items,
totalDelayMs, totalDelayMs,
jobNamePrefix, jobNamePrefix,
operation, operation,
service, service,
provider, provider,
priority = 2, priority = 2,
createJobData, createJobData,
removeOnComplete = 5, removeOnComplete = 5,
removeOnFail = 3 removeOnFail = 3
} = config; } = config;
const delayPerItem = Math.floor(totalDelayMs / items.length); const delayPerItem = Math.floor(totalDelayMs / items.length);
const chunkSize = 100; const chunkSize = 100;
let totalJobsCreated = 0; let totalJobsCreated = 0;
logger.info('Creating direct jobs', { logger.info('Creating direct jobs', {
totalItems: items.length, totalItems: items.length,
delayPerItem: `${(delayPerItem / 1000).toFixed(1)}s`, delayPerItem: `${(delayPerItem / 1000).toFixed(1)}s`,
estimatedDuration: `${(totalDelayMs / 1000 / 60 / 60).toFixed(1)} hours` estimatedDuration: `${(totalDelayMs / 1000 / 60 / 60).toFixed(1)} hours`
}); });
// Process in chunks to avoid overwhelming Redis // Process in chunks to avoid overwhelming Redis
for (let i = 0; i < items.length; i += chunkSize) { for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize); const chunk = items.slice(i, i + chunkSize);
const jobs = chunk.map((item, chunkIndex) => { const jobs = chunk.map((item, chunkIndex) => {
const globalIndex = i + chunkIndex; const globalIndex = i + chunkIndex;
return { return {
name: `${jobNamePrefix}-processing`, name: `${jobNamePrefix}-processing`,
data: { data: {
type: `${jobNamePrefix}-processing`, type: `${jobNamePrefix}-processing`,
service, service,
provider, provider,
operation, operation,
payload: createJobData(item, globalIndex), payload: createJobData(item, globalIndex),
priority priority
}, },
opts: { opts: {
delay: globalIndex * delayPerItem, delay: globalIndex * delayPerItem,
jobId: `${jobNamePrefix}-${globalIndex}-${Date.now()}`, jobId: `${jobNamePrefix}-${globalIndex}-${Date.now()}`,
removeOnComplete, removeOnComplete,
removeOnFail removeOnFail
} }
}; };
}); });
try { try {
const createdJobs = await this.queueManager.queue.addBulk(jobs); const createdJobs = await this.queueManager.queue.addBulk(jobs);
totalJobsCreated += createdJobs.length; totalJobsCreated += createdJobs.length;
// Log progress every 500 jobs // Log progress every 500 jobs
if (totalJobsCreated % 500 === 0 || i + chunkSize >= items.length) { if (totalJobsCreated % 500 === 0 || i + chunkSize >= items.length) {
logger.info('Direct job creation progress', { logger.info('Direct job creation progress', {
created: totalJobsCreated, created: totalJobsCreated,
total: items.length, total: items.length,
percentage: `${((totalJobsCreated / items.length) * 100).toFixed(1)}%` percentage: `${((totalJobsCreated / items.length) * 100).toFixed(1)}%`
}); });
} }
} catch (error) { } catch (error) {
logger.error('Failed to create job chunk', { logger.error('Failed to create job chunk', {
startIndex: i, startIndex: i,
chunkSize: chunk.length, chunkSize: chunk.length,
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
}); });
} }
} }
return { return {
totalItems: items.length, totalItems: items.length,
jobsCreated: totalJobsCreated, jobsCreated: totalJobsCreated,
mode: 'direct' mode: 'direct'
}; };
} }
private async createBatchJobs<T>(config: BatchConfig<T>) { private async createBatchJobs<T>(config: BatchConfig<T>) {
const { const {
items, items,
batchSize = 200, batchSize = 200,
totalDelayMs, totalDelayMs,
jobNamePrefix, jobNamePrefix,
operation, operation,
service, service,
provider, provider,
priority = 3 priority = 3
} = config; } = config;
const totalBatches = Math.ceil(items.length / batchSize); const totalBatches = Math.ceil(items.length / batchSize);
const delayPerBatch = Math.floor(totalDelayMs / totalBatches); const delayPerBatch = Math.floor(totalDelayMs / totalBatches);
const chunkSize = 50; // Create batch jobs in chunks const chunkSize = 50; // Create batch jobs in chunks
let batchJobsCreated = 0; let batchJobsCreated = 0;
logger.info('Creating batch jobs', { logger.info('Creating batch jobs', {
totalItems: items.length, totalItems: items.length,
batchSize, batchSize,
totalBatches, totalBatches,
delayPerBatch: `${(delayPerBatch / 1000 / 60).toFixed(2)} minutes` delayPerBatch: `${(delayPerBatch / 1000 / 60).toFixed(2)} minutes`
}); });
// Create batch jobs in chunks // Create batch jobs in chunks
for (let chunkStart = 0; chunkStart < totalBatches; chunkStart += chunkSize) { for (let chunkStart = 0; chunkStart < totalBatches; chunkStart += chunkSize) {
const chunkEnd = Math.min(chunkStart + chunkSize, totalBatches); const chunkEnd = Math.min(chunkStart + chunkSize, totalBatches);
const batchJobs = []; const batchJobs = [];
for (let batchIndex = chunkStart; batchIndex < chunkEnd; batchIndex++) { for (let batchIndex = chunkStart; batchIndex < chunkEnd; batchIndex++) {
const startIndex = batchIndex * batchSize; const startIndex = batchIndex * batchSize;
const endIndex = Math.min(startIndex + batchSize, items.length); const endIndex = Math.min(startIndex + batchSize, items.length);
const batchItems = items.slice(startIndex, endIndex); const batchItems = items.slice(startIndex, endIndex);
batchJobs.push({ batchJobs.push({
name: `${jobNamePrefix}-batch-processing`, name: `${jobNamePrefix}-batch-processing`,
data: { data: {
type: `${jobNamePrefix}-batch-processing`, type: `${jobNamePrefix}-batch-processing`,
service, service,
provider, provider,
operation: `process-${jobNamePrefix}-batch`, operation: `process-${jobNamePrefix}-batch`,
payload: { payload: {
items: batchItems, items: batchItems,
batchIndex, batchIndex,
total: totalBatches, total: totalBatches,
config: { ...config, priority: priority - 1 } config: { ...config, priority: priority - 1 }
}, },
priority priority
}, },
opts: { opts: {
delay: batchIndex * delayPerBatch, delay: batchIndex * delayPerBatch,
jobId: `${jobNamePrefix}-batch-${batchIndex}-${Date.now()}` jobId: `${jobNamePrefix}-batch-${batchIndex}-${Date.now()}`
} }
}); });
} }
try { try {
const createdJobs = await this.queueManager.queue.addBulk(batchJobs); const createdJobs = await this.queueManager.queue.addBulk(batchJobs);
batchJobsCreated += createdJobs.length; batchJobsCreated += createdJobs.length;
logger.info('Batch chunk created', { logger.info('Batch chunk created', {
chunkStart: chunkStart + 1, chunkStart: chunkStart + 1,
chunkEnd, chunkEnd,
created: createdJobs.length, created: createdJobs.length,
totalCreated: batchJobsCreated, totalCreated: batchJobsCreated,
progress: `${((chunkEnd / totalBatches) * 100).toFixed(1)}%` progress: `${((chunkEnd / totalBatches) * 100).toFixed(1)}%`
}); });
} catch (error) { } catch (error) {
logger.error('Failed to create batch chunk', { logger.error('Failed to create batch chunk', {
chunkStart, chunkStart,
chunkEnd, chunkEnd,
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
}); });
} }
// Small delay between chunks // Small delay between chunks
if (chunkEnd < totalBatches) { if (chunkEnd < totalBatches) {
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
} }
} }
return { return {
totalItems: items.length, totalItems: items.length,
batchJobsCreated, batchJobsCreated,
totalBatches, totalBatches,
estimatedDurationHours: totalDelayMs / 1000 / 60 / 60, estimatedDurationHours: totalDelayMs / 1000 / 60 / 60,
mode: 'batch' mode: 'batch'
}; };
} }
/** /**
* Process a batch (called by batch jobs) * Process a batch (called by batch jobs)
*/ */
async processBatch<T>(payload: { async processBatch<T>(payload: {
items: T[]; items: T[];
batchIndex: number; batchIndex: number;
total: number; total: number;
config: BatchConfig<T>; config: BatchConfig<T>;
}, createJobData?: (item: T, index: number) => any) { }, createJobData?: (item: T, index: number) => any) {
const { items, batchIndex, total, config } = payload; const { items, batchIndex, total, config } = payload;
logger.info('Processing batch', { logger.info('Processing batch', {
batchIndex, batchIndex,
batchSize: items.length, batchSize: items.length,
total, total,
progress: `${((batchIndex + 1) / total * 100).toFixed(2)}%` progress: `${((batchIndex + 1) / total * 100).toFixed(2)}%`
}); });
const totalBatchDelayMs = config.totalDelayMs / total; const totalBatchDelayMs = config.totalDelayMs / total;
const delayPerItem = Math.floor(totalBatchDelayMs / items.length); const delayPerItem = Math.floor(totalBatchDelayMs / items.length);
const jobs = items.map((item, itemIndex) => { const jobs = items.map((item, itemIndex) => {
// Use the provided createJobData function or fall back to config // Use the provided createJobData function or fall back to config
const jobDataFn = createJobData || config.createJobData; const jobDataFn = createJobData || config.createJobData;
if (!jobDataFn) { if (!jobDataFn) {
throw new Error('createJobData function is required'); throw new Error('createJobData function is required');
} }
const userData = jobDataFn(item, itemIndex); const userData = jobDataFn(item, itemIndex);
return { return {
name: `${config.jobNamePrefix}-processing`, name: `${config.jobNamePrefix}-processing`,
data: { data: {
type: `${config.jobNamePrefix}-processing`, type: `${config.jobNamePrefix}-processing`,
service: config.service, service: config.service,
provider: config.provider, provider: config.provider,
operation: config.operation, operation: config.operation,
payload: { payload: {
...userData, ...userData,
batchIndex, batchIndex,
itemIndex, itemIndex,
total, total,
source: userData.source || 'batch-processing' source: userData.source || 'batch-processing'
}, },
priority: config.priority || 2 priority: config.priority || 2
}, },
opts: { opts: {
delay: itemIndex * delayPerItem, delay: itemIndex * delayPerItem,
jobId: `${config.jobNamePrefix}-${batchIndex}-${itemIndex}-${Date.now()}`, jobId: `${config.jobNamePrefix}-${batchIndex}-${itemIndex}-${Date.now()}`,
removeOnComplete: config.removeOnComplete || 5, removeOnComplete: config.removeOnComplete || 5,
removeOnFail: config.removeOnFail || 3 removeOnFail: config.removeOnFail || 3
} }
}; };
}); });
try { try {
const createdJobs = await this.queueManager.queue.addBulk(jobs); const createdJobs = await this.queueManager.queue.addBulk(jobs);
logger.info('Batch processing completed', { logger.info('Batch processing completed', {
batchIndex, batchIndex,
totalItems: items.length, totalItems: items.length,
jobsCreated: createdJobs.length, jobsCreated: createdJobs.length,
progress: `${((batchIndex + 1) / total * 100).toFixed(2)}%` progress: `${((batchIndex + 1) / total * 100).toFixed(2)}%`
}); });
return { return {
batchIndex, batchIndex,
totalItems: items.length, totalItems: items.length,
jobsCreated: createdJobs.length, jobsCreated: createdJobs.length,
jobsFailed: 0 jobsFailed: 0
}; };
} catch (error) { } catch (error) {
logger.error('Failed to process batch', { logger.error('Failed to process batch', {
batchIndex, batchIndex,
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
}); });
return { return {
batchIndex, batchIndex,
totalItems: items.length, totalItems: items.length,
jobsCreated: 0, jobsCreated: 0,
jobsFailed: items.length jobsFailed: items.length
}; };
} }
} }
} }

View file

@ -1,20 +1,20 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src" "rootDir": "./src"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"],
"references": [ "references": [
{ "path": "../../libs/types" }, { "path": "../../libs/types" },
{ "path": "../../libs/config" }, { "path": "../../libs/config" },
{ "path": "../../libs/logger" }, { "path": "../../libs/logger" },
{ "path": "../../libs/http" }, { "path": "../../libs/http" },
{ "path": "../../libs/cache" }, { "path": "../../libs/cache" },
{ "path": "../../libs/questdb-client" }, { "path": "../../libs/questdb-client" },
{ "path": "../../libs/mongodb-client" }, { "path": "../../libs/mongodb-client" },
{ "path": "../../libs/event-bus" }, { "path": "../../libs/event-bus" },
{ "path": "../../libs/shutdown" } { "path": "../../libs/shutdown" }
] ]
} }

View file

@ -1,19 +1,19 @@
{ {
"extends": ["//"], "extends": ["//"],
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": [ "dependsOn": [
"@stock-bot/cache#build", "@stock-bot/cache#build",
"@stock-bot/config#build", "@stock-bot/config#build",
"@stock-bot/event-bus#build", "@stock-bot/event-bus#build",
"@stock-bot/http#build", "@stock-bot/http#build",
"@stock-bot/logger#build", "@stock-bot/logger#build",
"@stock-bot/mongodb-client#build", "@stock-bot/mongodb-client#build",
"@stock-bot/questdb-client#build", "@stock-bot/questdb-client#build",
"@stock-bot/shutdown#build" "@stock-bot/shutdown#build"
], ],
"outputs": ["dist/**"], "outputs": ["dist/**"],
"inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"] "inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"]
} }
} }
} }

View file

@ -1,37 +1,37 @@
{ {
"name": "@stock-bot/execution-service", "name": "@stock-bot/execution-service",
"version": "1.0.0", "version": "1.0.0",
"description": "Execution service for stock trading bot - handles order execution and broker integration", "description": "Execution service for stock trading bot - handles order execution and broker integration",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"devvvvv": "bun --watch src/index.ts", "devvvvv": "bun --watch src/index.ts",
"start": "bun src/index.ts", "start": "bun src/index.ts",
"test": "bun test", "test": "bun test",
"lint": "eslint src --ext .ts", "lint": "eslint src --ext .ts",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.12.0", "@hono/node-server": "^1.12.0",
"hono": "^4.6.1", "hono": "^4.6.1",
"@stock-bot/config": "*", "@stock-bot/config": "*",
"@stock-bot/logger": "*", "@stock-bot/logger": "*",
"@stock-bot/types": "*", "@stock-bot/types": "*",
"@stock-bot/event-bus": "*", "@stock-bot/event-bus": "*",
"@stock-bot/utils": "*" "@stock-bot/utils": "*"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.5.0", "@types/node": "^22.5.0",
"typescript": "^5.5.4" "typescript": "^5.5.4"
}, },
"keywords": [ "keywords": [
"trading", "trading",
"execution", "execution",
"broker", "broker",
"orders", "orders",
"stock-bot" "stock-bot"
], ],
"author": "Stock Bot Team", "author": "Stock Bot Team",
"license": "MIT" "license": "MIT"
} }

View file

@ -1,94 +1,94 @@
import { Order, OrderResult, OrderStatus } from '@stock-bot/types'; import { Order, OrderResult, OrderStatus } from '@stock-bot/types';
export interface BrokerInterface { export interface BrokerInterface {
/** /**
* Execute an order with the broker * Execute an order with the broker
*/ */
executeOrder(order: Order): Promise<OrderResult>; executeOrder(order: Order): Promise<OrderResult>;
/** /**
* Get order status from broker * Get order status from broker
*/ */
getOrderStatus(orderId: string): Promise<OrderStatus>; getOrderStatus(orderId: string): Promise<OrderStatus>;
/** /**
* Cancel an order * Cancel an order
*/ */
cancelOrder(orderId: string): Promise<boolean>; cancelOrder(orderId: string): Promise<boolean>;
/** /**
* Get current positions * Get current positions
*/ */
getPositions(): Promise<Position[]>; getPositions(): Promise<Position[]>;
/** /**
* Get account balance * Get account balance
*/ */
getAccountBalance(): Promise<AccountBalance>; getAccountBalance(): Promise<AccountBalance>;
} }
export interface Position { export interface Position {
symbol: string; symbol: string;
quantity: number; quantity: number;
averagePrice: number; averagePrice: number;
currentPrice: number; currentPrice: number;
unrealizedPnL: number; unrealizedPnL: number;
side: 'long' | 'short'; side: 'long' | 'short';
} }
export interface AccountBalance { export interface AccountBalance {
totalValue: number; totalValue: number;
availableCash: number; availableCash: number;
buyingPower: number; buyingPower: number;
marginUsed: number; marginUsed: number;
} }
export class MockBroker implements BrokerInterface { export class MockBroker implements BrokerInterface {
private orders: Map<string, OrderResult> = new Map(); private orders: Map<string, OrderResult> = new Map();
private positions: Position[] = []; private positions: Position[] = [];
async executeOrder(order: Order): Promise<OrderResult> { async executeOrder(order: Order): Promise<OrderResult> {
const orderId = `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const orderId = `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const result: OrderResult = { const result: OrderResult = {
orderId, orderId,
symbol: order.symbol, symbol: order.symbol,
quantity: order.quantity, quantity: order.quantity,
side: order.side, side: order.side,
status: 'filled', status: 'filled',
executedPrice: order.price || 100, // Mock price executedPrice: order.price || 100, // Mock price
executedAt: new Date(), executedAt: new Date(),
commission: 1.0 commission: 1.0
}; };
this.orders.set(orderId, result); this.orders.set(orderId, result);
return result; return result;
} }
async getOrderStatus(orderId: string): Promise<OrderStatus> { async getOrderStatus(orderId: string): Promise<OrderStatus> {
const order = this.orders.get(orderId); const order = this.orders.get(orderId);
return order?.status || 'unknown'; return order?.status || 'unknown';
} }
async cancelOrder(orderId: string): Promise<boolean> { async cancelOrder(orderId: string): Promise<boolean> {
const order = this.orders.get(orderId); const order = this.orders.get(orderId);
if (order && order.status === 'pending') { if (order && order.status === 'pending') {
order.status = 'cancelled'; order.status = 'cancelled';
return true; return true;
} }
return false; return false;
} }
async getPositions(): Promise<Position[]> { async getPositions(): Promise<Position[]> {
return this.positions; return this.positions;
} }
async getAccountBalance(): Promise<AccountBalance> { async getAccountBalance(): Promise<AccountBalance> {
return { return {
totalValue: 100000, totalValue: 100000,
availableCash: 50000, availableCash: 50000,
buyingPower: 200000, buyingPower: 200000,
marginUsed: 0 marginUsed: 0
}; };
} }
} }

View file

@ -1,57 +1,57 @@
import { Order, OrderResult } from '@stock-bot/types'; import { Order, OrderResult } from '@stock-bot/types';
import { logger } from '@stock-bot/logger'; import { logger } from '@stock-bot/logger';
import { BrokerInterface } from '../broker/interface.ts'; import { BrokerInterface } from '../broker/interface.ts';
export class OrderManager { export class OrderManager {
private broker: BrokerInterface; private broker: BrokerInterface;
private pendingOrders: Map<string, Order> = new Map(); private pendingOrders: Map<string, Order> = new Map();
constructor(broker: BrokerInterface) { constructor(broker: BrokerInterface) {
this.broker = broker; this.broker = broker;
} }
async executeOrder(order: Order): Promise<OrderResult> { async executeOrder(order: Order): Promise<OrderResult> {
try { try {
logger.info(`Executing order: ${order.symbol} ${order.side} ${order.quantity} @ ${order.price}`); logger.info(`Executing order: ${order.symbol} ${order.side} ${order.quantity} @ ${order.price}`);
// Add to pending orders // Add to pending orders
const orderId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const orderId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.pendingOrders.set(orderId, order); this.pendingOrders.set(orderId, order);
// Execute with broker // Execute with broker
const result = await this.broker.executeOrder(order); const result = await this.broker.executeOrder(order);
// Remove from pending // Remove from pending
this.pendingOrders.delete(orderId); this.pendingOrders.delete(orderId);
logger.info(`Order executed successfully: ${result.orderId}`); logger.info(`Order executed successfully: ${result.orderId}`);
return result; return result;
} catch (error) { } catch (error) {
logger.error('Order execution failed', error); logger.error('Order execution failed', error);
throw error; throw error;
} }
} }
async cancelOrder(orderId: string): Promise<boolean> { async cancelOrder(orderId: string): Promise<boolean> {
try { try {
const success = await this.broker.cancelOrder(orderId); const success = await this.broker.cancelOrder(orderId);
if (success) { if (success) {
this.pendingOrders.delete(orderId); this.pendingOrders.delete(orderId);
logger.info(`Order cancelled: ${orderId}`); logger.info(`Order cancelled: ${orderId}`);
} }
return success; return success;
} catch (error) { } catch (error) {
logger.error('Order cancellation failed', error); logger.error('Order cancellation failed', error);
throw error; throw error;
} }
} }
async getOrderStatus(orderId: string) { async getOrderStatus(orderId: string) {
return await this.broker.getOrderStatus(orderId); return await this.broker.getOrderStatus(orderId);
} }
getPendingOrders(): Order[] { getPendingOrders(): Order[] {
return Array.from(this.pendingOrders.values()); return Array.from(this.pendingOrders.values());
} }
} }

View file

@ -1,111 +1,111 @@
import { Order } from '@stock-bot/types'; import { Order } from '@stock-bot/types';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
export interface RiskRule { export interface RiskRule {
name: string; name: string;
validate(order: Order, context: RiskContext): Promise<RiskValidationResult>; validate(order: Order, context: RiskContext): Promise<RiskValidationResult>;
} }
export interface RiskContext { export interface RiskContext {
currentPositions: Map<string, number>; currentPositions: Map<string, number>;
accountBalance: number; accountBalance: number;
totalExposure: number; totalExposure: number;
maxPositionSize: number; maxPositionSize: number;
maxDailyLoss: number; maxDailyLoss: number;
} }
export interface RiskValidationResult { export interface RiskValidationResult {
isValid: boolean; isValid: boolean;
reason?: string; reason?: string;
severity: 'info' | 'warning' | 'error'; severity: 'info' | 'warning' | 'error';
} }
export class RiskManager { export class RiskManager {
private logger = getLogger('risk-manager'); private logger = getLogger('risk-manager');
private rules: RiskRule[] = []; private rules: RiskRule[] = [];
constructor() { constructor() {
this.initializeDefaultRules(); this.initializeDefaultRules();
} }
addRule(rule: RiskRule): void { addRule(rule: RiskRule): void {
this.rules.push(rule); this.rules.push(rule);
} }
async validateOrder(order: Order, context: RiskContext): Promise<RiskValidationResult> { async validateOrder(order: Order, context: RiskContext): Promise<RiskValidationResult> {
for (const rule of this.rules) { for (const rule of this.rules) {
const result = await rule.validate(order, context); const result = await rule.validate(order, context);
if (!result.isValid) { if (!result.isValid) {
logger.warn(`Risk rule violation: ${rule.name}`, { logger.warn(`Risk rule violation: ${rule.name}`, {
order, order,
reason: result.reason reason: result.reason
}); });
return result; return result;
} }
} }
return { isValid: true, severity: 'info' }; return { isValid: true, severity: 'info' };
} }
private initializeDefaultRules(): void { private initializeDefaultRules(): void {
// Position size rule // Position size rule
this.addRule({ this.addRule({
name: 'MaxPositionSize', name: 'MaxPositionSize',
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> { async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
const orderValue = order.quantity * (order.price || 0); const orderValue = order.quantity * (order.price || 0);
if (orderValue > context.maxPositionSize) { if (orderValue > context.maxPositionSize) {
return { return {
isValid: false, isValid: false,
reason: `Order size ${orderValue} exceeds maximum position size ${context.maxPositionSize}`, reason: `Order size ${orderValue} exceeds maximum position size ${context.maxPositionSize}`,
severity: 'error' severity: 'error'
}; };
} }
return { isValid: true, severity: 'info' }; return { isValid: true, severity: 'info' };
} }
}); });
// Balance check rule // Balance check rule
this.addRule({ this.addRule({
name: 'SufficientBalance', name: 'SufficientBalance',
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> { async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
const orderValue = order.quantity * (order.price || 0); const orderValue = order.quantity * (order.price || 0);
if (order.side === 'buy' && orderValue > context.accountBalance) { if (order.side === 'buy' && orderValue > context.accountBalance) {
return { return {
isValid: false, isValid: false,
reason: `Insufficient balance: need ${orderValue}, have ${context.accountBalance}`, reason: `Insufficient balance: need ${orderValue}, have ${context.accountBalance}`,
severity: 'error' severity: 'error'
}; };
} }
return { isValid: true, severity: 'info' }; return { isValid: true, severity: 'info' };
} }
}); });
// Concentration risk rule // Concentration risk rule
this.addRule({ this.addRule({
name: 'ConcentrationLimit', name: 'ConcentrationLimit',
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> { async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
const currentPosition = context.currentPositions.get(order.symbol) || 0; const currentPosition = context.currentPositions.get(order.symbol) || 0;
const newPosition = order.side === 'buy' ? const newPosition = order.side === 'buy' ?
currentPosition + order.quantity : currentPosition + order.quantity :
currentPosition - order.quantity; currentPosition - order.quantity;
const positionValue = Math.abs(newPosition) * (order.price || 0); const positionValue = Math.abs(newPosition) * (order.price || 0);
const concentrationRatio = positionValue / context.accountBalance; const concentrationRatio = positionValue / context.accountBalance;
if (concentrationRatio > 0.25) { // 25% max concentration if (concentrationRatio > 0.25) { // 25% max concentration
return { return {
isValid: false, isValid: false,
reason: `Position concentration ${(concentrationRatio * 100).toFixed(2)}% exceeds 25% limit`, reason: `Position concentration ${(concentrationRatio * 100).toFixed(2)}% exceeds 25% limit`,
severity: 'warning' severity: 'warning'
}; };
} }
return { isValid: true, severity: 'info' }; return { isValid: true, severity: 'info' };
} }
}); });
} }
} }

View file

@ -1,97 +1,97 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { config } from '@stock-bot/config'; import { config } from '@stock-bot/config';
// import { BrokerInterface } from './broker/interface.ts'; // import { BrokerInterface } from './broker/interface.ts';
// import { OrderManager } from './execution/order-manager.ts'; // import { OrderManager } from './execution/order-manager.ts';
// import { RiskManager } from './execution/risk-manager.ts'; // import { RiskManager } from './execution/risk-manager.ts';
const app = new Hono(); const app = new Hono();
const logger = getLogger('execution-service'); const logger = getLogger('execution-service');
// Health check endpoint // Health check endpoint
app.get('/health', (c) => { app.get('/health', (c) => {
return c.json({ return c.json({
status: 'healthy', status: 'healthy',
service: 'execution-service', service: 'execution-service',
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
}); });
// Order execution endpoints // Order execution endpoints
app.post('/orders/execute', async (c) => { app.post('/orders/execute', async (c) => {
try { try {
const orderRequest = await c.req.json(); const orderRequest = await c.req.json();
logger.info('Received order execution request', orderRequest); logger.info('Received order execution request', orderRequest);
// TODO: Validate order and execute // TODO: Validate order and execute
return c.json({ return c.json({
orderId: `order_${Date.now()}`, orderId: `order_${Date.now()}`,
status: 'pending', status: 'pending',
message: 'Order submitted for execution' message: 'Order submitted for execution'
}); });
} catch (error) { } catch (error) {
logger.error('Order execution failed', error); logger.error('Order execution failed', error);
return c.json({ error: 'Order execution failed' }, 500); return c.json({ error: 'Order execution failed' }, 500);
} }
}); });
app.get('/orders/:orderId/status', async (c) => { app.get('/orders/:orderId/status', async (c) => {
const orderId = c.req.param('orderId'); const orderId = c.req.param('orderId');
try { try {
// TODO: Get order status from broker // TODO: Get order status from broker
return c.json({ return c.json({
orderId, orderId,
status: 'filled', status: 'filled',
executedAt: new Date().toISOString() executedAt: new Date().toISOString()
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get order status', error); logger.error('Failed to get order status', error);
return c.json({ error: 'Failed to get order status' }, 500); return c.json({ error: 'Failed to get order status' }, 500);
} }
}); });
app.post('/orders/:orderId/cancel', async (c) => { app.post('/orders/:orderId/cancel', async (c) => {
const orderId = c.req.param('orderId'); const orderId = c.req.param('orderId');
try { try {
// TODO: Cancel order with broker // TODO: Cancel order with broker
return c.json({ return c.json({
orderId, orderId,
status: 'cancelled', status: 'cancelled',
cancelledAt: new Date().toISOString() cancelledAt: new Date().toISOString()
}); });
} catch (error) { } catch (error) {
logger.error('Failed to cancel order', error); logger.error('Failed to cancel order', error);
return c.json({ error: 'Failed to cancel order' }, 500); return c.json({ error: 'Failed to cancel order' }, 500);
} }
}); });
// Risk management endpoints // Risk management endpoints
app.get('/risk/position/:symbol', async (c) => { app.get('/risk/position/:symbol', async (c) => {
const symbol = c.req.param('symbol'); const symbol = c.req.param('symbol');
try { try {
// TODO: Get position risk metrics // TODO: Get position risk metrics
return c.json({ return c.json({
symbol, symbol,
position: 100, position: 100,
exposure: 10000, exposure: 10000,
risk: 'low' risk: 'low'
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get position risk', error); logger.error('Failed to get position risk', error);
return c.json({ error: 'Failed to get position risk' }, 500); return c.json({ error: 'Failed to get position risk' }, 500);
} }
}); });
const port = config.EXECUTION_SERVICE_PORT || 3004; const port = config.EXECUTION_SERVICE_PORT || 3004;
logger.info(`Starting execution service on port ${port}`); logger.info(`Starting execution service on port ${port}`);
serve({ serve({
fetch: app.fetch, fetch: app.fetch,
port port
}, (info) => { }, (info) => {
logger.info(`Execution service is running on port ${info.port}`); logger.info(`Execution service is running on port ${info.port}`);
}); });

View file

@ -1,17 +1,17 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src" "rootDir": "./src"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"],
"references": [ "references": [
{ "path": "../../libs/types" }, { "path": "../../libs/types" },
{ "path": "../../libs/config" }, { "path": "../../libs/config" },
{ "path": "../../libs/logger" }, { "path": "../../libs/logger" },
{ "path": "../../libs/utils" }, { "path": "../../libs/utils" },
{ "path": "../../libs/event-bus" }, { "path": "../../libs/event-bus" },
{ "path": "../../libs/shutdown" } { "path": "../../libs/shutdown" }
] ]
} }

View file

@ -1,17 +1,17 @@
{ {
"extends": ["//"], "extends": ["//"],
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": [ "dependsOn": [
"@stock-bot/types#build", "@stock-bot/types#build",
"@stock-bot/config#build", "@stock-bot/config#build",
"@stock-bot/logger#build", "@stock-bot/logger#build",
"@stock-bot/utils#build", "@stock-bot/utils#build",
"@stock-bot/event-bus#build", "@stock-bot/event-bus#build",
"@stock-bot/shutdown#build" "@stock-bot/shutdown#build"
], ],
"outputs": ["dist/**"], "outputs": ["dist/**"],
"inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"] "inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"]
} }
} }
} }

View file

@ -1,38 +1,38 @@
{ {
"name": "@stock-bot/portfolio-service", "name": "@stock-bot/portfolio-service",
"version": "1.0.0", "version": "1.0.0",
"description": "Portfolio service for stock trading bot - handles portfolio tracking and performance analytics", "description": "Portfolio service for stock trading bot - handles portfolio tracking and performance analytics",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"devvvvv": "bun --watch src/index.ts", "devvvvv": "bun --watch src/index.ts",
"start": "bun src/index.ts", "start": "bun src/index.ts",
"test": "bun test", "test": "bun test",
"lint": "eslint src --ext .ts", "lint": "eslint src --ext .ts",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.12.0", "@hono/node-server": "^1.12.0",
"hono": "^4.6.1", "hono": "^4.6.1",
"@stock-bot/config": "*", "@stock-bot/config": "*",
"@stock-bot/logger": "*", "@stock-bot/logger": "*",
"@stock-bot/types": "*", "@stock-bot/types": "*",
"@stock-bot/questdb-client": "*", "@stock-bot/questdb-client": "*",
"@stock-bot/utils": "*", "@stock-bot/utils": "*",
"@stock-bot/data-frame": "*" "@stock-bot/data-frame": "*"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.5.0", "@types/node": "^22.5.0",
"typescript": "^5.5.4" "typescript": "^5.5.4"
}, },
"keywords": [ "keywords": [
"trading", "trading",
"portfolio", "portfolio",
"performance", "performance",
"analytics", "analytics",
"stock-bot" "stock-bot"
], ],
"author": "Stock Bot Team", "author": "Stock Bot Team",
"license": "MIT" "license": "MIT"
} }

View file

@ -1,204 +1,204 @@
import { PortfolioSnapshot, Trade } from '../portfolio/portfolio-manager.ts'; import { PortfolioSnapshot, Trade } from '../portfolio/portfolio-manager.ts';
export interface PerformanceMetrics { export interface PerformanceMetrics {
totalReturn: number; totalReturn: number;
annualizedReturn: number; annualizedReturn: number;
sharpeRatio: number; sharpeRatio: number;
maxDrawdown: number; maxDrawdown: number;
volatility: number; volatility: number;
beta: number; beta: number;
alpha: number; alpha: number;
calmarRatio: number; calmarRatio: number;
sortinoRatio: number; sortinoRatio: number;
} }
export interface RiskMetrics { export interface RiskMetrics {
var95: number; // Value at Risk (95% confidence) var95: number; // Value at Risk (95% confidence)
cvar95: number; // Conditional Value at Risk cvar95: number; // Conditional Value at Risk
maxDrawdown: number; maxDrawdown: number;
downsideDeviation: number; downsideDeviation: number;
correlationMatrix: Record<string, Record<string, number>>; correlationMatrix: Record<string, Record<string, number>>;
} }
export class PerformanceAnalyzer { export class PerformanceAnalyzer {
private snapshots: PortfolioSnapshot[] = []; private snapshots: PortfolioSnapshot[] = [];
private benchmarkReturns: number[] = []; // S&P 500 or other benchmark private benchmarkReturns: number[] = []; // S&P 500 or other benchmark
addSnapshot(snapshot: PortfolioSnapshot): void { addSnapshot(snapshot: PortfolioSnapshot): void {
this.snapshots.push(snapshot); this.snapshots.push(snapshot);
// Keep only last 252 trading days (1 year) // Keep only last 252 trading days (1 year)
if (this.snapshots.length > 252) { if (this.snapshots.length > 252) {
this.snapshots = this.snapshots.slice(-252); this.snapshots = this.snapshots.slice(-252);
} }
} }
calculatePerformanceMetrics(period: 'daily' | 'weekly' | 'monthly' = 'daily'): PerformanceMetrics { calculatePerformanceMetrics(period: 'daily' | 'weekly' | 'monthly' = 'daily'): PerformanceMetrics {
if (this.snapshots.length < 2) { if (this.snapshots.length < 2) {
throw new Error('Need at least 2 snapshots to calculate performance'); throw new Error('Need at least 2 snapshots to calculate performance');
} }
const returns = this.calculateReturns(period); const returns = this.calculateReturns(period);
const riskFreeRate = 0.02; // 2% annual risk-free rate const riskFreeRate = 0.02; // 2% annual risk-free rate
return { return {
totalReturn: this.calculateTotalReturn(), totalReturn: this.calculateTotalReturn(),
annualizedReturn: this.calculateAnnualizedReturn(returns), annualizedReturn: this.calculateAnnualizedReturn(returns),
sharpeRatio: this.calculateSharpeRatio(returns, riskFreeRate), sharpeRatio: this.calculateSharpeRatio(returns, riskFreeRate),
maxDrawdown: this.calculateMaxDrawdown(), maxDrawdown: this.calculateMaxDrawdown(),
volatility: this.calculateVolatility(returns), volatility: this.calculateVolatility(returns),
beta: this.calculateBeta(returns), beta: this.calculateBeta(returns),
alpha: this.calculateAlpha(returns, riskFreeRate), alpha: this.calculateAlpha(returns, riskFreeRate),
calmarRatio: this.calculateCalmarRatio(returns), calmarRatio: this.calculateCalmarRatio(returns),
sortinoRatio: this.calculateSortinoRatio(returns, riskFreeRate) sortinoRatio: this.calculateSortinoRatio(returns, riskFreeRate)
}; };
} }
calculateRiskMetrics(): RiskMetrics { calculateRiskMetrics(): RiskMetrics {
const returns = this.calculateReturns('daily'); const returns = this.calculateReturns('daily');
return { return {
var95: this.calculateVaR(returns, 0.95), var95: this.calculateVaR(returns, 0.95),
cvar95: this.calculateCVaR(returns, 0.95), cvar95: this.calculateCVaR(returns, 0.95),
maxDrawdown: this.calculateMaxDrawdown(), maxDrawdown: this.calculateMaxDrawdown(),
downsideDeviation: this.calculateDownsideDeviation(returns), downsideDeviation: this.calculateDownsideDeviation(returns),
correlationMatrix: {} // TODO: Implement correlation matrix correlationMatrix: {} // TODO: Implement correlation matrix
}; };
} }
private calculateReturns(period: 'daily' | 'weekly' | 'monthly'): number[] { private calculateReturns(period: 'daily' | 'weekly' | 'monthly'): number[] {
if (this.snapshots.length < 2) return []; if (this.snapshots.length < 2) return [];
const returns: number[] = []; const returns: number[] = [];
for (let i = 1; i < this.snapshots.length; i++) { for (let i = 1; i < this.snapshots.length; i++) {
const currentValue = this.snapshots[i].totalValue; const currentValue = this.snapshots[i].totalValue;
const previousValue = this.snapshots[i - 1].totalValue; const previousValue = this.snapshots[i - 1].totalValue;
const return_ = (currentValue - previousValue) / previousValue; const return_ = (currentValue - previousValue) / previousValue;
returns.push(return_); returns.push(return_);
} }
return returns; return returns;
} }
private calculateTotalReturn(): number { private calculateTotalReturn(): number {
if (this.snapshots.length < 2) return 0; if (this.snapshots.length < 2) return 0;
const firstValue = this.snapshots[0].totalValue; const firstValue = this.snapshots[0].totalValue;
const lastValue = this.snapshots[this.snapshots.length - 1].totalValue; const lastValue = this.snapshots[this.snapshots.length - 1].totalValue;
return (lastValue - firstValue) / firstValue; return (lastValue - firstValue) / firstValue;
} }
private calculateAnnualizedReturn(returns: number[]): number { private calculateAnnualizedReturn(returns: number[]): number {
if (returns.length === 0) return 0; if (returns.length === 0) return 0;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
return Math.pow(1 + avgReturn, 252) - 1; // 252 trading days per year return Math.pow(1 + avgReturn, 252) - 1; // 252 trading days per year
} }
private calculateVolatility(returns: number[]): number { private calculateVolatility(returns: number[]): number {
if (returns.length === 0) return 0; if (returns.length === 0) return 0;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / returns.length;
return Math.sqrt(variance * 252); // Annualized volatility return Math.sqrt(variance * 252); // Annualized volatility
} }
private calculateSharpeRatio(returns: number[], riskFreeRate: number): number { private calculateSharpeRatio(returns: number[], riskFreeRate: number): number {
if (returns.length === 0) return 0; if (returns.length === 0) return 0;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
const annualizedReturn = Math.pow(1 + avgReturn, 252) - 1; const annualizedReturn = Math.pow(1 + avgReturn, 252) - 1;
const volatility = this.calculateVolatility(returns); const volatility = this.calculateVolatility(returns);
if (volatility === 0) return 0; if (volatility === 0) return 0;
return (annualizedReturn - riskFreeRate) / volatility; return (annualizedReturn - riskFreeRate) / volatility;
} }
private calculateMaxDrawdown(): number { private calculateMaxDrawdown(): number {
if (this.snapshots.length === 0) return 0; if (this.snapshots.length === 0) return 0;
let maxDrawdown = 0; let maxDrawdown = 0;
let peak = this.snapshots[0].totalValue; let peak = this.snapshots[0].totalValue;
for (const snapshot of this.snapshots) { for (const snapshot of this.snapshots) {
if (snapshot.totalValue > peak) { if (snapshot.totalValue > peak) {
peak = snapshot.totalValue; peak = snapshot.totalValue;
} }
const drawdown = (peak - snapshot.totalValue) / peak; const drawdown = (peak - snapshot.totalValue) / peak;
maxDrawdown = Math.max(maxDrawdown, drawdown); maxDrawdown = Math.max(maxDrawdown, drawdown);
} }
return maxDrawdown; return maxDrawdown;
} }
private calculateBeta(returns: number[]): number { private calculateBeta(returns: number[]): number {
if (returns.length === 0 || this.benchmarkReturns.length === 0) return 1.0; if (returns.length === 0 || this.benchmarkReturns.length === 0) return 1.0;
// Simple beta calculation - would need actual benchmark data // Simple beta calculation - would need actual benchmark data
return 1.0; // Placeholder return 1.0; // Placeholder
} }
private calculateAlpha(returns: number[], riskFreeRate: number): number { private calculateAlpha(returns: number[], riskFreeRate: number): number {
const beta = this.calculateBeta(returns); const beta = this.calculateBeta(returns);
const portfolioReturn = this.calculateAnnualizedReturn(returns); const portfolioReturn = this.calculateAnnualizedReturn(returns);
const benchmarkReturn = 0.10; // 10% benchmark return (placeholder) const benchmarkReturn = 0.10; // 10% benchmark return (placeholder)
return portfolioReturn - (riskFreeRate + beta * (benchmarkReturn - riskFreeRate)); return portfolioReturn - (riskFreeRate + beta * (benchmarkReturn - riskFreeRate));
} }
private calculateCalmarRatio(returns: number[]): number { private calculateCalmarRatio(returns: number[]): number {
const annualizedReturn = this.calculateAnnualizedReturn(returns); const annualizedReturn = this.calculateAnnualizedReturn(returns);
const maxDrawdown = this.calculateMaxDrawdown(); const maxDrawdown = this.calculateMaxDrawdown();
if (maxDrawdown === 0) return 0; if (maxDrawdown === 0) return 0;
return annualizedReturn / maxDrawdown; return annualizedReturn / maxDrawdown;
} }
private calculateSortinoRatio(returns: number[], riskFreeRate: number): number { private calculateSortinoRatio(returns: number[], riskFreeRate: number): number {
const annualizedReturn = this.calculateAnnualizedReturn(returns); const annualizedReturn = this.calculateAnnualizedReturn(returns);
const downsideDeviation = this.calculateDownsideDeviation(returns); const downsideDeviation = this.calculateDownsideDeviation(returns);
if (downsideDeviation === 0) return 0; if (downsideDeviation === 0) return 0;
return (annualizedReturn - riskFreeRate) / downsideDeviation; return (annualizedReturn - riskFreeRate) / downsideDeviation;
} }
private calculateDownsideDeviation(returns: number[]): number { private calculateDownsideDeviation(returns: number[]): number {
if (returns.length === 0) return 0; if (returns.length === 0) return 0;
const negativeReturns = returns.filter(ret => ret < 0); const negativeReturns = returns.filter(ret => ret < 0);
if (negativeReturns.length === 0) return 0; if (negativeReturns.length === 0) return 0;
const avgNegativeReturn = negativeReturns.reduce((sum, ret) => sum + ret, 0) / negativeReturns.length; const avgNegativeReturn = negativeReturns.reduce((sum, ret) => sum + ret, 0) / negativeReturns.length;
const variance = negativeReturns.reduce((sum, ret) => sum + Math.pow(ret - avgNegativeReturn, 2), 0) / negativeReturns.length; const variance = negativeReturns.reduce((sum, ret) => sum + Math.pow(ret - avgNegativeReturn, 2), 0) / negativeReturns.length;
return Math.sqrt(variance * 252); // Annualized return Math.sqrt(variance * 252); // Annualized
} }
private calculateVaR(returns: number[], confidence: number): number { private calculateVaR(returns: number[], confidence: number): number {
if (returns.length === 0) return 0; if (returns.length === 0) return 0;
const sortedReturns = returns.slice().sort((a, b) => a - b); const sortedReturns = returns.slice().sort((a, b) => a - b);
const index = Math.floor((1 - confidence) * sortedReturns.length); const index = Math.floor((1 - confidence) * sortedReturns.length);
return -sortedReturns[index]; // Return as positive value return -sortedReturns[index]; // Return as positive value
} }
private calculateCVaR(returns: number[], confidence: number): number { private calculateCVaR(returns: number[], confidence: number): number {
if (returns.length === 0) return 0; if (returns.length === 0) return 0;
const sortedReturns = returns.slice().sort((a, b) => a - b); const sortedReturns = returns.slice().sort((a, b) => a - b);
const cutoffIndex = Math.floor((1 - confidence) * sortedReturns.length); const cutoffIndex = Math.floor((1 - confidence) * sortedReturns.length);
const tailReturns = sortedReturns.slice(0, cutoffIndex + 1); const tailReturns = sortedReturns.slice(0, cutoffIndex + 1);
if (tailReturns.length === 0) return 0; if (tailReturns.length === 0) return 0;
const avgTailReturn = tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length; const avgTailReturn = tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length;
return -avgTailReturn; // Return as positive value return -avgTailReturn; // Return as positive value
} }
} }

View file

@ -1,133 +1,133 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { config } from '@stock-bot/config'; import { config } from '@stock-bot/config';
import { PortfolioManager } from './portfolio/portfolio-manager.ts'; import { PortfolioManager } from './portfolio/portfolio-manager.ts';
import { PerformanceAnalyzer } from './analytics/performance-analyzer.ts'; import { PerformanceAnalyzer } from './analytics/performance-analyzer.ts';
const app = new Hono(); const app = new Hono();
const logger = getLogger('portfolio-service'); const logger = getLogger('portfolio-service');
// Health check endpoint // Health check endpoint
app.get('/health', (c) => { app.get('/health', (c) => {
return c.json({ return c.json({
status: 'healthy', status: 'healthy',
service: 'portfolio-service', service: 'portfolio-service',
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
}); });
// Portfolio endpoints // Portfolio endpoints
app.get('/portfolio/overview', async (c) => { app.get('/portfolio/overview', async (c) => {
try { try {
// TODO: Get portfolio overview // TODO: Get portfolio overview
return c.json({ return c.json({
totalValue: 125000, totalValue: 125000,
totalReturn: 25000, totalReturn: 25000,
totalReturnPercent: 25.0, totalReturnPercent: 25.0,
dayChange: 1250, dayChange: 1250,
dayChangePercent: 1.0, dayChangePercent: 1.0,
positions: [] positions: []
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get portfolio overview', error); logger.error('Failed to get portfolio overview', error);
return c.json({ error: 'Failed to get portfolio overview' }, 500); return c.json({ error: 'Failed to get portfolio overview' }, 500);
} }
}); });
app.get('/portfolio/positions', async (c) => { app.get('/portfolio/positions', async (c) => {
try { try {
// TODO: Get current positions // TODO: Get current positions
return c.json([ return c.json([
{ {
symbol: 'AAPL', symbol: 'AAPL',
quantity: 100, quantity: 100,
averagePrice: 150.0, averagePrice: 150.0,
currentPrice: 155.0, currentPrice: 155.0,
marketValue: 15500, marketValue: 15500,
unrealizedPnL: 500, unrealizedPnL: 500,
unrealizedPnLPercent: 3.33 unrealizedPnLPercent: 3.33
} }
]); ]);
} catch (error) { } catch (error) {
logger.error('Failed to get positions', error); logger.error('Failed to get positions', error);
return c.json({ error: 'Failed to get positions' }, 500); return c.json({ error: 'Failed to get positions' }, 500);
} }
}); });
app.get('/portfolio/history', async (c) => { app.get('/portfolio/history', async (c) => {
const days = c.req.query('days') || '30'; const days = c.req.query('days') || '30';
try { try {
// TODO: Get portfolio history // TODO: Get portfolio history
return c.json({ return c.json({
period: `${days} days`, period: `${days} days`,
data: [] data: []
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get portfolio history', error); logger.error('Failed to get portfolio history', error);
return c.json({ error: 'Failed to get portfolio history' }, 500); return c.json({ error: 'Failed to get portfolio history' }, 500);
} }
}); });
// Performance analytics endpoints // Performance analytics endpoints
app.get('/analytics/performance', async (c) => { app.get('/analytics/performance', async (c) => {
const period = c.req.query('period') || '1M'; const period = c.req.query('period') || '1M';
try { try {
// TODO: Calculate performance metrics // TODO: Calculate performance metrics
return c.json({ return c.json({
period, period,
totalReturn: 0.25, totalReturn: 0.25,
annualizedReturn: 0.30, annualizedReturn: 0.30,
sharpeRatio: 1.5, sharpeRatio: 1.5,
maxDrawdown: 0.05, maxDrawdown: 0.05,
volatility: 0.15, volatility: 0.15,
beta: 1.1, beta: 1.1,
alpha: 0.02 alpha: 0.02
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get performance analytics', error); logger.error('Failed to get performance analytics', error);
return c.json({ error: 'Failed to get performance analytics' }, 500); return c.json({ error: 'Failed to get performance analytics' }, 500);
} }
}); });
app.get('/analytics/risk', async (c) => { app.get('/analytics/risk', async (c) => {
try { try {
// TODO: Calculate risk metrics // TODO: Calculate risk metrics
return c.json({ return c.json({
var95: 0.02, var95: 0.02,
cvar95: 0.03, cvar95: 0.03,
maxDrawdown: 0.05, maxDrawdown: 0.05,
downside_deviation: 0.08, downside_deviation: 0.08,
correlation_matrix: {} correlation_matrix: {}
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get risk analytics', error); logger.error('Failed to get risk analytics', error);
return c.json({ error: 'Failed to get risk analytics' }, 500); return c.json({ error: 'Failed to get risk analytics' }, 500);
} }
}); });
app.get('/analytics/attribution', async (c) => { app.get('/analytics/attribution', async (c) => {
try { try {
// TODO: Calculate performance attribution // TODO: Calculate performance attribution
return c.json({ return c.json({
sector_allocation: {}, sector_allocation: {},
security_selection: {}, security_selection: {},
interaction_effect: {} interaction_effect: {}
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get attribution analytics', error); logger.error('Failed to get attribution analytics', error);
return c.json({ error: 'Failed to get attribution analytics' }, 500); return c.json({ error: 'Failed to get attribution analytics' }, 500);
} }
}); });
const port = config.PORTFOLIO_SERVICE_PORT || 3005; const port = config.PORTFOLIO_SERVICE_PORT || 3005;
logger.info(`Starting portfolio service on port ${port}`); logger.info(`Starting portfolio service on port ${port}`);
serve({ serve({
fetch: app.fetch, fetch: app.fetch,
port port
}, (info) => { }, (info) => {
logger.info(`Portfolio service is running on port ${info.port}`); logger.info(`Portfolio service is running on port ${info.port}`);
}); });

View file

@ -1,159 +1,159 @@
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
export interface Position { export interface Position {
symbol: string; symbol: string;
quantity: number; quantity: number;
averagePrice: number; averagePrice: number;
currentPrice: number; currentPrice: number;
marketValue: number; marketValue: number;
unrealizedPnL: number; unrealizedPnL: number;
unrealizedPnLPercent: number; unrealizedPnLPercent: number;
costBasis: number; costBasis: number;
lastUpdated: Date; lastUpdated: Date;
} }
export interface PortfolioSnapshot { export interface PortfolioSnapshot {
timestamp: Date; timestamp: Date;
totalValue: number; totalValue: number;
cashBalance: number; cashBalance: number;
positions: Position[]; positions: Position[];
totalReturn: number; totalReturn: number;
totalReturnPercent: number; totalReturnPercent: number;
dayChange: number; dayChange: number;
dayChangePercent: number; dayChangePercent: number;
} }
export interface Trade { export interface Trade {
id: string; id: string;
symbol: string; symbol: string;
quantity: number; quantity: number;
price: number; price: number;
side: 'buy' | 'sell'; side: 'buy' | 'sell';
timestamp: Date; timestamp: Date;
commission: number; commission: number;
} }
export class PortfolioManager { export class PortfolioManager {
private logger = getLogger('PortfolioManager'); private logger = getLogger('PortfolioManager');
private positions: Map<string, Position> = new Map(); private positions: Map<string, Position> = new Map();
private trades: Trade[] = []; private trades: Trade[] = [];
private cashBalance: number = 100000; // Starting cash private cashBalance: number = 100000; // Starting cash
constructor(initialCash: number = 100000) { constructor(initialCash: number = 100000) {
this.cashBalance = initialCash; this.cashBalance = initialCash;
} }
addTrade(trade: Trade): void { addTrade(trade: Trade): void {
this.trades.push(trade); this.trades.push(trade);
this.updatePosition(trade); this.updatePosition(trade);
logger.info(`Trade added: ${trade.symbol} ${trade.side} ${trade.quantity} @ ${trade.price}`); logger.info(`Trade added: ${trade.symbol} ${trade.side} ${trade.quantity} @ ${trade.price}`);
} }
private updatePosition(trade: Trade): void { private updatePosition(trade: Trade): void {
const existing = this.positions.get(trade.symbol); const existing = this.positions.get(trade.symbol);
if (!existing) { if (!existing) {
// New position // New position
if (trade.side === 'buy') { if (trade.side === 'buy') {
this.positions.set(trade.symbol, { this.positions.set(trade.symbol, {
symbol: trade.symbol, symbol: trade.symbol,
quantity: trade.quantity, quantity: trade.quantity,
averagePrice: trade.price, averagePrice: trade.price,
currentPrice: trade.price, currentPrice: trade.price,
marketValue: trade.quantity * trade.price, marketValue: trade.quantity * trade.price,
unrealizedPnL: 0, unrealizedPnL: 0,
unrealizedPnLPercent: 0, unrealizedPnLPercent: 0,
costBasis: trade.quantity * trade.price + trade.commission, costBasis: trade.quantity * trade.price + trade.commission,
lastUpdated: trade.timestamp lastUpdated: trade.timestamp
}); });
this.cashBalance -= (trade.quantity * trade.price + trade.commission); this.cashBalance -= (trade.quantity * trade.price + trade.commission);
} }
return; return;
} }
// Update existing position // Update existing position
if (trade.side === 'buy') { if (trade.side === 'buy') {
const newQuantity = existing.quantity + trade.quantity; const newQuantity = existing.quantity + trade.quantity;
const newCostBasis = existing.costBasis + (trade.quantity * trade.price) + trade.commission; const newCostBasis = existing.costBasis + (trade.quantity * trade.price) + trade.commission;
existing.quantity = newQuantity; existing.quantity = newQuantity;
existing.averagePrice = (newCostBasis - this.getTotalCommissions(trade.symbol)) / newQuantity; existing.averagePrice = (newCostBasis - this.getTotalCommissions(trade.symbol)) / newQuantity;
existing.costBasis = newCostBasis; existing.costBasis = newCostBasis;
existing.lastUpdated = trade.timestamp; existing.lastUpdated = trade.timestamp;
this.cashBalance -= (trade.quantity * trade.price + trade.commission); this.cashBalance -= (trade.quantity * trade.price + trade.commission);
} else if (trade.side === 'sell') { } else if (trade.side === 'sell') {
existing.quantity -= trade.quantity; existing.quantity -= trade.quantity;
existing.lastUpdated = trade.timestamp; existing.lastUpdated = trade.timestamp;
const proceeds = trade.quantity * trade.price - trade.commission; const proceeds = trade.quantity * trade.price - trade.commission;
this.cashBalance += proceeds; this.cashBalance += proceeds;
// Remove position if quantity is zero // Remove position if quantity is zero
if (existing.quantity <= 0) { if (existing.quantity <= 0) {
this.positions.delete(trade.symbol); this.positions.delete(trade.symbol);
} }
} }
} }
updatePrice(symbol: string, price: number): void { updatePrice(symbol: string, price: number): void {
const position = this.positions.get(symbol); const position = this.positions.get(symbol);
if (position) { if (position) {
position.currentPrice = price; position.currentPrice = price;
position.marketValue = position.quantity * price; position.marketValue = position.quantity * price;
position.unrealizedPnL = position.marketValue - (position.quantity * position.averagePrice); position.unrealizedPnL = position.marketValue - (position.quantity * position.averagePrice);
position.unrealizedPnLPercent = position.unrealizedPnL / (position.quantity * position.averagePrice) * 100; position.unrealizedPnLPercent = position.unrealizedPnL / (position.quantity * position.averagePrice) * 100;
position.lastUpdated = new Date(); position.lastUpdated = new Date();
} }
} }
getPosition(symbol: string): Position | undefined { getPosition(symbol: string): Position | undefined {
return this.positions.get(symbol); return this.positions.get(symbol);
} }
getAllPositions(): Position[] { getAllPositions(): Position[] {
return Array.from(this.positions.values()); return Array.from(this.positions.values());
} }
getPortfolioSnapshot(): PortfolioSnapshot { getPortfolioSnapshot(): PortfolioSnapshot {
const positions = this.getAllPositions(); const positions = this.getAllPositions();
const totalMarketValue = positions.reduce((sum, pos) => sum + pos.marketValue, 0); const totalMarketValue = positions.reduce((sum, pos) => sum + pos.marketValue, 0);
const totalValue = totalMarketValue + this.cashBalance; const totalValue = totalMarketValue + this.cashBalance;
const totalUnrealizedPnL = positions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0); const totalUnrealizedPnL = positions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0);
return { return {
timestamp: new Date(), timestamp: new Date(),
totalValue, totalValue,
cashBalance: this.cashBalance, cashBalance: this.cashBalance,
positions, positions,
totalReturn: totalUnrealizedPnL, // Simplified - should include realized gains totalReturn: totalUnrealizedPnL, // Simplified - should include realized gains
totalReturnPercent: (totalUnrealizedPnL / (totalValue - totalUnrealizedPnL)) * 100, totalReturnPercent: (totalUnrealizedPnL / (totalValue - totalUnrealizedPnL)) * 100,
dayChange: 0, // TODO: Calculate from previous day dayChange: 0, // TODO: Calculate from previous day
dayChangePercent: 0 dayChangePercent: 0
}; };
} }
getTrades(symbol?: string): Trade[] { getTrades(symbol?: string): Trade[] {
if (symbol) { if (symbol) {
return this.trades.filter(trade => trade.symbol === symbol); return this.trades.filter(trade => trade.symbol === symbol);
} }
return this.trades; return this.trades;
} }
private getTotalCommissions(symbol: string): number { private getTotalCommissions(symbol: string): number {
return this.trades return this.trades
.filter(trade => trade.symbol === symbol) .filter(trade => trade.symbol === symbol)
.reduce((sum, trade) => sum + trade.commission, 0); .reduce((sum, trade) => sum + trade.commission, 0);
} }
getCashBalance(): number { getCashBalance(): number {
return this.cashBalance; return this.cashBalance;
} }
getNetLiquidationValue(): number { getNetLiquidationValue(): number {
const positions = this.getAllPositions(); const positions = this.getAllPositions();
const positionValue = positions.reduce((sum, pos) => sum + pos.marketValue, 0); const positionValue = positions.reduce((sum, pos) => sum + pos.marketValue, 0);
return positionValue + this.cashBalance; return positionValue + this.cashBalance;
} }
} }

View file

@ -1,18 +1,18 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src" "rootDir": "./src"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"],
"references": [ "references": [
{ "path": "../../libs/types" }, { "path": "../../libs/types" },
{ "path": "../../libs/config" }, { "path": "../../libs/config" },
{ "path": "../../libs/logger" }, { "path": "../../libs/logger" },
{ "path": "../../libs/utils" }, { "path": "../../libs/utils" },
{ "path": "../../libs/postgres-client" }, { "path": "../../libs/postgres-client" },
{ "path": "../../libs/event-bus" }, { "path": "../../libs/event-bus" },
{ "path": "../../libs/shutdown" } { "path": "../../libs/shutdown" }
] ]
} }

View file

@ -1,18 +1,18 @@
{ {
"extends": ["//"], "extends": ["//"],
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": [ "dependsOn": [
"@stock-bot/types#build", "@stock-bot/types#build",
"@stock-bot/config#build", "@stock-bot/config#build",
"@stock-bot/logger#build", "@stock-bot/logger#build",
"@stock-bot/utils#build", "@stock-bot/utils#build",
"@stock-bot/postgres-client#build", "@stock-bot/postgres-client#build",
"@stock-bot/event-bus#build", "@stock-bot/event-bus#build",
"@stock-bot/shutdown#build" "@stock-bot/shutdown#build"
], ],
"outputs": ["dist/**"], "outputs": ["dist/**"],
"inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"] "inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"]
} }
} }
} }

View file

@ -1,26 +1,26 @@
{ {
"name": "@stock-bot/processing-service", "name": "@stock-bot/processing-service",
"version": "1.0.0", "version": "1.0.0",
"description": "Combined data processing and technical indicators service", "description": "Combined data processing and technical indicators service",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"devvvvv": "bun --watch src/index.ts", "devvvvv": "bun --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist --target node", "build": "bun build src/index.ts --outdir dist --target node",
"start": "bun dist/index.js", "start": "bun dist/index.js",
"test": "bun test", "test": "bun test",
"clean": "rm -rf dist" "clean": "rm -rf dist"
}, },
"dependencies": { "dependencies": {
"@stock-bot/config": "*", "@stock-bot/config": "*",
"@stock-bot/logger": "*", "@stock-bot/logger": "*",
"@stock-bot/types": "*", "@stock-bot/types": "*",
"@stock-bot/utils": "*", "@stock-bot/utils": "*",
"@stock-bot/event-bus": "*", "@stock-bot/event-bus": "*",
"@stock-bot/vector-engine": "*", "@stock-bot/vector-engine": "*",
"hono": "^4.0.0" "hono": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"
} }
} }

View file

@ -1,54 +1,54 @@
/** /**
* Processing Service - Technical indicators and data processing * Processing Service - Technical indicators and data processing
*/ */
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { loadEnvVariables } from '@stock-bot/config'; import { loadEnvVariables } from '@stock-bot/config';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
// Load environment variables // Load environment variables
loadEnvVariables(); loadEnvVariables();
const app = new Hono(); const app = new Hono();
const logger = getLogger('processing-service'); const logger = getLogger('processing-service');
const PORT = parseInt(process.env.PROCESSING_SERVICE_PORT || '3003'); const PORT = parseInt(process.env.PROCESSING_SERVICE_PORT || '3003');
// Health check endpoint // Health check endpoint
app.get('/health', (c) => { app.get('/health', (c) => {
return c.json({ return c.json({
service: 'processing-service', service: 'processing-service',
status: 'healthy', status: 'healthy',
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
}); });
// Technical indicators endpoint // Technical indicators endpoint
app.post('/api/indicators', async (c) => { app.post('/api/indicators', async (c) => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Technical indicators request', { indicators: body.indicators }); logger.info('Technical indicators request', { indicators: body.indicators });
// TODO: Implement technical indicators processing // TODO: Implement technical indicators processing
return c.json({ return c.json({
message: 'Technical indicators endpoint - not implemented yet', message: 'Technical indicators endpoint - not implemented yet',
requestedIndicators: body.indicators requestedIndicators: body.indicators
}); });
}); });
// Vectorized processing endpoint // Vectorized processing endpoint
app.post('/api/vectorized/process', async (c) => { app.post('/api/vectorized/process', async (c) => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Vectorized processing request', { dataPoints: body.data?.length }); logger.info('Vectorized processing request', { dataPoints: body.data?.length });
// TODO: Implement vectorized processing // TODO: Implement vectorized processing
return c.json({ return c.json({
message: 'Vectorized processing endpoint - not implemented yet' message: 'Vectorized processing endpoint - not implemented yet'
}); });
}); });
// Start server // Start server
serve({ serve({
fetch: app.fetch, fetch: app.fetch,
port: PORT, port: PORT,
}); });
logger.info(`Processing Service started on port ${PORT}`); logger.info(`Processing Service started on port ${PORT}`);

View file

@ -1,82 +1,82 @@
/** /**
* Technical Indicators Service * Technical Indicators Service
* Leverages @stock-bot/utils for calculations * Leverages @stock-bot/utils for calculations
*/ */
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { import {
sma, sma,
ema, ema,
rsi, rsi,
macd macd
} from '@stock-bot/utils'; } from '@stock-bot/utils';
const logger = getLogger('indicators-service'); const logger = getLogger('indicators-service');
export interface IndicatorRequest { export interface IndicatorRequest {
symbol: string; symbol: string;
data: number[]; data: number[];
indicators: string[]; indicators: string[];
parameters?: Record<string, any>; parameters?: Record<string, any>;
} }
export interface IndicatorResult { export interface IndicatorResult {
symbol: string; symbol: string;
timestamp: Date; timestamp: Date;
indicators: Record<string, number[]>; indicators: Record<string, number[]>;
} }
export class IndicatorsService { export class IndicatorsService {
async calculateIndicators(request: IndicatorRequest): Promise<IndicatorResult> { async calculateIndicators(request: IndicatorRequest): Promise<IndicatorResult> {
logger.info('Calculating indicators', { logger.info('Calculating indicators', {
symbol: request.symbol, symbol: request.symbol,
indicators: request.indicators, indicators: request.indicators,
dataPoints: request.data.length dataPoints: request.data.length
}); });
const results: Record<string, number[]> = {}; const results: Record<string, number[]> = {};
for (const indicator of request.indicators) { for (const indicator of request.indicators) {
try { try {
switch (indicator.toLowerCase()) { switch (indicator.toLowerCase()) {
case 'sma': case 'sma':
const smaPeriod = request.parameters?.smaPeriod || 20; const smaPeriod = request.parameters?.smaPeriod || 20;
results.sma = sma(request.data, smaPeriod); results.sma = sma(request.data, smaPeriod);
break; break;
case 'ema': case 'ema':
const emaPeriod = request.parameters?.emaPeriod || 20; const emaPeriod = request.parameters?.emaPeriod || 20;
results.ema = ema(request.data, emaPeriod); results.ema = ema(request.data, emaPeriod);
break; break;
case 'rsi': case 'rsi':
const rsiPeriod = request.parameters?.rsiPeriod || 14; const rsiPeriod = request.parameters?.rsiPeriod || 14;
results.rsi = rsi(request.data, rsiPeriod); results.rsi = rsi(request.data, rsiPeriod);
break; break;
case 'macd': case 'macd':
const fast = request.parameters?.macdFast || 12; const fast = request.parameters?.macdFast || 12;
const slow = request.parameters?.macdSlow || 26; const slow = request.parameters?.macdSlow || 26;
const signal = request.parameters?.macdSignal || 9; const signal = request.parameters?.macdSignal || 9;
results.macd = macd(request.data, fast, slow, signal).macd; results.macd = macd(request.data, fast, slow, signal).macd;
break; break;
case 'stochastic': case 'stochastic':
// TODO: Implement stochastic oscillator // TODO: Implement stochastic oscillator
logger.warn('Stochastic oscillator not implemented yet'); logger.warn('Stochastic oscillator not implemented yet');
break; break;
default: default:
logger.warn('Unknown indicator requested', { indicator }); logger.warn('Unknown indicator requested', { indicator });
} }
} catch (error) { } catch (error) {
logger.error('Error calculating indicator', { indicator, error }); logger.error('Error calculating indicator', { indicator, error });
} }
} }
return { return {
symbol: request.symbol, symbol: request.symbol,
timestamp: new Date(), timestamp: new Date(),
indicators: results indicators: results
}; };
} }
} }

View file

@ -1,20 +1,20 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src" "rootDir": "./src"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"],
"references": [ "references": [
{ "path": "../../libs/types" }, { "path": "../../libs/types" },
{ "path": "../../libs/config" }, { "path": "../../libs/config" },
{ "path": "../../libs/logger" }, { "path": "../../libs/logger" },
{ "path": "../../libs/utils" }, { "path": "../../libs/utils" },
{ "path": "../../libs/data-frame" }, { "path": "../../libs/data-frame" },
{ "path": "../../libs/vector-engine" }, { "path": "../../libs/vector-engine" },
{ "path": "../../libs/mongodb-client" }, { "path": "../../libs/mongodb-client" },
{ "path": "../../libs/event-bus" }, { "path": "../../libs/event-bus" },
{ "path": "../../libs/shutdown" } { "path": "../../libs/shutdown" }
] ]
} }

View file

@ -1,20 +1,20 @@
{ {
"extends": ["//"], "extends": ["//"],
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": [ "dependsOn": [
"@stock-bot/types#build", "@stock-bot/types#build",
"@stock-bot/config#build", "@stock-bot/config#build",
"@stock-bot/logger#build", "@stock-bot/logger#build",
"@stock-bot/utils#build", "@stock-bot/utils#build",
"@stock-bot/data-frame#build", "@stock-bot/data-frame#build",
"@stock-bot/vector-engine#build", "@stock-bot/vector-engine#build",
"@stock-bot/mongodb-client#build", "@stock-bot/mongodb-client#build",
"@stock-bot/event-bus#build", "@stock-bot/event-bus#build",
"@stock-bot/shutdown#build" "@stock-bot/shutdown#build"
], ],
"outputs": ["dist/**"], "outputs": ["dist/**"],
"inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"] "inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"]
} }
} }
} }

View file

@ -1,33 +1,33 @@
{ {
"name": "@stock-bot/strategy-service", "name": "@stock-bot/strategy-service",
"version": "1.0.0", "version": "1.0.0",
"description": "Combined strategy execution and multi-mode backtesting service", "description": "Combined strategy execution and multi-mode backtesting service",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"devvvvv": "bun --watch src/index.ts", "devvvvv": "bun --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist --target node", "build": "bun build src/index.ts --outdir dist --target node",
"start": "bun dist/index.js", "start": "bun dist/index.js",
"test": "bun test", "clean": "rm -rf dist", "test": "bun test", "clean": "rm -rf dist",
"backtest": "bun src/cli/index.ts", "backtest": "bun src/cli/index.ts",
"optimize": "bun src/cli/index.ts optimize", "optimize": "bun src/cli/index.ts optimize",
"cli": "bun src/cli/index.ts" "cli": "bun src/cli/index.ts"
}, },
"dependencies": { "dependencies": {
"@stock-bot/config": "*", "@stock-bot/config": "*",
"@stock-bot/logger": "*", "@stock-bot/logger": "*",
"@stock-bot/types": "*", "@stock-bot/types": "*",
"@stock-bot/utils": "*", "@stock-bot/utils": "*",
"@stock-bot/event-bus": "*", "@stock-bot/event-bus": "*",
"@stock-bot/strategy-engine": "*", "@stock-bot/strategy-engine": "*",
"@stock-bot/vector-engine": "*", "@stock-bot/vector-engine": "*",
"@stock-bot/data-frame": "*", "@stock-bot/data-frame": "*",
"@stock-bot/questdb-client": "*", "@stock-bot/questdb-client": "*",
"hono": "^4.0.0", "hono": "^4.0.0",
"commander": "^11.0.0" "commander": "^11.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"typescript": "^5.0.0" "typescript": "^5.0.0"
} }
} }

View file

@ -1,75 +1,75 @@
/** /**
* Event-Driven Backtesting Mode * Event-Driven Backtesting Mode
* Processes data point by point with realistic order execution * Processes data point by point with realistic order execution
*/ */
import { ExecutionMode, Order, OrderResult, MarketData } from '../../framework/execution-mode'; import { ExecutionMode, Order, OrderResult, MarketData } from '../../framework/execution-mode';
export interface BacktestConfig { export interface BacktestConfig {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
initialCapital: number; initialCapital: number;
slippageModel?: string; slippageModel?: string;
commissionModel?: string; commissionModel?: string;
} }
export class EventMode extends ExecutionMode { export class EventMode extends ExecutionMode {
name = 'event-driven'; name = 'event-driven';
private simulationTime: Date; private simulationTime: Date;
private historicalData: Map<string, MarketData[]> = new Map(); private historicalData: Map<string, MarketData[]> = new Map();
constructor(private config: BacktestConfig) { constructor(private config: BacktestConfig) {
super(); super();
this.simulationTime = config.startDate; this.simulationTime = config.startDate;
} }
async executeOrder(order: Order): Promise<OrderResult> { async executeOrder(order: Order): Promise<OrderResult> {
this.logger.debug('Simulating order execution', { this.logger.debug('Simulating order execution', {
orderId: order.id, orderId: order.id,
simulationTime: this.simulationTime simulationTime: this.simulationTime
}); });
// TODO: Implement realistic order simulation // TODO: Implement realistic order simulation
// Include slippage, commission, market impact // Include slippage, commission, market impact
const simulatedResult: OrderResult = { const simulatedResult: OrderResult = {
orderId: order.id, orderId: order.id,
symbol: order.symbol, symbol: order.symbol,
executedQuantity: order.quantity, executedQuantity: order.quantity,
executedPrice: 100, // TODO: Get realistic price executedPrice: 100, // TODO: Get realistic price
commission: 1.0, // TODO: Calculate based on commission model commission: 1.0, // TODO: Calculate based on commission model
slippage: 0.01, // TODO: Calculate based on slippage model slippage: 0.01, // TODO: Calculate based on slippage model
timestamp: this.simulationTime, timestamp: this.simulationTime,
executionTime: 50 // ms executionTime: 50 // ms
}; };
return simulatedResult; return simulatedResult;
} }
getCurrentTime(): Date { getCurrentTime(): Date {
return this.simulationTime; return this.simulationTime;
} }
async getMarketData(symbol: string): Promise<MarketData> { async getMarketData(symbol: string): Promise<MarketData> {
const data = this.historicalData.get(symbol) || []; const data = this.historicalData.get(symbol) || [];
const currentData = data.find(d => d.timestamp <= this.simulationTime); const currentData = data.find(d => d.timestamp <= this.simulationTime);
if (!currentData) { if (!currentData) {
throw new Error(`No market data available for ${symbol} at ${this.simulationTime}`); throw new Error(`No market data available for ${symbol} at ${this.simulationTime}`);
} }
return currentData; return currentData;
} }
async publishEvent(event: string, data: any): Promise<void> { async publishEvent(event: string, data: any): Promise<void> {
// In-memory event bus for simulation // In-memory event bus for simulation
this.logger.debug('Publishing simulation event', { event, data }); this.logger.debug('Publishing simulation event', { event, data });
} }
// Simulation control methods // Simulation control methods
advanceTime(newTime: Date): void { advanceTime(newTime: Date): void {
this.simulationTime = newTime; this.simulationTime = newTime;
} }
loadHistoricalData(symbol: string, data: MarketData[]): void { loadHistoricalData(symbol: string, data: MarketData[]): void {
this.historicalData.set(symbol, data); this.historicalData.set(symbol, data);
} }
} }

View file

@ -1,422 +1,422 @@
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { EventBus } from '@stock-bot/event-bus'; import { EventBus } from '@stock-bot/event-bus';
import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine'; import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine';
import { DataFrame } from '@stock-bot/data-frame'; import { DataFrame } from '@stock-bot/data-frame';
import { ExecutionMode, BacktestContext, BacktestResult } from '../framework/execution-mode'; import { ExecutionMode, BacktestContext, BacktestResult } from '../framework/execution-mode';
import { EventMode } from './event-mode'; import { EventMode } from './event-mode';
import VectorizedMode from './vectorized-mode'; import VectorizedMode from './vectorized-mode';
import { create } from 'domain'; import { create } from 'domain';
export interface HybridModeConfig { export interface HybridModeConfig {
vectorizedThreshold: number; // Switch to vectorized if data points > threshold vectorizedThreshold: number; // Switch to vectorized if data points > threshold
warmupPeriod: number; // Number of periods for initial vectorized calculation warmupPeriod: number; // Number of periods for initial vectorized calculation
eventDrivenRealtime: boolean; // Use event-driven for real-time portions eventDrivenRealtime: boolean; // Use event-driven for real-time portions
optimizeIndicators: boolean; // Pre-calculate indicators vectorized optimizeIndicators: boolean; // Pre-calculate indicators vectorized
batchSize: number; // Size of batches for hybrid processing batchSize: number; // Size of batches for hybrid processing
} }
export class HybridMode extends ExecutionMode { export class HybridMode extends ExecutionMode {
private vectorEngine: VectorEngine; private vectorEngine: VectorEngine;
private eventMode: EventMode; private eventMode: EventMode;
private vectorizedMode: VectorizedMode; private vectorizedMode: VectorizedMode;
private config: HybridModeConfig; private config: HybridModeConfig;
private precomputedIndicators: Map<string, number[]> = new Map(); private precomputedIndicators: Map<string, number[]> = new Map();
private currentIndex: number = 0; private currentIndex: number = 0;
constructor( constructor(
context: BacktestContext, context: BacktestContext,
eventBus: EventBus, eventBus: EventBus,
config: HybridModeConfig = {} config: HybridModeConfig = {}
) { ) {
super(context, eventBus); super(context, eventBus);
this.config = { this.config = {
vectorizedThreshold: 50000, vectorizedThreshold: 50000,
warmupPeriod: 1000, warmupPeriod: 1000,
eventDrivenRealtime: true, eventDrivenRealtime: true,
optimizeIndicators: true, optimizeIndicators: true,
batchSize: 10000, batchSize: 10000,
...config ...config
}; };
this.vectorEngine = new VectorEngine(); this.vectorEngine = new VectorEngine();
this.eventMode = new EventMode(context, eventBus); this.eventMode = new EventMode(context, eventBus);
this.vectorizedMode = new VectorizedMode(context, eventBus); this.vectorizedMode = new VectorizedMode(context, eventBus);
this.logger = getLogger('hybrid-mode'); this.logger = getLogger('hybrid-mode');
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
await super.initialize(); await super.initialize();
// Initialize both modes // Initialize both modes
await this.eventMode.initialize(); await this.eventMode.initialize();
await this.vectorizedMode.initialize(); await this.vectorizedMode.initialize();
this.logger.info('Hybrid mode initialized', { this.logger.info('Hybrid mode initialized', {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
config: this.config config: this.config
}); });
} }
async execute(): Promise<BacktestResult> { async execute(): Promise<BacktestResult> {
const startTime = Date.now(); const startTime = Date.now();
this.logger.info('Starting hybrid backtest execution'); this.logger.info('Starting hybrid backtest execution');
try { try {
// Determine execution strategy based on data size // Determine execution strategy based on data size
const dataSize = await this.estimateDataSize(); const dataSize = await this.estimateDataSize();
if (dataSize <= this.config.vectorizedThreshold) { if (dataSize <= this.config.vectorizedThreshold) {
// Small dataset: use pure vectorized approach // Small dataset: use pure vectorized approach
this.logger.info('Using pure vectorized approach for small dataset', { dataSize }); this.logger.info('Using pure vectorized approach for small dataset', { dataSize });
return await this.vectorizedMode.execute(); return await this.vectorizedMode.execute();
} }
// Large dataset: use hybrid approach // Large dataset: use hybrid approach
this.logger.info('Using hybrid approach for large dataset', { dataSize }); this.logger.info('Using hybrid approach for large dataset', { dataSize });
return await this.executeHybrid(startTime); return await this.executeHybrid(startTime);
} catch (error) { } catch (error) {
this.logger.error('Hybrid backtest failed', { this.logger.error('Hybrid backtest failed', {
error, error,
backtestId: this.context.backtestId backtestId: this.context.backtestId
}); });
await this.eventBus.publishBacktestUpdate( await this.eventBus.publishBacktestUpdate(
this.context.backtestId, this.context.backtestId,
0, 0,
{ status: 'failed', error: error.message } { status: 'failed', error: error.message }
); );
throw error; throw error;
} }
} }
private async executeHybrid(startTime: number): Promise<BacktestResult> { private async executeHybrid(startTime: number): Promise<BacktestResult> {
// Phase 1: Vectorized warmup and indicator pre-computation // Phase 1: Vectorized warmup and indicator pre-computation
const warmupResult = await this.executeWarmupPhase(); const warmupResult = await this.executeWarmupPhase();
// Phase 2: Event-driven processing with pre-computed indicators // Phase 2: Event-driven processing with pre-computed indicators
const eventResult = await this.executeEventPhase(warmupResult); const eventResult = await this.executeEventPhase(warmupResult);
// Phase 3: Combine results // Phase 3: Combine results
const combinedResult = this.combineResults(warmupResult, eventResult, startTime); const combinedResult = this.combineResults(warmupResult, eventResult, startTime);
await this.eventBus.publishBacktestUpdate( await this.eventBus.publishBacktestUpdate(
this.context.backtestId, this.context.backtestId,
100, 100,
{ status: 'completed', result: combinedResult } { status: 'completed', result: combinedResult }
); );
this.logger.info('Hybrid backtest completed', { this.logger.info('Hybrid backtest completed', {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
duration: Date.now() - startTime, duration: Date.now() - startTime,
totalTrades: combinedResult.trades.length, totalTrades: combinedResult.trades.length,
warmupTrades: warmupResult.trades.length, warmupTrades: warmupResult.trades.length,
eventTrades: eventResult.trades.length eventTrades: eventResult.trades.length
}); });
return combinedResult; return combinedResult;
} }
private async executeWarmupPhase(): Promise<BacktestResult> { private async executeWarmupPhase(): Promise<BacktestResult> {
this.logger.info('Executing vectorized warmup phase', { this.logger.info('Executing vectorized warmup phase', {
warmupPeriod: this.config.warmupPeriod warmupPeriod: this.config.warmupPeriod
}); });
// Load warmup data // Load warmup data
const warmupData = await this.loadWarmupData(); const warmupData = await this.loadWarmupData();
const dataFrame = this.createDataFrame(warmupData); const dataFrame = this.createDataFrame(warmupData);
// Pre-compute indicators for entire dataset if optimization is enabled // Pre-compute indicators for entire dataset if optimization is enabled
if (this.config.optimizeIndicators) { if (this.config.optimizeIndicators) {
await this.precomputeIndicators(dataFrame); await this.precomputeIndicators(dataFrame);
} }
// Run vectorized backtest on warmup period // Run vectorized backtest on warmup period
const strategyCode = this.generateStrategyCode(); const strategyCode = this.generateStrategyCode();
const vectorResult = await this.vectorEngine.executeVectorizedStrategy( const vectorResult = await this.vectorEngine.executeVectorizedStrategy(
dataFrame.head(this.config.warmupPeriod), dataFrame.head(this.config.warmupPeriod),
strategyCode strategyCode
); );
// Convert to standard format // Convert to standard format
return this.convertVectorizedResult(vectorResult, Date.now()); return this.convertVectorizedResult(vectorResult, Date.now());
} }
private async executeEventPhase(warmupResult: BacktestResult): Promise<BacktestResult> { private async executeEventPhase(warmupResult: BacktestResult): Promise<BacktestResult> {
this.logger.info('Executing event-driven phase'); this.logger.info('Executing event-driven phase');
// Set up event mode with warmup context // Set up event mode with warmup context
this.currentIndex = this.config.warmupPeriod; this.currentIndex = this.config.warmupPeriod;
// Create modified context for event phase // Create modified context for event phase
const eventContext: BacktestContext = { const eventContext: BacktestContext = {
...this.context, ...this.context,
initialPortfolio: this.extractFinalPortfolio(warmupResult) initialPortfolio: this.extractFinalPortfolio(warmupResult)
}; };
// Execute event-driven backtest for remaining data // Execute event-driven backtest for remaining data
const eventMode = new EventMode(eventContext, this.eventBus); const eventMode = new EventMode(eventContext, this.eventBus);
await eventMode.initialize(); await eventMode.initialize();
// Override indicator calculations to use pre-computed values // Override indicator calculations to use pre-computed values
if (this.config.optimizeIndicators) { if (this.config.optimizeIndicators) {
this.overrideIndicatorCalculations(eventMode); this.overrideIndicatorCalculations(eventMode);
} }
return await eventMode.execute(); return await eventMode.execute();
} }
private async precomputeIndicators(dataFrame: DataFrame): Promise<void> { private async precomputeIndicators(dataFrame: DataFrame): Promise<void> {
this.logger.info('Pre-computing indicators vectorized'); this.logger.info('Pre-computing indicators vectorized');
const close = dataFrame.getColumn('close'); const close = dataFrame.getColumn('close');
const high = dataFrame.getColumn('high'); const high = dataFrame.getColumn('high');
const low = dataFrame.getColumn('low'); const low = dataFrame.getColumn('low');
// Import technical indicators from vector engine // Import technical indicators from vector engine
const { TechnicalIndicators } = await import('@stock-bot/vector-engine'); const { TechnicalIndicators } = await import('@stock-bot/vector-engine');
// Pre-compute common indicators // Pre-compute common indicators
this.precomputedIndicators.set('sma_20', TechnicalIndicators.sma(close, 20)); this.precomputedIndicators.set('sma_20', TechnicalIndicators.sma(close, 20));
this.precomputedIndicators.set('sma_50', TechnicalIndicators.sma(close, 50)); this.precomputedIndicators.set('sma_50', TechnicalIndicators.sma(close, 50));
this.precomputedIndicators.set('ema_12', TechnicalIndicators.ema(close, 12)); this.precomputedIndicators.set('ema_12', TechnicalIndicators.ema(close, 12));
this.precomputedIndicators.set('ema_26', TechnicalIndicators.ema(close, 26)); this.precomputedIndicators.set('ema_26', TechnicalIndicators.ema(close, 26));
this.precomputedIndicators.set('rsi', TechnicalIndicators.rsi(close)); this.precomputedIndicators.set('rsi', TechnicalIndicators.rsi(close));
this.precomputedIndicators.set('atr', TechnicalIndicators.atr(high, low, close)); this.precomputedIndicators.set('atr', TechnicalIndicators.atr(high, low, close));
const macd = TechnicalIndicators.macd(close); const macd = TechnicalIndicators.macd(close);
this.precomputedIndicators.set('macd', macd.macd); this.precomputedIndicators.set('macd', macd.macd);
this.precomputedIndicators.set('macd_signal', macd.signal); this.precomputedIndicators.set('macd_signal', macd.signal);
this.precomputedIndicators.set('macd_histogram', macd.histogram); this.precomputedIndicators.set('macd_histogram', macd.histogram);
const bb = TechnicalIndicators.bollingerBands(close); const bb = TechnicalIndicators.bollingerBands(close);
this.precomputedIndicators.set('bb_upper', bb.upper); this.precomputedIndicators.set('bb_upper', bb.upper);
this.precomputedIndicators.set('bb_middle', bb.middle); this.precomputedIndicators.set('bb_middle', bb.middle);
this.precomputedIndicators.set('bb_lower', bb.lower); this.precomputedIndicators.set('bb_lower', bb.lower);
this.logger.info('Indicators pre-computed', { this.logger.info('Indicators pre-computed', {
indicators: Array.from(this.precomputedIndicators.keys()) indicators: Array.from(this.precomputedIndicators.keys())
}); });
} }
private overrideIndicatorCalculations(eventMode: EventMode): void { private overrideIndicatorCalculations(eventMode: EventMode): void {
// Override the event mode's indicator calculations to use pre-computed values // Override the event mode's indicator calculations to use pre-computed values
// This is a simplified approach - in production you'd want a more sophisticated interface // This is a simplified approach - in production you'd want a more sophisticated interface
const originalCalculateIndicators = (eventMode as any).calculateIndicators; const originalCalculateIndicators = (eventMode as any).calculateIndicators;
(eventMode as any).calculateIndicators = (symbol: string, index: number) => { (eventMode as any).calculateIndicators = (symbol: string, index: number) => {
const indicators: Record<string, number> = {}; const indicators: Record<string, number> = {};
for (const [name, values] of this.precomputedIndicators.entries()) { for (const [name, values] of this.precomputedIndicators.entries()) {
if (index < values.length) { if (index < values.length) {
indicators[name] = values[index]; indicators[name] = values[index];
} }
} }
return indicators; return indicators;
}; };
} }
private async estimateDataSize(): Promise<number> { private async estimateDataSize(): Promise<number> {
// Estimate the number of data points for the backtest period // Estimate the number of data points for the backtest period
const startTime = new Date(this.context.startDate).getTime(); const startTime = new Date(this.context.startDate).getTime();
const endTime = new Date(this.context.endDate).getTime(); const endTime = new Date(this.context.endDate).getTime();
const timeRange = endTime - startTime; const timeRange = endTime - startTime;
// Assume 1-minute intervals (60000ms) // Assume 1-minute intervals (60000ms)
const estimatedPoints = Math.floor(timeRange / 60000); const estimatedPoints = Math.floor(timeRange / 60000);
this.logger.debug('Estimated data size', { this.logger.debug('Estimated data size', {
timeRange, timeRange,
estimatedPoints, estimatedPoints,
threshold: this.config.vectorizedThreshold threshold: this.config.vectorizedThreshold
}); });
return estimatedPoints; return estimatedPoints;
} }
private async loadWarmupData(): Promise<any[]> { private async loadWarmupData(): Promise<any[]> {
// Load historical data for warmup phase // Load historical data for warmup phase
// This should load more data than just the warmup period for indicator calculations // This should load more data than just the warmup period for indicator calculations
const data = []; const data = [];
const startTime = new Date(this.context.startDate).getTime(); const startTime = new Date(this.context.startDate).getTime();
const warmupEndTime = startTime + (this.config.warmupPeriod * 60000); const warmupEndTime = startTime + (this.config.warmupPeriod * 60000);
// Add extra lookback for indicator calculations // Add extra lookback for indicator calculations
const lookbackTime = startTime - (200 * 60000); // 200 periods lookback const lookbackTime = startTime - (200 * 60000); // 200 periods lookback
for (let timestamp = lookbackTime; timestamp <= warmupEndTime; timestamp += 60000) { for (let timestamp = lookbackTime; timestamp <= warmupEndTime; timestamp += 60000) {
const basePrice = 100 + Math.sin(timestamp / 1000000) * 10; const basePrice = 100 + Math.sin(timestamp / 1000000) * 10;
const volatility = 0.02; const volatility = 0.02;
const open = basePrice + (Math.random() - 0.5) * volatility * basePrice; const open = basePrice + (Math.random() - 0.5) * volatility * basePrice;
const close = open + (Math.random() - 0.5) * volatility * basePrice; const close = open + (Math.random() - 0.5) * volatility * basePrice;
const high = Math.max(open, close) + Math.random() * volatility * basePrice; const high = Math.max(open, close) + Math.random() * volatility * basePrice;
const low = Math.min(open, close) - Math.random() * volatility * basePrice; const low = Math.min(open, close) - Math.random() * volatility * basePrice;
const volume = Math.floor(Math.random() * 10000) + 1000; const volume = Math.floor(Math.random() * 10000) + 1000;
data.push({ data.push({
timestamp, timestamp,
symbol: this.context.symbol, symbol: this.context.symbol,
open, open,
high, high,
low, low,
close, close,
volume volume
}); });
} }
return data; return data;
} }
private createDataFrame(data: any[]): DataFrame { private createDataFrame(data: any[]): DataFrame {
return new DataFrame(data, { return new DataFrame(data, {
columns: ['timestamp', 'symbol', 'open', 'high', 'low', 'close', 'volume'], columns: ['timestamp', 'symbol', 'open', 'high', 'low', 'close', 'volume'],
dtypes: { dtypes: {
timestamp: 'number', timestamp: 'number',
symbol: 'string', symbol: 'string',
open: 'number', open: 'number',
high: 'number', high: 'number',
low: 'number', low: 'number',
close: 'number', close: 'number',
volume: 'number' volume: 'number'
} }
}); });
} }
private generateStrategyCode(): string { private generateStrategyCode(): string {
// Generate strategy code based on context // Generate strategy code based on context
const strategy = this.context.strategy; const strategy = this.context.strategy;
if (strategy.type === 'sma_crossover') { if (strategy.type === 'sma_crossover') {
return 'sma_crossover'; return 'sma_crossover';
} }
return strategy.code || 'sma_crossover'; return strategy.code || 'sma_crossover';
} }
private convertVectorizedResult(vectorResult: VectorizedBacktestResult, startTime: number): BacktestResult { private convertVectorizedResult(vectorResult: VectorizedBacktestResult, startTime: number): BacktestResult {
return { return {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
strategy: this.context.strategy, strategy: this.context.strategy,
symbol: this.context.symbol, symbol: this.context.symbol,
startDate: this.context.startDate, startDate: this.context.startDate,
endDate: this.context.endDate, endDate: this.context.endDate,
mode: 'hybrid-vectorized', mode: 'hybrid-vectorized',
duration: Date.now() - startTime, duration: Date.now() - startTime,
trades: vectorResult.trades.map(trade => ({ trades: vectorResult.trades.map(trade => ({
id: `trade_${trade.entryIndex}_${trade.exitIndex}`, id: `trade_${trade.entryIndex}_${trade.exitIndex}`,
symbol: this.context.symbol, symbol: this.context.symbol,
side: trade.side, side: trade.side,
entryTime: vectorResult.timestamps[trade.entryIndex], entryTime: vectorResult.timestamps[trade.entryIndex],
exitTime: vectorResult.timestamps[trade.exitIndex], exitTime: vectorResult.timestamps[trade.exitIndex],
entryPrice: trade.entryPrice, entryPrice: trade.entryPrice,
exitPrice: trade.exitPrice, exitPrice: trade.exitPrice,
quantity: trade.quantity, quantity: trade.quantity,
pnl: trade.pnl, pnl: trade.pnl,
commission: 0, commission: 0,
slippage: 0 slippage: 0
})), })),
performance: { performance: {
totalReturn: vectorResult.metrics.totalReturns, totalReturn: vectorResult.metrics.totalReturns,
sharpeRatio: vectorResult.metrics.sharpeRatio, sharpeRatio: vectorResult.metrics.sharpeRatio,
maxDrawdown: vectorResult.metrics.maxDrawdown, maxDrawdown: vectorResult.metrics.maxDrawdown,
winRate: vectorResult.metrics.winRate, winRate: vectorResult.metrics.winRate,
profitFactor: vectorResult.metrics.profitFactor, profitFactor: vectorResult.metrics.profitFactor,
totalTrades: vectorResult.metrics.totalTrades, totalTrades: vectorResult.metrics.totalTrades,
winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length, winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length,
losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length, losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length,
avgTrade: vectorResult.metrics.avgTrade, avgTrade: vectorResult.metrics.avgTrade,
avgWin: vectorResult.trades.filter(t => t.pnl > 0) avgWin: vectorResult.trades.filter(t => t.pnl > 0)
.reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl > 0).length || 0, .reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl > 0).length || 0,
avgLoss: vectorResult.trades.filter(t => t.pnl <= 0) avgLoss: vectorResult.trades.filter(t => t.pnl <= 0)
.reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl <= 0).length || 0, .reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl <= 0).length || 0,
largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0), largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0),
largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0) largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0)
}, },
equity: vectorResult.equity, equity: vectorResult.equity,
drawdown: vectorResult.metrics.drawdown, drawdown: vectorResult.metrics.drawdown,
metadata: { metadata: {
mode: 'hybrid-vectorized', mode: 'hybrid-vectorized',
dataPoints: vectorResult.timestamps.length, dataPoints: vectorResult.timestamps.length,
signals: Object.keys(vectorResult.signals), signals: Object.keys(vectorResult.signals),
optimizations: ['vectorized_warmup', 'precomputed_indicators'] optimizations: ['vectorized_warmup', 'precomputed_indicators']
} }
}; };
} }
private extractFinalPortfolio(warmupResult: BacktestResult): any { private extractFinalPortfolio(warmupResult: BacktestResult): any {
// Extract the final portfolio state from warmup phase // Extract the final portfolio state from warmup phase
const finalEquity = warmupResult.equity[warmupResult.equity.length - 1] || 10000; const finalEquity = warmupResult.equity[warmupResult.equity.length - 1] || 10000;
return { return {
cash: finalEquity, cash: finalEquity,
positions: [], // Simplified - in production would track actual positions positions: [], // Simplified - in production would track actual positions
equity: finalEquity equity: finalEquity
}; };
} }
private combineResults(warmupResult: BacktestResult, eventResult: BacktestResult, startTime: number): BacktestResult { private combineResults(warmupResult: BacktestResult, eventResult: BacktestResult, startTime: number): BacktestResult {
// Combine results from both phases // Combine results from both phases
const combinedTrades = [...warmupResult.trades, ...eventResult.trades]; const combinedTrades = [...warmupResult.trades, ...eventResult.trades];
const combinedEquity = [...warmupResult.equity, ...eventResult.equity]; const combinedEquity = [...warmupResult.equity, ...eventResult.equity];
const combinedDrawdown = [...(warmupResult.drawdown || []), ...(eventResult.drawdown || [])]; const combinedDrawdown = [...(warmupResult.drawdown || []), ...(eventResult.drawdown || [])];
// Recalculate combined performance metrics // Recalculate combined performance metrics
const totalPnL = combinedTrades.reduce((sum, trade) => sum + trade.pnl, 0); const totalPnL = combinedTrades.reduce((sum, trade) => sum + trade.pnl, 0);
const winningTrades = combinedTrades.filter(t => t.pnl > 0); const winningTrades = combinedTrades.filter(t => t.pnl > 0);
const losingTrades = combinedTrades.filter(t => t.pnl <= 0); const losingTrades = combinedTrades.filter(t => t.pnl <= 0);
const grossProfit = winningTrades.reduce((sum, t) => sum + t.pnl, 0); const grossProfit = winningTrades.reduce((sum, t) => sum + t.pnl, 0);
const grossLoss = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0)); const grossLoss = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0));
return { return {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
strategy: this.context.strategy, strategy: this.context.strategy,
symbol: this.context.symbol, symbol: this.context.symbol,
startDate: this.context.startDate, startDate: this.context.startDate,
endDate: this.context.endDate, endDate: this.context.endDate,
mode: 'hybrid', mode: 'hybrid',
duration: Date.now() - startTime, duration: Date.now() - startTime,
trades: combinedTrades, trades: combinedTrades,
performance: { performance: {
totalReturn: (combinedEquity[combinedEquity.length - 1] - combinedEquity[0]) / combinedEquity[0], totalReturn: (combinedEquity[combinedEquity.length - 1] - combinedEquity[0]) / combinedEquity[0],
sharpeRatio: eventResult.performance.sharpeRatio, // Use event result for more accurate calculation sharpeRatio: eventResult.performance.sharpeRatio, // Use event result for more accurate calculation
maxDrawdown: Math.max(...combinedDrawdown), maxDrawdown: Math.max(...combinedDrawdown),
winRate: winningTrades.length / combinedTrades.length, winRate: winningTrades.length / combinedTrades.length,
profitFactor: grossLoss !== 0 ? grossProfit / grossLoss : Infinity, profitFactor: grossLoss !== 0 ? grossProfit / grossLoss : Infinity,
totalTrades: combinedTrades.length, totalTrades: combinedTrades.length,
winningTrades: winningTrades.length, winningTrades: winningTrades.length,
losingTrades: losingTrades.length, losingTrades: losingTrades.length,
avgTrade: totalPnL / combinedTrades.length, avgTrade: totalPnL / combinedTrades.length,
avgWin: grossProfit / winningTrades.length || 0, avgWin: grossProfit / winningTrades.length || 0,
avgLoss: grossLoss / losingTrades.length || 0, avgLoss: grossLoss / losingTrades.length || 0,
largestWin: Math.max(...combinedTrades.map(t => t.pnl), 0), largestWin: Math.max(...combinedTrades.map(t => t.pnl), 0),
largestLoss: Math.min(...combinedTrades.map(t => t.pnl), 0) largestLoss: Math.min(...combinedTrades.map(t => t.pnl), 0)
}, },
equity: combinedEquity, equity: combinedEquity,
drawdown: combinedDrawdown, drawdown: combinedDrawdown,
metadata: { metadata: {
mode: 'hybrid', mode: 'hybrid',
phases: ['vectorized-warmup', 'event-driven'], phases: ['vectorized-warmup', 'event-driven'],
warmupPeriod: this.config.warmupPeriod, warmupPeriod: this.config.warmupPeriod,
optimizations: ['precomputed_indicators', 'hybrid_execution'], optimizations: ['precomputed_indicators', 'hybrid_execution'],
warmupTrades: warmupResult.trades.length, warmupTrades: warmupResult.trades.length,
eventTrades: eventResult.trades.length eventTrades: eventResult.trades.length
} }
}; };
} }
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
await super.cleanup(); await super.cleanup();
await this.eventMode.cleanup(); await this.eventMode.cleanup();
await this.vectorizedMode.cleanup(); await this.vectorizedMode.cleanup();
this.precomputedIndicators.clear(); this.precomputedIndicators.clear();
this.logger.info('Hybrid mode cleanup completed'); this.logger.info('Hybrid mode cleanup completed');
} }
} }
export default HybridMode; export default HybridMode;

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