running prettier for cleanup

This commit is contained in:
Boki 2025-06-11 10:13:25 -04:00
parent fe7733aeb5
commit d85cd58acd
151 changed files with 29158 additions and 27966 deletions

View file

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

View file

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

View file

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