206 lines
12 KiB
Java
206 lines
12 KiB
Java
package com.quantai.trader.persistence;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.quantai.trader.config.TraderProperties;
|
|
import com.quantai.trader.domain.*;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
import org.springframework.stereotype.Repository;
|
|
|
|
import java.sql.Timestamp;
|
|
import java.util.Map;
|
|
|
|
@Repository
|
|
public class JdbcTraderDecisionTraceWriter implements TraderDecisionTraceWriter {
|
|
private static final Logger log = LoggerFactory.getLogger(JdbcTraderDecisionTraceWriter.class);
|
|
|
|
private final JdbcTemplate jdbcTemplate;
|
|
private final TraderJsonCodec jsonCodec;
|
|
private final TraderProperties properties;
|
|
|
|
public JdbcTraderDecisionTraceWriter(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper, TraderProperties properties) {
|
|
this.jdbcTemplate = jdbcTemplate;
|
|
this.jsonCodec = new TraderJsonCodec(objectMapper);
|
|
this.properties = properties;
|
|
}
|
|
|
|
@Override
|
|
public void persistCycleTrace(TraderDecisionCycle cycle,
|
|
TraderMarketSnapshot snapshot,
|
|
TraderModelOutput modelOutput,
|
|
TraderPositionState positionState,
|
|
TraderAccountState accountState,
|
|
TraderExecutionState executionState,
|
|
TraderPositionManagerDecision pmDecision,
|
|
TraderRiskDecision riskDecision,
|
|
TraderAction action) {
|
|
upsertRun(cycle);
|
|
insertMarketSnapshot(snapshot);
|
|
insertPositionState(positionState, "PM_INPUT");
|
|
insertAccountState(accountState, "PM_INPUT");
|
|
insertExecutionState(executionState, "PM_INPUT");
|
|
insertCycle(cycle, positionState, riskDecision);
|
|
insertModelOutput(modelOutput, snapshot);
|
|
insertPmDecision(pmDecision);
|
|
insertRiskDecision(riskDecision);
|
|
insertAction(action);
|
|
log.info("event=trader.trace.persisted runId={} cycleId={} action={} riskAllowed={}",
|
|
cycle.runId(), cycle.cycleId(), action.actionType(), riskDecision.allowAction());
|
|
}
|
|
|
|
void upsertRun(TraderDecisionCycle cycle) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_run
|
|
(run_id, run_mode, symbol, model_bundle_version, calibration_bundle_version,
|
|
pm_config_version, execution_mode, status, config_json, started_at)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
on duplicate key update
|
|
model_bundle_version = values(model_bundle_version),
|
|
calibration_bundle_version = values(calibration_bundle_version),
|
|
pm_config_version = values(pm_config_version),
|
|
execution_mode = values(execution_mode),
|
|
status = values(status),
|
|
updated_at = current_timestamp(3)
|
|
""",
|
|
cycle.runId(), cycle.runMode().name(), cycle.symbol(), cycle.modelBundleVersion(),
|
|
cycle.calibrationBundleVersion(), cycle.pmConfigVersion(), properties.execution().mode().name(),
|
|
"RUNNING", jsonCodec.toJson(Map.of(
|
|
"serviceName", properties.serviceName(),
|
|
"artifactRoot", properties.artifact().artifactRoot(),
|
|
"redisKeyPrefix", properties.runtime().redisKeyPrefix())),
|
|
Timestamp.from(cycle.cycleTime()));
|
|
}
|
|
|
|
void insertMarketSnapshot(TraderMarketSnapshot snapshot) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_market_snapshot
|
|
(run_id, cycle_id, snapshot_id, symbol, snapshot_time, feature_version,
|
|
mark_price, index_price, spread_bps, funding_rate_bps,
|
|
depth_notional_5bps, depth_notional_10bps, depth_notional_25bps,
|
|
data_ready, feature_json, data_quality_json)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
snapshot.runId(), snapshot.cycleId(), snapshot.snapshotId(), snapshot.symbol(),
|
|
Timestamp.from(snapshot.snapshotTime()), snapshot.featureVersion(), snapshot.markPrice(),
|
|
snapshot.indexPrice(), snapshot.spreadBps(), snapshot.fundingRateBps(),
|
|
snapshot.depthNotional5Bps(), snapshot.depthNotional10Bps(), snapshot.depthNotional25Bps(),
|
|
snapshot.dataReady(), jsonCodec.toJson(snapshot.featureJson()), jsonCodec.toJson(snapshot.dataQualityJson()));
|
|
}
|
|
|
|
void insertPositionState(TraderPositionState state, String role) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_position_state
|
|
(run_id, cycle_id, position_state_id, state_role, symbol, side, position_ratio,
|
|
average_entry_price, current_price, unrealized_pnl_bps, liquidation_buffer_bps,
|
|
add_count, remaining_add_capacity, last_add_time)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
state.runId(), state.cycleId(), state.positionStateId(), role, state.symbol(), state.side().name(),
|
|
state.positionRatio(), state.averageEntryPrice(), state.currentPrice(), state.unrealizedPnlBps(),
|
|
state.liquidationBufferBps(), state.addCount(), state.remainingAddCapacity(),
|
|
state.lastAddTime() == null ? null : Timestamp.from(state.lastAddTime()));
|
|
}
|
|
|
|
void insertAccountState(TraderAccountState state, String role) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_account_state
|
|
(run_id, cycle_id, account_state_id, state_role, daily_drawdown_bps,
|
|
portfolio_exposure_ratio, remaining_symbol_capacity_ratio, consecutive_losses)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
state.runId(), state.cycleId(), state.accountStateId(), role, state.dailyDrawdownBps(),
|
|
state.portfolioExposureRatio(), state.remainingSymbolCapacityRatio(), state.consecutiveLosses());
|
|
}
|
|
|
|
void insertExecutionState(TraderExecutionState state, String role) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_execution_state
|
|
(run_id, cycle_id, execution_state_id, state_role, symbol, open_orders_json,
|
|
expected_slippage_bps, exchange_latency_ms, api_error_count, maker_fee_bps,
|
|
taker_fee_bps, min_notional, price_tick_size, lot_size_step_size,
|
|
market_lot_size_step_size, liquidity_capacity_ratio)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
state.runId(), state.cycleId(), state.executionStateId(), role, state.symbol(),
|
|
jsonCodec.toJson(state.openOrders()), state.expectedSlippageBps(), state.exchangeLatencyMs(),
|
|
state.apiErrorCount(), state.makerFeeBps(), state.takerFeeBps(), state.minNotional(),
|
|
state.priceTickSize(), state.lotSizeStepSize(), state.marketLotSizeStepSize(),
|
|
state.liquidityCapacityRatio());
|
|
}
|
|
|
|
void insertCycle(TraderDecisionCycle cycle, TraderPositionState positionState, TraderRiskDecision riskDecision) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_decision_cycle
|
|
(run_id, cycle_id, symbol, cycle_time, state, decision_status, blocker)
|
|
values (?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
cycle.runId(), cycle.cycleId(), cycle.symbol(), Timestamp.from(cycle.cycleTime()),
|
|
positionState.isFlat() ? "FLAT" : "POSITION",
|
|
riskDecision.allowAction() ? "ACTION_ALLOWED" : "ACTION_BLOCKED",
|
|
riskDecision.blocker());
|
|
}
|
|
|
|
void insertModelOutput(TraderModelOutput modelOutput, TraderMarketSnapshot snapshot) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_model_output
|
|
(run_id, cycle_id, model_output_id, model_bundle_version, calibration_bundle_version,
|
|
direction_json, entry_json, continue_json, exit_json, risk_json,
|
|
metadata_json, uncertainty, ood_score, usable, blocker)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
modelOutput.runId(), modelOutput.cycleId(), modelOutput.modelOutputId(),
|
|
modelOutput.metadata().modelBundleVersion(), modelOutput.metadata().calibrationBundleVersion(),
|
|
jsonCodec.toJson(modelOutput.direction()), jsonCodec.toJson(modelOutput.entry()),
|
|
jsonCodec.toJson(modelOutput.continuation()), jsonCodec.toJson(modelOutput.exit()),
|
|
jsonCodec.toJson(modelOutput.risk()), jsonCodec.toJson(modelOutput.metadata()),
|
|
modelOutput.metadata().uncertainty(), modelOutput.metadata().oodScore(),
|
|
snapshot.dataReady(), snapshot.dataReady() ? null : "DATA_NOT_READY");
|
|
}
|
|
|
|
void insertPmDecision(TraderPositionManagerDecision decision) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_position_manager_decision
|
|
(run_id, cycle_id, pm_decision_id, model_output_id, position_state_id, account_state_id,
|
|
execution_state_id, candidate_action, side, price_plan_id, price_plan_config_hash,
|
|
target_position_ratio, add_ratio, reduce_ratio, stop_price, target_price, reason, decision_json)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
decision.runId(), decision.cycleId(), decision.pmDecisionId(), decision.modelOutputId(),
|
|
decision.positionStateId(), decision.accountStateId(), decision.executionStateId(),
|
|
decision.candidateAction().name(), decision.side().name(), decision.pricePlanId(),
|
|
decision.pricePlanConfigHash(), decision.targetPositionRatio(), decision.addRatio(),
|
|
decision.reduceRatio(), decision.stopPrice(), decision.targetPrice(), decision.reason(),
|
|
jsonCodec.toJson(decision.decisionJson()));
|
|
}
|
|
|
|
void insertRiskDecision(TraderRiskDecision decision) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_risk_decision
|
|
(run_id, cycle_id, risk_decision_id, pm_decision_id, original_action, final_action,
|
|
allow_action, blocker, decision_json)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
decision.runId(), decision.cycleId(), decision.riskDecisionId(), decision.pmDecisionId(),
|
|
decision.originalAction().name(), decision.finalAction().name(), decision.allowAction(),
|
|
decision.blocker(), jsonCodec.toJson(decision.decisionJson()));
|
|
}
|
|
|
|
void insertAction(TraderAction action) {
|
|
jdbcTemplate.update("""
|
|
insert into trader_action
|
|
(run_id, cycle_id, action_id, model_output_id, pm_decision_id, risk_decision_id,
|
|
action_type, symbol, side, price_plan_id, price_plan_config_hash, position_ratio,
|
|
quantity, stop_price, target_price, reduce_only, idempotency_key, send_status,
|
|
reason, action_context_json)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
action.runId(), action.cycleId(), action.actionId(), action.modelOutputId(),
|
|
action.pmDecisionId(), action.riskDecisionId(), action.actionType().name(),
|
|
action.symbol(), action.side().name(), action.pricePlanId(), action.pricePlanConfigHash(),
|
|
action.positionRatio(), action.quantity(), action.stopPrice(), action.targetPrice(),
|
|
action.reduceOnly(), action.idempotencyKey(), "PENDING", action.reason(),
|
|
jsonCodec.toJson(action.actionContextJson()));
|
|
}
|
|
}
|