linxus fs fixes
This commit is contained in:
parent
ac23b70146
commit
0b7846fe67
292 changed files with 41947 additions and 41947 deletions
120
.dockerignore
120
.dockerignore
|
|
@ -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
224
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
46
.vscode/settings.json
vendored
|
|
@ -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
38
.vscode/tasks.json
vendored
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
||||||
84
apps/dashboard/.gitignore
vendored
84
apps/dashboard/.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"@tailwindcss/postcss": {}
|
"@tailwindcss/postcss": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
apps/dashboard/.vscode/extensions.json
vendored
8
apps/dashboard/.vscode/extensions.json
vendored
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
apps/dashboard/.vscode/launch.json
vendored
40
apps/dashboard/.vscode/launch.json
vendored
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
84
apps/dashboard/.vscode/tasks.json
vendored
84
apps/dashboard/.vscode/tasks.json
vendored
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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' }
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
/* Market Data specific styles */
|
/* Market Data specific styles */
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
/* Portfolio specific styles */
|
/* Portfolio specific styles */
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
/* Risk Management specific styles */
|
/* Risk Management specific styles */
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
/* Settings specific styles */
|
/* Settings specific styles */
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
/* Strategies specific styles */
|
/* Strategies specific styles */
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: [],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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'
|
||||||
// }
|
// }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
// }
|
// }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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__/**"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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__/**"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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__/**"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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__/**"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue