moved indicators to rust
This commit is contained in:
parent
c106a719e8
commit
6df32dc18b
27 changed files with 6113 additions and 1 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue