package com.quantai.trader.risk; import com.quantai.trader.domain.TraderRiskDecision; import com.quantai.trader.enums.TraderActionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.Map; @Service public class TraderRiskGate { private static final Logger log = LoggerFactory.getLogger(TraderRiskGate.class); public TraderRiskDecision evaluate(RiskGateInput input) { TraderRiskDecision decision; if (input.riskLimits().killSwitchActive() && input.pmDecision().candidateAction().increasesExposure()) { decision = block(input, "KILL_SWITCH_ACTIVE"); } else if (input.riskLimits().executionBlocked()) { decision = block(input, input.riskLimits().executionBlocker()); } else if (input.accountState().dailyDrawdownBps().compareTo(input.riskLimits().maxDailyLossBps()) >= 0) { decision = block(input, "MAX_DAILY_LOSS"); } else if (input.accountState().portfolioExposureRatio().compareTo(input.riskLimits().maxTotalExposureRatio()) >= 0 && input.pmDecision().candidateAction().increasesExposure()) { decision = block(input, "MAX_TOTAL_EXPOSURE"); } else if (input.positionState().liquidationBufferBps().compareTo(input.riskLimits().minLiquidationBufferBps()) < 0) { decision = forceClose(input, "LIQUIDATION_BUFFER_LOW"); } else if (!input.snapshot().dataReady()) { decision = block(input, "DATA_NOT_READY"); } else if (input.executionState().apiErrorCount() >= input.riskLimits().maxApiErrorCount()) { decision = block(input, "EXCHANGE_UNSTABLE"); } else if (input.executionState().exchangeLatencyMs() > input.riskLimits().maxExchangeLatencyMs()) { decision = block(input, "EXCHANGE_LATENCY_HIGH"); } else { decision = allow(input); } log.info("event=trader.risk.decided runId={} cycleId={} originalAction={} finalAction={} allow={} blocker={}", decision.runId(), decision.cycleId(), decision.originalAction(), decision.finalAction(), decision.allowAction(), decision.blocker()); return decision; } private TraderRiskDecision allow(RiskGateInput input) { return new TraderRiskDecision("risk_" + input.pmDecision().cycleId(), input.pmDecision().runId(), input.pmDecision().cycleId(), input.pmDecision().pmDecisionId(), true, input.pmDecision().candidateAction(), input.pmDecision().candidateAction(), null, Map.of("risk", "pass")); } private TraderRiskDecision block(RiskGateInput input, String blocker) { if (blocker == null || blocker.isBlank()) { blocker = "EXECUTION_BLOCKED"; } TraderActionType finalAction = input.pmDecision().candidateAction().increasesExposure() ? TraderActionType.WAIT : input.pmDecision().candidateAction(); return new TraderRiskDecision("risk_" + input.pmDecision().cycleId(), input.pmDecision().runId(), input.pmDecision().cycleId(), input.pmDecision().pmDecisionId(), false, input.pmDecision().candidateAction(), finalAction, blocker, Map.of("blocker", blocker)); } private TraderRiskDecision forceClose(RiskGateInput input, String blocker) { TraderActionType finalAction = input.positionState().side().isShort() ? TraderActionType.CLOSE_SHORT : TraderActionType.CLOSE_LONG; return new TraderRiskDecision("risk_" + input.pmDecision().cycleId(), input.pmDecision().runId(), input.pmDecision().cycleId(), input.pmDecision().pmDecisionId(), true, input.pmDecision().candidateAction(), finalAction, blocker, Map.of("blocker", blocker, "forced", true)); } }