added initial py analytics / rust core / ts orchestrator services
This commit is contained in:
parent
680b5fd2ae
commit
c862ed496b
62 changed files with 13459 additions and 0 deletions
367
apps/stock/orchestrator/src/paper/PaperTradingManager.ts
Normal file
367
apps/stock/orchestrator/src/paper/PaperTradingManager.ts
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
import { logger } from '@stock-bot/logger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { OrderRequest, Position } from '../types';
|
||||
import { ExecutionService } from '../services/ExecutionService';
|
||||
|
||||
interface VirtualAccount {
|
||||
balance: number;
|
||||
buyingPower: number;
|
||||
positions: Map<string, VirtualPosition>;
|
||||
orders: Map<string, VirtualOrder>;
|
||||
trades: VirtualTrade[];
|
||||
equity: number;
|
||||
marginUsed: number;
|
||||
}
|
||||
|
||||
interface VirtualPosition {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
marketValue: number;
|
||||
unrealizedPnl: number;
|
||||
realizedPnl: number;
|
||||
}
|
||||
|
||||
interface VirtualOrder {
|
||||
id: string;
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
quantity: number;
|
||||
orderType: string;
|
||||
limitPrice?: number;
|
||||
status: string;
|
||||
submittedAt: Date;
|
||||
}
|
||||
|
||||
interface VirtualTrade {
|
||||
orderId: string;
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
quantity: number;
|
||||
price: number;
|
||||
commission: number;
|
||||
timestamp: Date;
|
||||
pnl?: number;
|
||||
}
|
||||
|
||||
export class PaperTradingManager extends EventEmitter {
|
||||
private account: VirtualAccount;
|
||||
private marketPrices = new Map<string, { bid: number; ask: number }>();
|
||||
private readonly COMMISSION_RATE = 0.001; // 0.1%
|
||||
private readonly MARGIN_REQUIREMENT = 0.25; // 25% margin requirement
|
||||
|
||||
constructor(
|
||||
private executionService: ExecutionService,
|
||||
initialBalance: number = 100000
|
||||
) {
|
||||
super();
|
||||
|
||||
this.account = {
|
||||
balance: initialBalance,
|
||||
buyingPower: initialBalance * (1 / this.MARGIN_REQUIREMENT),
|
||||
positions: new Map(),
|
||||
orders: new Map(),
|
||||
trades: [],
|
||||
equity: initialBalance,
|
||||
marginUsed: 0
|
||||
};
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
// Listen for market data updates to track prices
|
||||
// In real implementation, would connect to market data service
|
||||
}
|
||||
|
||||
updateMarketPrice(symbol: string, bid: number, ask: number): void {
|
||||
this.marketPrices.set(symbol, { bid, ask });
|
||||
|
||||
// Update position values
|
||||
const position = this.account.positions.get(symbol);
|
||||
if (position) {
|
||||
const midPrice = (bid + ask) / 2;
|
||||
position.marketValue = position.quantity * midPrice;
|
||||
position.unrealizedPnl = position.quantity * (midPrice - position.averagePrice);
|
||||
}
|
||||
|
||||
// Update account equity
|
||||
this.updateAccountEquity();
|
||||
}
|
||||
|
||||
async executeOrder(order: OrderRequest): Promise<any> {
|
||||
// Validate order
|
||||
const validation = this.validateOrder(order);
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
status: 'rejected',
|
||||
reason: validation.reason
|
||||
};
|
||||
}
|
||||
|
||||
// Check buying power
|
||||
const requiredCapital = this.calculateRequiredCapital(order);
|
||||
if (requiredCapital > this.account.buyingPower) {
|
||||
return {
|
||||
status: 'rejected',
|
||||
reason: 'Insufficient buying power'
|
||||
};
|
||||
}
|
||||
|
||||
// Create virtual order
|
||||
const virtualOrder: VirtualOrder = {
|
||||
id: `paper_${Date.now()}`,
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
quantity: order.quantity,
|
||||
orderType: order.orderType,
|
||||
limitPrice: order.limitPrice,
|
||||
status: 'pending',
|
||||
submittedAt: new Date()
|
||||
};
|
||||
|
||||
this.account.orders.set(virtualOrder.id, virtualOrder);
|
||||
|
||||
// Simulate order execution based on type
|
||||
if (order.orderType === 'market') {
|
||||
await this.executeMarketOrder(virtualOrder);
|
||||
} else if (order.orderType === 'limit') {
|
||||
// Limit orders would be checked periodically
|
||||
virtualOrder.status = 'accepted';
|
||||
}
|
||||
|
||||
return {
|
||||
orderId: virtualOrder.id,
|
||||
status: virtualOrder.status
|
||||
};
|
||||
}
|
||||
|
||||
private async executeMarketOrder(order: VirtualOrder): Promise<void> {
|
||||
const marketPrice = this.marketPrices.get(order.symbol);
|
||||
if (!marketPrice) {
|
||||
order.status = 'rejected';
|
||||
this.emit('orderUpdate', {
|
||||
orderId: order.id,
|
||||
status: 'rejected',
|
||||
reason: 'No market data available'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate realistic fill with slippage
|
||||
const fillPrice = order.side === 'buy'
|
||||
? marketPrice.ask * (1 + this.getSlippage(order.quantity))
|
||||
: marketPrice.bid * (1 - this.getSlippage(order.quantity));
|
||||
|
||||
const commission = fillPrice * order.quantity * this.COMMISSION_RATE;
|
||||
|
||||
// Create trade
|
||||
const trade: VirtualTrade = {
|
||||
orderId: order.id,
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
quantity: order.quantity,
|
||||
price: fillPrice,
|
||||
commission,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// Update position
|
||||
this.updatePosition(trade);
|
||||
|
||||
// Update account
|
||||
const totalCost = (fillPrice * order.quantity) + commission;
|
||||
if (order.side === 'buy') {
|
||||
this.account.balance -= totalCost;
|
||||
} else {
|
||||
this.account.balance += (fillPrice * order.quantity) - commission;
|
||||
}
|
||||
|
||||
// Record trade
|
||||
this.account.trades.push(trade);
|
||||
order.status = 'filled';
|
||||
|
||||
// Update buying power and margin
|
||||
this.updateBuyingPower();
|
||||
|
||||
// Emit events
|
||||
this.emit('fill', {
|
||||
orderId: order.id,
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
quantity: order.quantity,
|
||||
price: fillPrice,
|
||||
commission,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
this.emit('orderUpdate', {
|
||||
orderId: order.id,
|
||||
status: 'filled'
|
||||
});
|
||||
}
|
||||
|
||||
private updatePosition(trade: VirtualTrade): void {
|
||||
const position = this.account.positions.get(trade.symbol) || {
|
||||
symbol: trade.symbol,
|
||||
quantity: 0,
|
||||
averagePrice: 0,
|
||||
marketValue: 0,
|
||||
unrealizedPnl: 0,
|
||||
realizedPnl: 0
|
||||
};
|
||||
|
||||
const oldQuantity = position.quantity;
|
||||
const oldAvgPrice = position.averagePrice;
|
||||
|
||||
if (trade.side === 'buy') {
|
||||
// Adding to position
|
||||
const newQuantity = oldQuantity + trade.quantity;
|
||||
position.averagePrice = oldQuantity >= 0
|
||||
? ((oldQuantity * oldAvgPrice) + (trade.quantity * trade.price)) / newQuantity
|
||||
: trade.price;
|
||||
position.quantity = newQuantity;
|
||||
} else {
|
||||
// Reducing position
|
||||
const newQuantity = oldQuantity - trade.quantity;
|
||||
|
||||
if (oldQuantity > 0) {
|
||||
// Realize P&L on closed portion
|
||||
const realizedPnl = trade.quantity * (trade.price - oldAvgPrice) - trade.commission;
|
||||
position.realizedPnl += realizedPnl;
|
||||
trade.pnl = realizedPnl;
|
||||
}
|
||||
|
||||
position.quantity = newQuantity;
|
||||
|
||||
if (Math.abs(newQuantity) < 0.0001) {
|
||||
// Position closed
|
||||
this.account.positions.delete(trade.symbol);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.account.positions.set(trade.symbol, position);
|
||||
}
|
||||
|
||||
private validateOrder(order: OrderRequest): { valid: boolean; reason?: string } {
|
||||
if (order.quantity <= 0) {
|
||||
return { valid: false, reason: 'Invalid quantity' };
|
||||
}
|
||||
|
||||
if (order.orderType === 'limit' && !order.limitPrice) {
|
||||
return { valid: false, reason: 'Limit price required for limit orders' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
private calculateRequiredCapital(order: OrderRequest): number {
|
||||
const marketPrice = this.marketPrices.get(order.symbol);
|
||||
if (!marketPrice) return Infinity;
|
||||
|
||||
const price = order.side === 'buy' ? marketPrice.ask : marketPrice.bid;
|
||||
const notional = price * order.quantity;
|
||||
const commission = notional * this.COMMISSION_RATE;
|
||||
const marginRequired = notional * this.MARGIN_REQUIREMENT;
|
||||
|
||||
return order.side === 'buy' ? marginRequired + commission : commission;
|
||||
}
|
||||
|
||||
private updateBuyingPower(): void {
|
||||
let totalMarginUsed = 0;
|
||||
|
||||
for (const position of this.account.positions.values()) {
|
||||
totalMarginUsed += Math.abs(position.marketValue) * this.MARGIN_REQUIREMENT;
|
||||
}
|
||||
|
||||
this.account.marginUsed = totalMarginUsed;
|
||||
this.account.buyingPower = (this.account.equity - totalMarginUsed) / this.MARGIN_REQUIREMENT;
|
||||
}
|
||||
|
||||
private updateAccountEquity(): void {
|
||||
let totalUnrealizedPnl = 0;
|
||||
|
||||
for (const position of this.account.positions.values()) {
|
||||
totalUnrealizedPnl += position.unrealizedPnl;
|
||||
}
|
||||
|
||||
this.account.equity = this.account.balance + totalUnrealizedPnl;
|
||||
}
|
||||
|
||||
private getSlippage(quantity: number): number {
|
||||
// Simple slippage model - increases with order size
|
||||
const baseSlippage = 0.0001; // 1 basis point
|
||||
const sizeImpact = quantity / 10000; // Impact increases with size
|
||||
return baseSlippage + (sizeImpact * 0.0001);
|
||||
}
|
||||
|
||||
checkLimitOrders(): void {
|
||||
// Called periodically to check if limit orders can be filled
|
||||
for (const [orderId, order] of this.account.orders) {
|
||||
if (order.status !== 'accepted' || order.orderType !== 'limit') continue;
|
||||
|
||||
const marketPrice = this.marketPrices.get(order.symbol);
|
||||
if (!marketPrice) continue;
|
||||
|
||||
const canFill = order.side === 'buy'
|
||||
? marketPrice.ask <= order.limitPrice!
|
||||
: marketPrice.bid >= order.limitPrice!;
|
||||
|
||||
if (canFill) {
|
||||
this.executeMarketOrder(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAccount(): VirtualAccount {
|
||||
return { ...this.account };
|
||||
}
|
||||
|
||||
getPosition(symbol: string): VirtualPosition | undefined {
|
||||
return this.account.positions.get(symbol);
|
||||
}
|
||||
|
||||
getAllPositions(): VirtualPosition[] {
|
||||
return Array.from(this.account.positions.values());
|
||||
}
|
||||
|
||||
getPerformanceMetrics(): any {
|
||||
const totalTrades = this.account.trades.length;
|
||||
const winningTrades = this.account.trades.filter(t => t.pnl && t.pnl > 0);
|
||||
const losingTrades = this.account.trades.filter(t => t.pnl && t.pnl < 0);
|
||||
|
||||
const totalPnl = this.account.trades.reduce((sum, t) => sum + (t.pnl || 0), 0);
|
||||
const totalCommission = this.account.trades.reduce((sum, t) => sum + t.commission, 0);
|
||||
|
||||
return {
|
||||
totalTrades,
|
||||
winningTrades: winningTrades.length,
|
||||
losingTrades: losingTrades.length,
|
||||
winRate: totalTrades > 0 ? (winningTrades.length / totalTrades) * 100 : 0,
|
||||
totalPnl,
|
||||
totalCommission,
|
||||
netPnl: totalPnl - totalCommission,
|
||||
currentEquity: this.account.equity,
|
||||
currentPositions: this.account.positions.size
|
||||
};
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
const initialBalance = this.account.balance +
|
||||
Array.from(this.account.positions.values())
|
||||
.reduce((sum, p) => sum + p.marketValue, 0);
|
||||
|
||||
this.account = {
|
||||
balance: initialBalance,
|
||||
buyingPower: initialBalance * (1 / this.MARGIN_REQUIREMENT),
|
||||
positions: new Map(),
|
||||
orders: new Map(),
|
||||
trades: [],
|
||||
equity: initialBalance,
|
||||
marginUsed: 0
|
||||
};
|
||||
|
||||
logger.info('Paper trading account reset');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue