111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import { Order } from '@stock-bot/types';
|
|
import { getLogger } from '@stock-bot/logger';
|
|
|
|
export interface RiskRule {
|
|
name: string;
|
|
validate(order: Order, context: RiskContext): Promise<RiskValidationResult>;
|
|
}
|
|
|
|
export interface RiskContext {
|
|
currentPositions: Map<string, number>;
|
|
accountBalance: number;
|
|
totalExposure: number;
|
|
maxPositionSize: number;
|
|
maxDailyLoss: number;
|
|
}
|
|
|
|
export interface RiskValidationResult {
|
|
isValid: boolean;
|
|
reason?: string;
|
|
severity: 'info' | 'warning' | 'error';
|
|
}
|
|
|
|
export class RiskManager {
|
|
private logger = getLogger('risk-manager');
|
|
private rules: RiskRule[] = [];
|
|
|
|
constructor() {
|
|
this.initializeDefaultRules();
|
|
}
|
|
|
|
addRule(rule: RiskRule): void {
|
|
this.rules.push(rule);
|
|
}
|
|
|
|
async validateOrder(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
|
for (const rule of this.rules) {
|
|
const result = await rule.validate(order, context);
|
|
if (!result.isValid) {
|
|
logger.warn(`Risk rule violation: ${rule.name}`, {
|
|
order,
|
|
reason: result.reason
|
|
});
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return { isValid: true, severity: 'info' };
|
|
}
|
|
|
|
private initializeDefaultRules(): void {
|
|
// Position size rule
|
|
this.addRule({
|
|
name: 'MaxPositionSize',
|
|
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
|
const orderValue = order.quantity * (order.price || 0);
|
|
|
|
if (orderValue > context.maxPositionSize) {
|
|
return {
|
|
isValid: false,
|
|
reason: `Order size ${orderValue} exceeds maximum position size ${context.maxPositionSize}`,
|
|
severity: 'error'
|
|
};
|
|
}
|
|
|
|
return { isValid: true, severity: 'info' };
|
|
}
|
|
});
|
|
|
|
// Balance check rule
|
|
this.addRule({
|
|
name: 'SufficientBalance',
|
|
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
|
const orderValue = order.quantity * (order.price || 0);
|
|
|
|
if (order.side === 'buy' && orderValue > context.accountBalance) {
|
|
return {
|
|
isValid: false,
|
|
reason: `Insufficient balance: need ${orderValue}, have ${context.accountBalance}`,
|
|
severity: 'error'
|
|
};
|
|
}
|
|
|
|
return { isValid: true, severity: 'info' };
|
|
}
|
|
});
|
|
|
|
// Concentration risk rule
|
|
this.addRule({
|
|
name: 'ConcentrationLimit',
|
|
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
|
const currentPosition = context.currentPositions.get(order.symbol) || 0;
|
|
const newPosition = order.side === 'buy' ?
|
|
currentPosition + order.quantity :
|
|
currentPosition - order.quantity;
|
|
|
|
const positionValue = Math.abs(newPosition) * (order.price || 0);
|
|
const concentrationRatio = positionValue / context.accountBalance;
|
|
|
|
if (concentrationRatio > 0.25) { // 25% max concentration
|
|
return {
|
|
isValid: false,
|
|
reason: `Position concentration ${(concentrationRatio * 100).toFixed(2)}% exceeds 25% limit`,
|
|
severity: 'warning'
|
|
};
|
|
}
|
|
|
|
return { isValid: true, severity: 'info' };
|
|
}
|
|
});
|
|
}
|
|
}
|