|
@@ -11,6 +11,7 @@ import { TradingCycleManager } from './TradingCycleManager';
|
|
|
import { SignalExecutor } from '../services/SignalExecutor';
|
|
|
import { TradingSignal } from '../core/HedgingDecisionModule';
|
|
|
import { PriceTick } from '../types/market';
|
|
|
+import { RiskControlSettings } from './ConfigurationManager';
|
|
|
|
|
|
// Monitoring components
|
|
|
import {
|
|
@@ -71,6 +72,7 @@ export interface MonitoringConfig {
|
|
|
useMarketOrder?: boolean;
|
|
|
netExposureThreshold?: number;
|
|
|
};
|
|
|
+ riskControls?: RiskControlSettings;
|
|
|
}
|
|
|
|
|
|
export class MonitoringManager {
|
|
@@ -82,6 +84,8 @@ export class MonitoringManager {
|
|
|
private emergencyMitigationConfig: MonitoringConfig['emergencyMitigation'] | null = null;
|
|
|
private lastEmergencyMitigation = 0;
|
|
|
private emergencyReductionActive = false;
|
|
|
+ private netExposureTolerance: number = 0;
|
|
|
+ private maxDrawdownPercent: number = 0;
|
|
|
|
|
|
// Dependencies
|
|
|
private dataAggregator: DataAggregator;
|
|
@@ -124,7 +128,8 @@ export class MonitoringManager {
|
|
|
percent: Number(maxExposureConfig['percent'] ?? 0)
|
|
|
},
|
|
|
monitoringInterval: Number(raw['monitoringInterval'] ?? 5000),
|
|
|
- leverageMultiplier
|
|
|
+ leverageMultiplier,
|
|
|
+ maxDrawdownPercent: this.maxDrawdownPercent
|
|
|
};
|
|
|
}
|
|
|
|
|
@@ -249,6 +254,13 @@ export class MonitoringManager {
|
|
|
this.emergencyMitigationConfig.netExposureThreshold = config.emergencyMitigation.netExposureThreshold;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ if (config.riskControls) {
|
|
|
+ const tolerance = config.riskControls.netExposureTolerance ?? 0;
|
|
|
+ this.netExposureTolerance = tolerance > 0 ? tolerance : 0;
|
|
|
+ const drawdown = config.riskControls.maxDrawdownPercent ?? 0;
|
|
|
+ this.maxDrawdownPercent = drawdown > 0 ? drawdown : 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -348,6 +360,14 @@ export class MonitoringManager {
|
|
|
});
|
|
|
|
|
|
await this.closePositionsForAccount(signal.accountId, 'margin_insufficient');
|
|
|
+ try {
|
|
|
+ await this.accountManager.refreshAccountData(signal.accountId);
|
|
|
+ } catch (error) {
|
|
|
+ this.logger.warn('Failed to refresh account data after forced close', {
|
|
|
+ accountId: signal.accountId,
|
|
|
+ error
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
this.marginMonitor?.clearReduceCooldown(signal.accountId);
|
|
|
if (this.tradingCycleManager) {
|
|
@@ -367,6 +387,15 @@ export class MonitoringManager {
|
|
|
await this.reduceCounterpartPositions(signal);
|
|
|
}
|
|
|
|
|
|
+ try {
|
|
|
+ await this.accountManager.refreshAccountData(signal.accountId);
|
|
|
+ } catch (error) {
|
|
|
+ this.logger.warn('Failed to refresh account data after margin reduction', {
|
|
|
+ accountId: signal.accountId,
|
|
|
+ error
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
await this.syncCurrentBalancesToMarginMonitor();
|
|
|
|
|
|
const marginStatus = this.marginMonitor?.getAccountsStatus().find(status => status.accountId === signal.accountId);
|
|
@@ -784,6 +813,10 @@ export class MonitoringManager {
|
|
|
await this.handleMaxExposure(status);
|
|
|
});
|
|
|
|
|
|
+ this.exposureMonitor.on('drawdown', async (status: ExposureStatus) => {
|
|
|
+ await this.handleExposureDrawdown(status);
|
|
|
+ });
|
|
|
+
|
|
|
// Setup position update listener
|
|
|
this.dataAggregator.on('account_positions_updated', ({ accountId, positions }: any) => {
|
|
|
this.updateExposureData(accountId, positions);
|
|
@@ -855,6 +888,19 @@ export class MonitoringManager {
|
|
|
await this.closeAllPositionsForExposure('take_profit', status.reason ?? undefined);
|
|
|
}
|
|
|
|
|
|
+ private async handleExposureDrawdown(status: ExposureStatus): Promise<void> {
|
|
|
+ this.logger.error('📉 Exposure drawdown threshold hit!', {
|
|
|
+ drawdownPercent: ((status.drawdownPercent ?? 0) * 100).toFixed(2) + '%',
|
|
|
+ reason: status.reason
|
|
|
+ });
|
|
|
+
|
|
|
+ await this.closeAllPositionsForExposure('drawdown', status.reason ?? undefined);
|
|
|
+
|
|
|
+ if (this.tradingCycleManager) {
|
|
|
+ this.tradingCycleManager.stop();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Handle max exposure
|
|
|
*/
|
|
@@ -984,6 +1030,8 @@ export class MonitoringManager {
|
|
|
reduceFraction?: number;
|
|
|
minNotional?: number;
|
|
|
allowMarketOrders?: boolean;
|
|
|
+ toleranceSize?: number;
|
|
|
+ targetSymbol?: string;
|
|
|
}
|
|
|
): Promise<void> {
|
|
|
if (this.emergencyReductionActive && !(options?.forceReduce)) {
|
|
@@ -1088,20 +1136,27 @@ export class MonitoringManager {
|
|
|
}
|
|
|
|
|
|
if (forceReduce) {
|
|
|
- symbolTotals.forEach((totalExposure, symbol) => {
|
|
|
- if (Math.abs(totalExposure) < MIN_ORDER_SIZE) {
|
|
|
+ const toleranceSize = options?.toleranceSize ?? 0;
|
|
|
+ const symbols = options?.targetSymbol
|
|
|
+ ? [options.targetSymbol]
|
|
|
+ : Array.from(symbolTotals.keys());
|
|
|
+
|
|
|
+ symbols.forEach(symbol => {
|
|
|
+ const totalExposure = symbolTotals.get(symbol) ?? 0;
|
|
|
+ const excessExposure = Math.max(0, Math.abs(totalExposure) - toleranceSize);
|
|
|
+ if (excessExposure <= MIN_ORDER_SIZE) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const priceData: PriceTick | undefined = aggregatedData.market.prices.get(symbol);
|
|
|
const currentPrice = this.extractPrice(priceData);
|
|
|
if (!(currentPrice > 0)) {
|
|
|
- this.logger.warn('Unable to perform emergency reduction without price data', { symbol });
|
|
|
+ this.logger.warn('Unable to perform reduction without price data', { symbol });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const isNetLong = totalExposure > 0;
|
|
|
- let remaining = Math.abs(totalExposure);
|
|
|
+ let remaining = excessExposure;
|
|
|
const minAmount = Math.max(emergencyMinNotional / currentPrice, MIN_ORDER_SIZE);
|
|
|
|
|
|
const candidateAccounts = Array.from(exposuresByAccount.entries())
|
|
@@ -1139,7 +1194,7 @@ export class MonitoringManager {
|
|
|
amount,
|
|
|
price,
|
|
|
useMarketOrder: allowMarketOrders,
|
|
|
- reason: `emergency_force_reduce: ${reasonText}`,
|
|
|
+ reason: `exposure_force_reduce: ${reasonText}`,
|
|
|
timestamp: Date.now()
|
|
|
});
|
|
|
|
|
@@ -1147,9 +1202,10 @@ export class MonitoringManager {
|
|
|
}
|
|
|
|
|
|
if (remaining > MIN_ORDER_SIZE) {
|
|
|
- this.logger.warn('Residual exposure remains after emergency reduction attempt', {
|
|
|
+ this.logger.warn('Residual exposure remains after forced reduction attempt', {
|
|
|
symbol,
|
|
|
- remaining
|
|
|
+ remaining,
|
|
|
+ toleranceSize
|
|
|
});
|
|
|
}
|
|
|
});
|
|
@@ -1279,6 +1335,38 @@ export class MonitoringManager {
|
|
|
this.emergencyReductionActive = false;
|
|
|
}
|
|
|
|
|
|
+ public async enforceNetExposureTolerance(): Promise<boolean> {
|
|
|
+ if (this.netExposureTolerance <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { netSize } = this.computeSymbolNetExposure(this.tradingSymbol);
|
|
|
+ if (Math.abs(netSize) <= this.netExposureTolerance) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.emergencyReductionActive) {
|
|
|
+ this.logger.debug('Emergency mitigation active; skipping net exposure enforcement');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.logger.debug('Net exposure tolerance exceeded; triggering rebalance', {
|
|
|
+ netSize,
|
|
|
+ tolerance: this.netExposureTolerance
|
|
|
+ });
|
|
|
+
|
|
|
+ await this.rebalanceExposurePositions(0, 'net_tolerance', {
|
|
|
+ forceReduce: true,
|
|
|
+ reduceFraction: 1,
|
|
|
+ minNotional: this.minOrderValue,
|
|
|
+ allowMarketOrders: false,
|
|
|
+ toleranceSize: this.netExposureTolerance,
|
|
|
+ targetSymbol: this.tradingSymbol
|
|
|
+ });
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
private selectAccountForExposureOpen(
|
|
|
symbol: string,
|
|
|
totalExposure: number,
|