Rewrite trader service for V4 P0

This commit is contained in:
Codex
2026-06-26 21:53:22 +08:00
parent 2fe4077164
commit 5d210053d0
184 changed files with 2780 additions and 6945 deletions
@@ -1,163 +1,61 @@
package com.quantai.trader.risk;
import com.quantai.trader.config.TraderProperties;
import com.quantai.trader.domain.ExecutionDecision;
import com.quantai.trader.domain.ManagementDecision;
import com.quantai.trader.domain.RiskDecision;
import com.quantai.trader.domain.TraderAction;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderEntryPlan;
import com.quantai.trader.domain.TraderPositionPath;
import com.quantai.trader.domain.TraderRiskDecision;
import com.quantai.trader.enums.TraderErrorCode;
import com.quantai.trader.persistence.TraderRiskDecisionRepository;
import com.quantai.trader.enums.TraderActionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.Instant;
import java.util.Map;
@Component
@Service
public class TraderRiskGate {
private static final Logger log = LoggerFactory.getLogger(TraderRiskGate.class);
private static final MathContext MC = new MathContext(10, RoundingMode.HALF_UP);
private final TraderProperties properties;
private final TraderRiskDecisionRepository repository;
public TraderRiskGate(TraderProperties properties, TraderRiskDecisionRepository repository) {
this.properties = properties;
this.repository = repository;
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, "EXECUTION_BLOCKED");
} 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;
}
public RiskDecision evaluate(TraderDecisionCycle cycle, TraderEntryPlan entryPlan, ExecutionDecision execution) {
RiskDecision result = decide(entryPlan, execution);
TraderRiskDecision persisted = new TraderRiskDecision(
cycle.runId(),
cycle.cycleId(),
entryPlan.actionId(),
null,
entryPlan.entryAction(),
properties.getRisk().getLeverageScreen(),
entryPlan.plannedLegRatio(),
maxLossBps(entryPlan),
null,
null,
null,
null,
null,
result.allowAction(),
result.blocker(),
result.details(),
Instant.now()
);
repository.insert(persisted);
log.info(
"event=trader.risk.decision runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} actionId={} actionType={} allowAction={} blocker={}",
cycle.runId(),
cycle.cycleId(),
cycle.symbol(),
cycle.playbookId(),
cycle.playbookVersion(),
cycle.state(),
entryPlan.actionId(),
entryPlan.entryAction(),
result.allowAction(),
result.blocker()
);
return result;
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"));
}
public RiskDecision evaluateManagement(
TraderDecisionCycle cycle,
TraderAction action,
TraderPositionPath path,
ManagementDecision management
) {
if (path == null || path.totalPositionRatio() == null) {
throw new IllegalArgumentException("management risk evaluation requires an opened position path");
}
BigDecimal ratio = path.totalPositionRatio();
RiskDecision result = new RiskDecision(true, null, BigDecimal.ONE, Map.of(
"positionId", path.positionId(),
"managementReason", management.reason(),
"plannedTotalPositionRatio", ratio
));
TraderRiskDecision persisted = new TraderRiskDecision(
cycle.runId(),
cycle.cycleId(),
action.actionId(),
null,
action.actionType(),
properties.getRisk().getLeverageScreen(),
ratio,
BigDecimal.ZERO,
null,
null,
null,
null,
null,
true,
null,
result.details(),
Instant.now()
);
repository.insert(persisted);
log.info(
"event=trader.risk.decision runId={} cycleId={} symbol={} playbookId={} playbookVersion={} state={} actionId={} actionType={} allowAction={} blocker={}",
cycle.runId(),
cycle.cycleId(),
cycle.symbol(),
cycle.playbookId(),
cycle.playbookVersion(),
cycle.state(),
action.actionId(),
action.actionType(),
true,
null
);
return result;
private TraderRiskDecision block(RiskGateInput input, String blocker) {
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 RiskDecision decide(TraderEntryPlan entryPlan, ExecutionDecision execution) {
if (!entryPlan.completeForEntry()) {
return block(TraderErrorCode.TRADER_ENTRY_PLAN_INCOMPLETE.name(), Map.of("reason", "entry plan incomplete"));
}
if (execution.blocked()) {
return block(execution.blocker(), Map.of("reason", execution.reason()));
}
if (entryPlan.plannedLegRatio().compareTo(BigDecimal.ZERO) <= 0
|| entryPlan.plannedLegRatio().compareTo(properties.getSizing().getMaxSingleLegRatio()) > 0
|| entryPlan.plannedLegRatio().compareTo(properties.getSizing().getMaxTotalPositionRatio()) > 0) {
return block(TraderErrorCode.TRADER_RISK_BLOCKED.name(), Map.of(
"reason", "planned position ratio exceeds P0 risk constraints",
"plannedLegRatio", entryPlan.plannedLegRatio()
));
}
return new RiskDecision(true, null, entryPlan.riskGateScore(), Map.of(
"plannedLegRatio", entryPlan.plannedLegRatio(),
"leverageScreen", properties.getRisk().getLeverageScreen(),
"requireOneXNotNegative", properties.getRisk().isRequireOneXNotNegative()
));
}
private RiskDecision block(String blocker, Map<String, Object> details) {
return new RiskDecision(false, blocker, BigDecimal.ZERO, details);
}
private BigDecimal maxLossBps(TraderEntryPlan entryPlan) {
if (entryPlan.entryPrice() == null || entryPlan.stopPrice() == null || entryPlan.entryPrice().compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
return entryPlan.entryPrice()
.subtract(entryPlan.stopPrice(), MC)
.abs()
.divide(entryPlan.entryPrice(), MC)
.multiply(BigDecimal.valueOf(10_000), MC)
.setScale(8, RoundingMode.HALF_UP);
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));
}
}