Add crypto lake replay labels

This commit is contained in:
Codex
2026-06-23 22:21:56 +08:00
parent 7ff786f658
commit 2fe4077164
28 changed files with 977 additions and 64 deletions
@@ -5,6 +5,7 @@ import com.quantai.trader.domain.PlaybookCandidate;
import com.quantai.trader.domain.TraderAction;
import com.quantai.trader.domain.TraderDecisionCycle;
import com.quantai.trader.domain.TraderEntryPlan;
import com.quantai.trader.domain.TraderMarketSnapshot;
import com.quantai.trader.domain.TraderPositionPath;
import com.quantai.trader.domain.TraderPricePlan;
import com.quantai.trader.domain.TriggerDecision;
@@ -15,6 +16,7 @@ import com.quantai.trader.enums.TraderState;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public final class TestFixtures {
@@ -72,6 +74,35 @@ public final class TestFixtures {
);
}
public static TraderMarketSnapshot labeledSnapshot() {
return new TraderMarketSnapshot(
"trader_run_test",
"trader_cycle_test",
"trader_snapshot_test",
"BTCUSDT",
NOW,
"trader_feature_v0",
Map.of("contextPass", true),
Map.of("setupPass", true, "side", "LONG"),
Map.of("triggerScore", "0.95"),
Map.of("lastPrice", "65010"),
Map.of("missing_features", List.of()),
Map.ofEntries(
Map.entry("labelSource", "CRYPTO_LAKE_1M_REPLAY"),
Map.entry("labelStatus", "REPLAY_MARKOUT_LABELED"),
Map.entry("side", "LONG"),
Map.entry("entryPrice", "65000"),
Map.entry("markoutBps1m", "5"),
Map.entry("markoutBps5m", "12"),
Map.entry("markoutBps15m", "24"),
Map.entry("mfeBps15m", "30"),
Map.entry("maeBps15m", "6"),
Map.entry("targetBeforeStop15m", true),
Map.entry("expectedSlippageBps", "1")
)
);
}
public static TriggerDecision strongTrigger() {
return new TriggerDecision(true, new BigDecimal("0.95"), "TRIGGER_ACCEPTED", null, Map.of());
}
@@ -12,6 +12,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -24,13 +25,15 @@ class TraderDecisionCycleRunnerTest {
@Test
void exportsSampleForHappyPathReplayTick() {
String runId = "trader_run_runner_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
ReplayClockTick tick = new ReplayClockTick(
"trader_run_runner",
runId,
"BTCUSDT",
Instant.parse("2026-06-23T12:00:00Z"),
Map.of("contextPass", true),
Map.of(
"setupPass", true,
"side", "LONG",
"entryPrice", new BigDecimal("65000"),
"invalidPrice", new BigDecimal("64800"),
"stopPrice", new BigDecimal("64920"),
@@ -39,13 +42,14 @@ class TraderDecisionCycleRunnerTest {
),
Map.of("triggerScore", "0.95"),
Map.of("lastPrice", "65010"),
Map.of(),
Map.of()
);
TraderCycleResult result = runner.runReplayTick(
tick,
new TraderRuntimeState(
"trader_run_runner",
runId,
TraderRunMode.REPLAY,
"BREAKOUT_RETEST_CONTINUATION",
"2026-06-22.p0"
@@ -64,9 +68,10 @@ class TraderDecisionCycleRunnerTest {
"BTCUSDT",
Instant.parse("2026-06-23T12:00:00Z"),
Map.of("contextPass", true),
Map.of("setupPass", true),
Map.of("setupPass", true, "side", "LONG"),
Map.of("triggerScore", "0.95"),
Map.of("lastPrice", "65010"),
Map.of(),
Map.of()
);
@@ -88,7 +88,7 @@ class TraderControllerTest {
mockMvc.perform(get("/api/trader/replay/runs/{runId}/report", runId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.candidateEvents").value(1))
.andExpect(jsonPath("$.strictVsLoose.p0ReplayEngine").value("jsonl_fixture"));
.andExpect(jsonPath("$.strictVsLoose.replayEngine").value("jsonl_fixture"));
}
@Test
@@ -47,7 +47,7 @@ class MybatisReplayPersistenceTest {
assertThat(run.status()).isEqualTo(ReplayRunStatus.COMPLETED);
assertThat(reportRepository.findByRunId(run.runId())).isPresent()
.get()
.extracting(report -> report.strictVsLoose().get("p0ReplayEngine"))
.extracting(report -> report.strictVsLoose().get("replayEngine"))
.isEqualTo("jsonl_fixture");
var samples = sampleRepository.findByRunId(run.runId());
@@ -56,6 +56,7 @@ class TraderPositionManagerTest {
Map.of(),
Map.of(),
Map.of("lastPrice", "65010"),
Map.of(),
Map.of()
);
}
@@ -0,0 +1,76 @@
package com.quantai.trader.replay;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class CryptoLakeReplayCsvMarketEventReaderTest {
private final CryptoLakeReplayCsvMarketEventReader reader = new CryptoLakeReplayCsvMarketEventReader();
@Test
void readsCandidateTicksAndRecomputesReplayMarkoutLabels() throws Exception {
ReplayRunConfig config = config();
var ticks = reader.readTicks(config);
assertThat(ticks).hasSize(2);
assertThat(ticks.getFirst().setupFeatures())
.containsEntry("setupPass", true)
.containsEntry("side", "LONG")
.containsEntry("entryPrice", "100");
assertThat(ticks.getFirst().labelInputs())
.containsEntry("labelStatus", "REPLAY_MARKOUT_LABELED")
.containsEntry("side", "LONG")
.containsEntry("markoutBps15m", "200");
assertThat(ticks.get(1).setupFeatures())
.containsEntry("side", "SHORT");
assertThat(ticks.get(1).labelInputs())
.containsEntry("labelStatus", "REPLAY_MARKOUT_LABELED")
.containsEntry("side", "SHORT");
}
private ReplayRunConfig config() throws Exception {
Path replay = Path.of(new ClassPathResource("replay-fixtures/crypto-lake-replay-mini.csv").getFile().toURI());
Path candidates = Path.of(new ClassPathResource("replay-fixtures/crypto-lake-candidate-events-mini.csv").getFile().toURI());
return new ReplayRunConfig(
"trader_run_csv_test",
"BTCUSDT",
"BREAKOUT_RETEST_CONTINUATION",
"2026-06-22.p0",
Instant.parse("2026-01-01T00:00:00Z"),
Instant.parse("2026-01-01T00:05:00Z"),
"trader_feature_v0",
"trader_label_v0",
Map.of(
"cryptoLakeReplay1m", new DataSourceSpec(
"crypto-lake-mini",
replay.toString(),
"fixture-hash-not-used",
null,
18L,
null,
null,
"UTC",
Map.of()
),
"candidateEvents", new DataSourceSpec(
"candidate-events-mini",
candidates.toString(),
"fixture-hash-not-used",
null,
2L,
null,
null,
"UTC",
Map.of()
)
)
);
}
}
@@ -44,15 +44,18 @@ class TraderReplayFixtureAcceptanceTest {
assertThat(run.status()).as(fixture.fileName()).isEqualTo(ReplayRunStatus.COMPLETED);
TraderReplayReport report = reportRepository.findByRunId(run.runId()).orElseThrow();
assertThat(report.strictVsLoose())
.containsEntry("p0ReplayEngine", "jsonl_fixture")
.containsEntry("replayEngine", "jsonl_fixture")
.containsEntry("tickCount", 1)
.containsEntry("sampleCount", 1)
.containsEntry("actionCount", fixture.expectedActionCount());
.containsEntry("actionCount", fixture.expectedActionCount())
.containsEntry("labeledSampleCount", 0)
.containsEntry("proxyOnlySampleCount", 1);
assertThat(report.candidateEvents()).isEqualTo(fixture.expectedActionCount());
var samples = sampleRepository.findByRunId(run.runId());
assertThat(samples).hasSize(1);
assertThat(samples.getFirst().features()).containsEntry("actionType", fixture.expectedSampleActionType());
assertThat(samples.getFirst().labels()).containsEntry("label_status", "PROXY_ONLY_NO_REPLAY_LABEL");
}
}
@@ -13,20 +13,23 @@ import static org.assertj.core.api.Assertions.assertThat;
class TrainingSampleExporterTest {
@Test
void exportsProxyOnlySampleWithFeatureAndLabelVersions() {
void exportsReplayMarkoutSampleWithFeatureAndLabelVersions() {
CapturingSampleRepository repository = new CapturingSampleRepository();
TrainingSampleExporter exporter = new TrainingSampleExporter(TestFixtures.properties(), repository);
TrainingSampleExporter exporter = new TrainingSampleExporter(TestFixtures.properties(), repository, new TriggerMarkoutLabeler());
var sample = exporter.export(
TestFixtures.cycle(com.quantai.trader.enums.TraderState.SAMPLE_EXPORTED),
TestFixtures.labeledSnapshot(),
TestFixtures.candidate(),
TestFixtures.action(),
TestFixtures.openedPath(false)
);
assertThat(sample.proxyOnly()).isTrue();
assertThat(sample.proxyOnly()).isFalse();
assertThat(sample.featureVersion()).isEqualTo("trader_feature_v0");
assertThat(sample.labelVersion()).isEqualTo("trader_label_v0");
assertThat(sample.netReturnBps1x()).isEqualByComparingTo("14.00000000");
assertThat(sample.labels()).containsEntry("label_status", "REPLAY_MARKOUT_LABELED");
assertThat(repository.findByRunId("trader_run_test")).hasSize(1);
}
@@ -0,0 +1,3 @@
event_id,bar_time,signal_type,direction,source_service,symbol,old_fusion_score
mini-long-1,1767225600000,BREAKOUT_RETEST_CONTINUATION,LONG,TEST_CANDIDATE_EVENT,BTCUSDT,0.95
mini-short-1,1767225720000,BREAKOUT_RETEST_CONTINUATION,SHORT,TEST_CANDIDATE_EVENT,BTCUSDT,0.90
1 event_id bar_time signal_type direction source_service symbol old_fusion_score
2 mini-long-1 1767225600000 BREAKOUT_RETEST_CONTINUATION LONG TEST_CANDIDATE_EVENT BTCUSDT 0.95
3 mini-short-1 1767225720000 BREAKOUT_RETEST_CONTINUATION SHORT TEST_CANDIDATE_EVENT BTCUSDT 0.90
@@ -0,0 +1,19 @@
symbol,timeframe,open_time,open,high,low,close,volume,taker_buy_volume,funding_bps,open_interest,best_bid_price,best_ask_price,observed_spread_bps,observed_slippage_bps,expected_slippage_bps,p95_latency_ms,source_coverage
BTCUSDT,1m,2026-01-01T00:00:00Z,100.0,100.4,99.8,100.0,10,5,0.1,1000,99.99,100.01,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:01:00Z,100.0,100.7,99.9,100.4,11,6,0.1,1001,100.39,100.41,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:02:00Z,100.4,100.6,100.0,100.2,12,7,0.1,1002,100.19,100.21,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:03:00Z,100.2,100.8,100.1,100.5,13,8,0.1,1003,100.49,100.51,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:04:00Z,100.5,101.0,100.2,100.8,14,9,0.1,1004,100.79,100.81,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:05:00Z,100.8,101.3,100.5,101.0,15,10,0.1,1005,100.99,101.01,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:06:00Z,101.0,101.6,100.7,101.2,16,11,0.1,1006,101.19,101.21,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:07:00Z,101.2,101.7,100.8,101.3,17,12,0.1,1007,101.29,101.31,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:08:00Z,101.3,101.8,100.9,101.4,18,13,0.1,1008,101.39,101.41,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:09:00Z,101.4,101.9,101.0,101.5,19,14,0.1,1009,101.49,101.51,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:10:00Z,101.5,102.0,101.1,101.6,20,15,0.1,1010,101.59,101.61,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:11:00Z,101.6,102.1,101.2,101.7,21,16,0.1,1011,101.69,101.71,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:12:00Z,101.7,102.2,101.3,101.8,22,17,0.1,1012,101.79,101.81,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:13:00Z,101.8,102.3,101.4,101.9,23,18,0.1,1013,101.89,101.91,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:14:00Z,101.9,102.4,101.5,102.0,24,19,0.1,1014,101.99,102.01,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:15:00Z,102.0,102.5,101.6,102.0,25,20,0.1,1015,101.99,102.01,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:16:00Z,102.0,102.1,99.5,99.8,26,6,0.1,1016,99.79,99.81,2,1,1,30,1
BTCUSDT,1m,2026-01-01T00:17:00Z,99.8,100.0,98.8,99.0,27,5,0.1,1017,98.99,99.01,2,1,1,30,1
1 symbol timeframe open_time open high low close volume taker_buy_volume funding_bps open_interest best_bid_price best_ask_price observed_spread_bps observed_slippage_bps expected_slippage_bps p95_latency_ms source_coverage
2 BTCUSDT 1m 2026-01-01T00:00:00Z 100.0 100.4 99.8 100.0 10 5 0.1 1000 99.99 100.01 2 1 1 30 1
3 BTCUSDT 1m 2026-01-01T00:01:00Z 100.0 100.7 99.9 100.4 11 6 0.1 1001 100.39 100.41 2 1 1 30 1
4 BTCUSDT 1m 2026-01-01T00:02:00Z 100.4 100.6 100.0 100.2 12 7 0.1 1002 100.19 100.21 2 1 1 30 1
5 BTCUSDT 1m 2026-01-01T00:03:00Z 100.2 100.8 100.1 100.5 13 8 0.1 1003 100.49 100.51 2 1 1 30 1
6 BTCUSDT 1m 2026-01-01T00:04:00Z 100.5 101.0 100.2 100.8 14 9 0.1 1004 100.79 100.81 2 1 1 30 1
7 BTCUSDT 1m 2026-01-01T00:05:00Z 100.8 101.3 100.5 101.0 15 10 0.1 1005 100.99 101.01 2 1 1 30 1
8 BTCUSDT 1m 2026-01-01T00:06:00Z 101.0 101.6 100.7 101.2 16 11 0.1 1006 101.19 101.21 2 1 1 30 1
9 BTCUSDT 1m 2026-01-01T00:07:00Z 101.2 101.7 100.8 101.3 17 12 0.1 1007 101.29 101.31 2 1 1 30 1
10 BTCUSDT 1m 2026-01-01T00:08:00Z 101.3 101.8 100.9 101.4 18 13 0.1 1008 101.39 101.41 2 1 1 30 1
11 BTCUSDT 1m 2026-01-01T00:09:00Z 101.4 101.9 101.0 101.5 19 14 0.1 1009 101.49 101.51 2 1 1 30 1
12 BTCUSDT 1m 2026-01-01T00:10:00Z 101.5 102.0 101.1 101.6 20 15 0.1 1010 101.59 101.61 2 1 1 30 1
13 BTCUSDT 1m 2026-01-01T00:11:00Z 101.6 102.1 101.2 101.7 21 16 0.1 1011 101.69 101.71 2 1 1 30 1
14 BTCUSDT 1m 2026-01-01T00:12:00Z 101.7 102.2 101.3 101.8 22 17 0.1 1012 101.79 101.81 2 1 1 30 1
15 BTCUSDT 1m 2026-01-01T00:13:00Z 101.8 102.3 101.4 101.9 23 18 0.1 1013 101.89 101.91 2 1 1 30 1
16 BTCUSDT 1m 2026-01-01T00:14:00Z 101.9 102.4 101.5 102.0 24 19 0.1 1014 101.99 102.01 2 1 1 30 1
17 BTCUSDT 1m 2026-01-01T00:15:00Z 102.0 102.5 101.6 102.0 25 20 0.1 1015 101.99 102.01 2 1 1 30 1
18 BTCUSDT 1m 2026-01-01T00:16:00Z 102.0 102.1 99.5 99.8 26 6 0.1 1016 99.79 99.81 2 1 1 30 1
19 BTCUSDT 1m 2026-01-01T00:17:00Z 99.8 100.0 98.8 99.0 27 5 0.1 1017 98.99 99.01 2 1 1 30 1
@@ -1 +1 @@
{"eventTime":"2026-01-01T03:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"weak_breakout_retest","timeframeAlignment":"1h_up_4h_flat"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","entryPrice":"65120","invalidPrice":"64980","stopPrice":"65040","targetPrice":"65450","executionQualityScore":"0.76","volumeImpulse":"0.88","retestHold":true},"triggerFeatures":{"triggerScore":"0.32","triggerName":"false_breakout_probe","breakoutFollowThrough":false},"executionFeatures":{"lastPrice":"65105","spreadBps":"1.4","bookImbalance":"0.49"},"dataQuality":{"missing_features":[]}}
{"eventTime":"2026-01-01T03:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"weak_breakout_retest","timeframeAlignment":"1h_up_4h_flat"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","side":"LONG","entryPrice":"65120","invalidPrice":"64980","stopPrice":"65040","targetPrice":"65450","executionQualityScore":"0.76","volumeImpulse":"0.88","retestHold":true},"triggerFeatures":{"triggerScore":"0.32","triggerName":"false_breakout_probe","breakoutFollowThrough":false},"executionFeatures":{"lastPrice":"65105","spreadBps":"1.4","bookImbalance":"0.49"},"dataQuality":{"missing_features":[]}}
@@ -1 +1 @@
{"eventTime":"2026-01-01T05:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","executionQualityScore":"0.90"},"triggerFeatures":{"triggerScore":"0.95","triggerName":"micro_continuation"},"executionFeatures":{"lastPrice":"65300"},"dataQuality":{"missing_features":[]}}
{"eventTime":"2026-01-01T05:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","side":"LONG","executionQualityScore":"0.90"},"triggerFeatures":{"triggerScore":"0.95","triggerName":"micro_continuation"},"executionFeatures":{"lastPrice":"65300"},"dataQuality":{"missing_features":[]}}
@@ -1 +1 @@
{"eventTime":"2026-01-01T04:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","entryPrice":"65200","invalidPrice":"65020","stopPrice":"65100","targetPrice":"65580","executionQualityScore":"0.84"},"triggerFeatures":{"triggerScore":"0.91","triggerName":"micro_continuation"},"executionFeatures":{"lastPrice":"65210"},"dataQuality":{"missing_features":["level_1.best_bid","level_1.best_ask"]}}
{"eventTime":"2026-01-01T04:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","side":"LONG","entryPrice":"65200","invalidPrice":"65020","stopPrice":"65100","targetPrice":"65580","executionQualityScore":"0.84"},"triggerFeatures":{"triggerScore":"0.91","triggerName":"micro_continuation"},"executionFeatures":{"lastPrice":"65210"},"dataQuality":{"missing_features":["level_1.best_bid","level_1.best_ask"]}}
@@ -1 +1 @@
{"eventTime":"2026-01-01T00:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest","timeframeAlignment":"1h_4h_up"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","entryPrice":"65000","invalidPrice":"64800","stopPrice":"64920","targetPrice":"65350","executionQualityScore":"0.92","volumeImpulse":"1.48","retestHold":true},"triggerFeatures":{"triggerScore":"0.95","triggerName":"micro_continuation","breakoutFollowThrough":true},"executionFeatures":{"lastPrice":"65010","spreadBps":"0.8","bookImbalance":"0.57"},"dataQuality":{"missing_features":[]}}
{"eventTime":"2026-01-01T00:00:00Z","contextFeatures":{"contextPass":true,"trendRegime":"UP","marketStructure":"breakout_retest","timeframeAlignment":"1h_4h_up"},"setupFeatures":{"setupPass":true,"setupName":"breakout_retest_continuation","side":"LONG","entryPrice":"65000","invalidPrice":"64800","stopPrice":"64920","targetPrice":"65350","executionQualityScore":"0.92","volumeImpulse":"1.48","retestHold":true},"triggerFeatures":{"triggerScore":"0.95","triggerName":"micro_continuation","breakoutFollowThrough":true},"executionFeatures":{"lastPrice":"65010","spreadBps":"0.8","bookImbalance":"0.57"},"dataQuality":{"missing_features":[]}}