moved indicators to rust

This commit is contained in:
Boki 2025-07-03 16:54:43 -04:00
parent c106a719e8
commit 6df32dc18b
27 changed files with 6113 additions and 1 deletions

View file

@ -0,0 +1,290 @@
import { getLogger } from '@stock-bot/logger';
const logger = getLogger('PositionManager');
export interface Position {
symbol: string;
quantity: number;
avgPrice: number;
currentPrice?: number;
unrealizedPnl?: number;
realizedPnl: number;
openTime: Date;
lastUpdateTime: Date;
}
export interface Trade {
symbol: string;
side: 'buy' | 'sell';
quantity: number;
price: number;
commission: number;
timestamp: Date;
pnl?: number;
}
export interface PositionSizingParams {
accountBalance: number;
riskPerTrade: number; // As percentage (e.g., 0.02 for 2%)
stopLossDistance?: number; // Price distance for stop loss
maxPositionSize?: number; // Max % of account in one position
volatilityAdjustment?: boolean;
atr?: number; // For volatility-based sizing
}
/**
* Manages positions and calculates position sizes
*/
export class PositionManager {
private positions: Map<string, Position> = new Map();
private trades: Trade[] = [];
private accountBalance: number;
private initialBalance: number;
constructor(initialBalance: number = 100000) {
this.initialBalance = initialBalance;
this.accountBalance = initialBalance;
}
/**
* Update position with a new trade
*/
updatePosition(trade: Trade): Position {
const { symbol, side, quantity, price, commission } = trade;
let position = this.positions.get(symbol);
if (!position) {
// New position
position = {
symbol,
quantity: side === 'buy' ? quantity : -quantity,
avgPrice: price,
realizedPnl: -commission,
openTime: trade.timestamp,
lastUpdateTime: trade.timestamp
};
} else {
const oldQuantity = position.quantity;
const newQuantity = side === 'buy'
? oldQuantity + quantity
: oldQuantity - quantity;
if (Math.sign(oldQuantity) !== Math.sign(newQuantity) && oldQuantity !== 0) {
// Position flip or close
const closedQuantity = Math.min(Math.abs(oldQuantity), quantity);
const pnl = this.calculatePnl(
position.avgPrice,
price,
closedQuantity,
oldQuantity > 0 ? 'sell' : 'buy'
);
position.realizedPnl += pnl - commission;
trade.pnl = pnl - commission;
// Update average price if position continues
if (Math.abs(newQuantity) > 0.0001) {
position.avgPrice = price;
}
} else if (Math.sign(oldQuantity) === Math.sign(newQuantity) || oldQuantity === 0) {
// Adding to position
const totalCost = Math.abs(oldQuantity) * position.avgPrice + quantity * price;
const totalQuantity = Math.abs(oldQuantity) + quantity;
position.avgPrice = totalCost / totalQuantity;
position.realizedPnl -= commission;
}
position.quantity = newQuantity;
position.lastUpdateTime = trade.timestamp;
}
// Store or remove position
if (Math.abs(position.quantity) < 0.0001) {
this.positions.delete(symbol);
logger.info(`Closed position for ${symbol}, realized P&L: $${position.realizedPnl.toFixed(2)}`);
} else {
this.positions.set(symbol, position);
}
// Record trade
this.trades.push(trade);
// Update account balance
if (trade.pnl !== undefined) {
this.accountBalance += trade.pnl;
}
return position;
}
/**
* Get current position for a symbol
*/
getPosition(symbol: string): Position | undefined {
return this.positions.get(symbol);
}
/**
* Get position quantity
*/
getPositionQuantity(symbol: string): number {
const position = this.positions.get(symbol);
return position ? position.quantity : 0;
}
/**
* Check if has position
*/
hasPosition(symbol: string): boolean {
const position = this.positions.get(symbol);
return position !== undefined && Math.abs(position.quantity) > 0.0001;
}
/**
* Get all open positions
*/
getOpenPositions(): Position[] {
return Array.from(this.positions.values());
}
/**
* Update market prices for positions
*/
updateMarketPrices(prices: Map<string, number>): void {
for (const [symbol, position] of this.positions) {
const currentPrice = prices.get(symbol);
if (currentPrice) {
position.currentPrice = currentPrice;
position.unrealizedPnl = this.calculatePnl(
position.avgPrice,
currentPrice,
Math.abs(position.quantity),
position.quantity > 0 ? 'sell' : 'buy'
);
}
}
}
/**
* Calculate position size based on risk parameters
*/
calculatePositionSize(params: PositionSizingParams, currentPrice: number): number {
const {
accountBalance,
riskPerTrade,
stopLossDistance,
maxPositionSize = 0.25,
volatilityAdjustment = false,
atr
} = params;
let positionSize: number;
if (stopLossDistance && stopLossDistance > 0) {
// Risk-based position sizing
const riskAmount = accountBalance * riskPerTrade;
positionSize = Math.floor(riskAmount / stopLossDistance);
} else if (volatilityAdjustment && atr) {
// Volatility-based position sizing
const riskAmount = accountBalance * riskPerTrade;
const stopDistance = atr * 2; // 2 ATR stop
positionSize = Math.floor(riskAmount / stopDistance);
} else {
// Fixed percentage position sizing
const positionValue = accountBalance * riskPerTrade * 10; // Simplified
positionSize = Math.floor(positionValue / currentPrice);
}
// Apply max position size limit
const maxShares = Math.floor((accountBalance * maxPositionSize) / currentPrice);
positionSize = Math.min(positionSize, maxShares);
// Ensure minimum position size
return Math.max(1, positionSize);
}
/**
* Calculate Kelly Criterion position size
*/
calculateKellySize(winRate: number, avgWin: number, avgLoss: number, currentPrice: number): number {
if (avgLoss === 0) return 0;
const b = avgWin / avgLoss;
const p = winRate;
const q = 1 - p;
const kelly = (p * b - q) / b;
// Apply Kelly fraction (usually 0.25 to be conservative)
const kellyFraction = 0.25;
const percentageOfCapital = Math.max(0, Math.min(0.25, kelly * kellyFraction));
const positionValue = this.accountBalance * percentageOfCapital;
return Math.max(1, Math.floor(positionValue / currentPrice));
}
/**
* Get performance metrics
*/
getPerformanceMetrics() {
const totalTrades = this.trades.length;
const winningTrades = this.trades.filter(t => (t.pnl || 0) > 0);
const losingTrades = this.trades.filter(t => (t.pnl || 0) < 0);
const totalPnl = this.trades.reduce((sum, t) => sum + (t.pnl || 0), 0);
const unrealizedPnl = Array.from(this.positions.values())
.reduce((sum, p) => sum + (p.unrealizedPnl || 0), 0);
const winRate = totalTrades > 0 ? winningTrades.length / totalTrades : 0;
const avgWin = winningTrades.length > 0
? winningTrades.reduce((sum, t) => sum + (t.pnl || 0), 0) / winningTrades.length
: 0;
const avgLoss = losingTrades.length > 0
? Math.abs(losingTrades.reduce((sum, t) => sum + (t.pnl || 0), 0) / losingTrades.length)
: 0;
const profitFactor = avgLoss > 0 ? (avgWin * winningTrades.length) / (avgLoss * losingTrades.length) : 0;
return {
totalTrades,
winningTrades: winningTrades.length,
losingTrades: losingTrades.length,
winRate: winRate * 100,
totalPnl,
unrealizedPnl,
totalEquity: this.accountBalance + unrealizedPnl,
avgWin,
avgLoss,
profitFactor,
returnPct: ((this.accountBalance - this.initialBalance) / this.initialBalance) * 100
};
}
/**
* Get account balance
*/
getAccountBalance(): number {
return this.accountBalance;
}
/**
* Get total equity (balance + unrealized P&L)
*/
getTotalEquity(): number {
const unrealizedPnl = Array.from(this.positions.values())
.reduce((sum, p) => sum + (p.unrealizedPnl || 0), 0);
return this.accountBalance + unrealizedPnl;
}
private calculatePnl(
entryPrice: number,
exitPrice: number,
quantity: number,
side: 'buy' | 'sell'
): number {
if (side === 'sell') {
return (exitPrice - entryPrice) * quantity;
} else {
return (entryPrice - exitPrice) * quantity;
}
}
}