65 lines
3.7 KiB
Java
65 lines
3.7 KiB
Java
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));
|
|
}
|
|
}
|