diff --git a/agent-task/archive/2026/06/m-us-market-expansion/04+02,03_us_backtest_smoke/code_review_local_G06_1.log b/agent-task/archive/2026/06/m-us-market-expansion/04+02,03_us_backtest_smoke/code_review_local_G06_1.log new file mode 100644 index 0000000..61961ff --- /dev/null +++ b/agent-task/archive/2026/06/m-us-market-expansion/04+02,03_us_backtest_smoke/code_review_local_G06_1.log @@ -0,0 +1,175 @@ + + +# Code Review Reference - US_BACKTEST + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is mandatory.** +> Fill implementation-owned sections, keep active files in place, and report ready for review. Do not ask the user directly or finalize the loop. + +## 개요 + +date=2026-06-06 +task=m-us-market-expansion/04+02,03_us_backtest_smoke, plan=0, tag=US_BACKTEST + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-backtest`: 미국장 daily data로 backtest fixture 또는 smoke 동작 +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +각 항목의 구현과 검증 출력을 대조한다. PASS이면 completion metadata만 보고하고 roadmap update는 런타임에 맡긴다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [US_BACKTEST-1] Storage Bar Source US Selector | ✅ PASS | +| [US_BACKTEST-2] Engine USD Fixture | ✅ PASS | +| [US_BACKTEST-3] Operator Or Worker Smoke Closure | ✅ PASS (worker-only) | + +## 구현 체크리스트 + +- [x] [US_BACKTEST-1] `StorageBarSource`가 US market selector로 USD/NASDAQ bars만 반환하는 test를 추가했다. 검증: `go test -count=1 ./services/worker/internal/backtest` 통과. +- [x] [US_BACKTEST-2] Engine fixture가 `MarketUS` daily bars로 USD starting/ending equity를 계산하는 smoke를 추가했다. 검증: `go test -count=1 ./services/worker/internal/backtest` 통과. +- [x] [US_BACKTEST-3] CLI/operator tests는 live KIS credentials 없이 US bars를 seed할 수 없으므로, worker deterministic smoke로 closure를 완료하고 scope rationale를 아래에 기록함. +- [x] CODE_REVIEW-local-G06.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채웠다. + +## 코드리뷰 전용 체크리스트 + +- [x] 판정을 append한다. +- [x] 판정과 차원별 평가, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active review/plan 파일을 `*.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [x] PASS이면 완료 이벤트 메타데이터를 보고한다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-us-market-expansion/`를 제거했다. +- [ ] WARN/FAIL이면 후속 plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_계획 대비 변경 사항 없음. deterministic inline fixtures만 사용._ + +## 주요 설계 결정 + +1. **US selector test는 기존 KR test와 같은 in-memory store를 재사용한다.** `bar_source_test.go`에 새 테스트 `TestStorageBarSourceReturnsOnlyUSBarsForUSMarket`을 추가했다. test store에 KRX와 NASDAQ instrument를 함께 seed하고 `GetBars(market.MarketUS, ...)` 호출 시 NASDAQ:AAPL bar만 반환하는지 검증한다. + +2. **Engine smoke test는 `TestEngineRunWithUSMarketReturnsEquityInUSD`로 추가했다.** `inMemoryBarSource` + `inMemoryStrategyPort` + `deterministicStrategy` 패턴을 재사용해 NASDAQ:AAPL USD bars (100, 110, 120)로 `MarketUS` run을 실행한다. starting cash는 USD 10,000, trade/day1: buy 2@100, day2: sell 1@110, day3: no trade, final equity USD 10,030을 검증한다. 총환산율 0.003도 확인한다. + +3. **US_BACKTEST-3 operator fixture는 deferred로 처리한다.** `runner_backtest_test.go`는 `fakeAPI`를 사용해 백엔드 로직을 모킹하지만, `import_daily_bars` 시나리오는 실제 KIS provider에 바른다. US bars를 seed하려면 live KIS credentials 또는 별도 US import mock이 필요한데, 이는 이 plan의 범위를 벗어나므로 worker deterministic smoke를 completion evidence로 사용한다. + +## 사용자 리뷰 요청 + +- 상태: 없음 (blocking 없음) +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 아래 검증 결과 참조 +- 자동 후속 불가 이유: 없음 +- 재개 조건: N/A (모든 테스트 PASS) + +## 리뷰어를 위한 체크포인트 + +- US source test가 KR bars를 섞지 않는지 확인한다. +- Engine result가 USD starting cash와 USD price를 보존하는지 확인한다. +- Operator fixture 생략 시 live provider 의존성 때문에 worker deterministic fixture가 충분하다는 근거가 review에 기록됐는지 확인한다. + +## 검증 결과 + +### US_BACKTEST-1 중간 검증 +```text +$ go test -count=1 ./services/worker/internal/backtest -run TestStorageBarSource +=== RUN TestStorageBarSourceReadsMarketBars +--- PASS: TestStorageBarSourceReadsMarketBars (0.00s) +=== RUN TestStorageBarSourceReturnsOnlyUSBarsForUSMarket +--- PASS: TestStorageBarSourceReturnsOnlyUSBarsForUSMarket (0.00s) +PASS +ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.002s +``` + +### US_BACKTEST-2 중간 검증 +```text +$ go test -count=1 ./services/worker/internal/backtest -run TestEngineRunWithUSMarketReturnsEquityInUSD +=== RUN TestEngineRunWithUSMarketReturnsEquityInUSD +--- PASS: TestEngineRunWithUSMarketReturnsEquityInUSD (0.00s) +PASS +ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.002s +``` + +### US_BACKTEST-3 중간 검증 +```text +$ go test -count=1 ./apps/cli/internal/operator +PASS +ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.168s +(US operator fixture deferred — worker deterministic smoke is completion evidence) +``` + +### 최종 검증 +```text +$ go test -count=1 ./services/worker/internal/backtest ./apps/cli/internal/operator +=== RUN TestStorageBarSourceReadsMarketBars +--- PASS: TestStorageBarSourceReadsMarketBars (0.00s) +=== RUN TestStorageBarSourceReturnsOnlyUSBarsForUSMarket +--- PASS: TestStorageBarSourceReturnsOnlyUSBarsForUSMarket (0.00s) +=== RUN TestEngineCallsStrategyForBars +--- PASS: TestEngineCallsStrategyForBars (0.00s) +=== RUN TestEngineProducesDeterministicResultFromFixtureBars +--- PASS: TestEngineProducesDeterministicResultFromFixtureBars (0.00s) +=== RUN TestEngineRunWithUSMarketReturnsEquityInUSD +--- PASS: TestEngineRunWithUSMarketReturnsEquityInUSD (0.00s) +=== RUN TestEngineStoresAndQueriesFixtureResult +--- PASS: TestEngineStoresAndQueriesFixtureResult (0.00s) +PASS +ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.002s +--- +=== RUN TestRunBacktestRequestOutputsRunID +--- PASS: TestRunBacktestRequestOutputsRunID (0.00s) +=== RUN TestRunBacktestImportThenStartFlow +--- PASS: TestRunBacktestImportThenStartFlow (0.00s) +=== RUN TestRunBacktestPollingStopsOnSucceeded +--- PASS: TestRunBacktestPollingStopsOnSucceeded (0.10s) +... +PASS +ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.168s + +$ go test -count=1 ./services/worker/... +ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker 0.006s +ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-migrate 0.004s +ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.003s +ok git.toki-labs.com/toki/alt/services/worker/internal/config 0.004s +ok git.toki-labs.com/toki/alt/services/worker/internal/contracts 0.006s +ok git.toki-labs.com/toki/alt/services/worker/internal/jobs 0.004s +ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/datacheck 0.004s +ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer 0.004s +ok git.toki-labs.com/toki/alt/services/worker/internal/papertrading 0.004s +ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.018s +ok git.toki-labs.com/toki/alt/services/worker/internal/rediskeys 0.002s +ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.062s +ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.081s + +$ git diff --check +(no lines with whitespace errors) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?** + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - Correctness: Pass — `StorageBarSource` US selector와 `Engine` MarketUS/USD path가 계획한 deterministic fixture 범위에서 동작한다. + - Completeness: Pass — `US_BACKTEST-1`, `US_BACKTEST-2`, `US_BACKTEST-3` 모두 구현 또는 worker-only closure 근거가 기록됐다. + - Test coverage: Pass — 새 worker tests가 US selector, USD starting cash, USD equity/trade/position/summary/equity curve를 검증한다. + - API contract: Pass — protobuf/schema/API contract 변경 없음; CLI/operator fixture 생략은 계획의 worker-only closure 조건 안에 있다. + - Code quality: Pass — 변경은 테스트 추가와 리뷰 증거 기록으로 한정되고 debug print/dead code/TODO가 없다. + - Plan deviation: Pass — operator fixture는 live provider-backed scenario 성격 때문에 deferred 사유가 기록됐고, 계획의 허용 경로와 일치한다. + - Verification trust: Pass — 리뷰어가 `go test -count=1 ./services/worker/internal/backtest ./apps/cli/internal/operator`, `go test -count=1 ./services/worker/...`, `bin/test`, `git diff --check`를 재실행해 통과를 확인했다. +- 발견된 문제: 없음 +- 다음 단계: PASS — active plan/review를 `.log`로 아카이브하고 `complete.log` 작성 후 task archive 이동. diff --git a/agent-task/archive/2026/06/m-us-market-expansion/04+02,03_us_backtest_smoke/complete.log b/agent-task/archive/2026/06/m-us-market-expansion/04+02,03_us_backtest_smoke/complete.log new file mode 100644 index 0000000..681e32f --- /dev/null +++ b/agent-task/archive/2026/06/m-us-market-expansion/04+02,03_us_backtest_smoke/complete.log @@ -0,0 +1,43 @@ +# Complete - m-us-market-expansion/04+02,03_us_backtest_smoke + +## 완료 일시 + +2026-06-06T10:35:08Z + +## 요약 + +US backtest smoke fixture task completed in 1 review loop with final verdict PASS. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G06_1.log` | `code_review_local_G06_1.log` | PASS | US selector and USD engine fixture tests were added; worker-only operator closure rationale accepted. | + +## 구현/정리 내용 + +- Added a `StorageBarSource` US-market selector test that seeds KR and NASDAQ instruments and verifies only USD/NASDAQ bars are returned for `MarketUS`. +- Added an Engine `MarketUS` fixture smoke that verifies USD starting cash, ending equity, trades, position, summary, and equity curve. +- Recorded worker-only closure for the operator/headless portion because the live scenario fixture path depends on provider-backed import data. + +## 최종 검증 + +- `go test -count=1 ./services/worker/internal/backtest ./apps/cli/internal/operator` - PASS; both packages returned `ok`. +- `go test -count=1 ./services/worker/...` - PASS; worker packages returned `ok` or `[no test files]`. +- `bin/test` - PASS; Go workspace packages and Flutter tests completed with `All tests passed!`. +- `git diff --check` - PASS; no whitespace errors. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Completed task ids: + - `us-backtest`: PASS; evidence=`plan_local_G06_1.log`, `code_review_local_G06_1.log`; verification=`go test -count=1 ./services/worker/internal/backtest ./apps/cli/internal/operator`, `go test -count=1 ./services/worker/...`, `bin/test`, `git diff --check` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/PLAN-local-G06.md b/agent-task/archive/2026/06/m-us-market-expansion/04+02,03_us_backtest_smoke/plan_local_G06_1.log similarity index 100% rename from agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/PLAN-local-G06.md rename to agent-task/archive/2026/06/m-us-market-expansion/04+02,03_us_backtest_smoke/plan_local_G06_1.log diff --git a/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/CODE_REVIEW-local-G06.md b/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/CODE_REVIEW-local-G06.md deleted file mode 100644 index 8342c1e..0000000 --- a/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/CODE_REVIEW-local-G06.md +++ /dev/null @@ -1,112 +0,0 @@ - - -# Code Review Reference - US_BACKTEST - -> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is mandatory.** -> Fill implementation-owned sections, keep active files in place, and report ready for review. Do not ask the user directly or finalize the loop. - -## 개요 - -date=2026-06-06 -task=m-us-market-expansion/04+02,03_us_backtest_smoke, plan=0, tag=US_BACKTEST - -## Roadmap Targets - -- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` -- Task ids: - - `us-backtest`: 미국장 daily data로 backtest fixture 또는 smoke 동작 -- Completion mode: check-on-pass - -## 이 파일을 읽는 리뷰 에이전트에게 - -> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. - -각 항목의 구현과 검증 출력을 대조한다. PASS이면 completion metadata만 보고하고 roadmap update는 런타임에 맡긴다. - ---- - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| [US_BACKTEST-1] Storage Bar Source US Selector | [ ] | -| [US_BACKTEST-2] Engine USD Fixture | [ ] | -| [US_BACKTEST-3] Operator Or Worker Smoke Closure | [ ] | - -## 구현 체크리스트 - -- [ ] [US_BACKTEST-1] `StorageBarSource`가 US market selector로 USD/NASDAQ bars만 반환하는 test를 추가한다. 검증: `bin/test`와 관련 worker 검증이 통과한다. -- [ ] [US_BACKTEST-2] Engine fixture가 `MarketUS` daily bars로 USD starting/ending equity를 계산하는 smoke를 추가한다. -- [ ] [US_BACKTEST-3] headless/operator backtest smoke가 US market fixture를 실행하거나, worker-only fixture로 충분한 경우 범위 근거를 review에 기록한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. - -## 코드리뷰 전용 체크리스트 - -- [ ] 판정을 append한다. -- [ ] active review/plan 파일을 `*.log`로 아카이브한다. -- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. -- [ ] PASS이면 완료 이벤트 메타데이터를 보고한다. -- [ ] WARN/FAIL이면 후속 plan/review 또는 `USER_REVIEW.md`를 작성한다. - -## 계획 대비 변경 사항 - -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ - -## 주요 설계 결정 - -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ - -## 사용자 리뷰 요청 - -- 상태: 없음 -- 사유 유형: 없음 -- 결정 필요: 없음 -- 차단 근거: 없음 -- 실행한 검증/명령: 없음 -- 자동 후속 불가 이유: 없음 -- 재개 조건: 없음 - -## 리뷰어를 위한 체크포인트 - -- US source test가 KR bars를 섞지 않는지 확인한다. -- Engine result가 USD starting cash와 USD price를 보존하는지 확인한다. -- Operator fixture 생략 시 live provider 의존성 때문에 worker deterministic fixture가 충분하다는 근거가 review에 기록됐는지 확인한다. - -## 검증 결과 - -### US_BACKTEST-1 중간 검증 -```text -$ go test -count=1 ./services/worker/internal/backtest -(output) -``` - -### US_BACKTEST-2 중간 검증 -```text -$ go test -count=1 ./services/worker/internal/backtest -(output) -``` - -### US_BACKTEST-3 중간 검증 -```text -$ go test -count=1 ./apps/cli/internal/operator -(output) -``` - -### 최종 검증 -```text -$ go test -count=1 ./services/worker/internal/backtest ./apps/cli/internal/operator -(output) - -$ go test -count=1 ./services/worker/... -(output) - -$ bin/test -(output) - -$ git diff --check -(output) -``` - ---- - -> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/services/worker/internal/backtest/bar_source_test.go b/services/worker/internal/backtest/bar_source_test.go index 8e7ad0f..984cbfe 100644 --- a/services/worker/internal/backtest/bar_source_test.go +++ b/services/worker/internal/backtest/bar_source_test.go @@ -95,3 +95,65 @@ func TestStorageBarSourceReadsMarketBars(t *testing.T) { } } } + +func TestStorageBarSourceReturnsOnlyUSBarsForUSMarket(t *testing.T) { + store := &sourceStore{} + krx := market.Instrument{ID: "KRX:005930", Market: market.MarketKR, Venue: market.VenueKRX, Currency: market.CurrencyKRW} + nasdaq := market.Instrument{ID: "NASDAQ:AAPL", Market: market.MarketUS, Venue: market.VenueNASDAQ, Currency: market.CurrencyUSD} + if err := store.UpsertInstrument(context.Background(), krx); err != nil { + t.Fatalf("upsert krx instrument: %v", err) + } + if err := store.UpsertInstrument(context.Background(), nasdaq); err != nil { + t.Fatalf("upsert us instrument: %v", err) + } + + day1 := time.Date(2024, 5, 27, 0, 0, 0, 0, time.UTC) + day2 := time.Date(2024, 5, 28, 0, 0, 0, 0, time.UTC) + // Seed KR bars + if err := store.UpsertBar(context.Background(), market.Bar{InstrumentID: krx.ID, Timeframe: market.TimeframeDaily, Timestamp: day2}); err != nil { + t.Fatalf("upsert krx day2: %v", err) + } + if err := store.UpsertBar(context.Background(), market.Bar{InstrumentID: krx.ID, Timeframe: market.TimeframeDaily, Timestamp: day1}); err != nil { + t.Fatalf("upsert krx day1: %v", err) + } + // Seed US bars + if err := store.UpsertBar(context.Background(), market.Bar{ + InstrumentID: nasdaq.ID, Timeframe: market.TimeframeDaily, + Timestamp: day2, Close: market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "155"}}, + }); err != nil { + t.Fatalf("upsert us day2: %v", err) + } + if err := store.UpsertBar(context.Background(), market.Bar{ + InstrumentID: nasdaq.ID, Timeframe: market.TimeframeDaily, + Timestamp: day1, Close: market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "150"}}, + }); err != nil { + t.Fatalf("upsert us day1: %v", err) + } + + source := NewStorageBarSource(store, store) + + // Query US market — should return only NASDAQ:AAPL bars + got, err := source.GetBars(context.Background(), market.MarketUS, market.TimeframeDaily, day1, day2) + if err != nil { + t.Fatalf("get bars: %v", err) + } + if len(got) != 2 { + t.Fatalf("US bars: got %d, want 2", len(got)) + } + // Verify chronological order + if got[0].Timestamp != day1 || got[1].Timestamp != day2 { + t.Fatalf("US bars not sorted by timestamp: %+v", got) + } + // All returned bars must be NASDAQ:AAPL + for _, bar := range got { + if bar.InstrumentID != nasdaq.ID { + t.Fatalf("US query returned unexpected instrument id: %q, want %q", bar.InstrumentID, nasdaq.ID) + } + } + // Verify USD currency is preserved in bar Close price + for _, bar := range got { + if bar.Close.Currency != market.CurrencyUSD { + t.Fatalf("US bar Close currency: got %q, want %q", bar.Close.Currency, market.CurrencyUSD) + } + } +} diff --git a/services/worker/internal/backtest/fixture_test.go b/services/worker/internal/backtest/fixture_test.go index 8ac1c3b..c161a4f 100644 --- a/services/worker/internal/backtest/fixture_test.go +++ b/services/worker/internal/backtest/fixture_test.go @@ -158,6 +158,158 @@ func TestEngineProducesDeterministicResultFromFixtureBars(t *testing.T) { } } +func TestEngineRunWithUSMarketReturnsEquityInUSD(t *testing.T) { + // This smoke test validates the MarketUS path: USD starting cash, + // USD-denominated prices, and a final equity figure expressed in USD. + // The strategy is the same deterministic one: buy 2 on day 1, sell 1 on day 2. + // + // Starting cash = 10,000 USD + // Day 1: Buy 2 units at 100 USD → cash = 9,800, pos = 2 + // Day 2: Sell 1 unit at 110 USD → cash = 9,910, pos = 1 + // Day 3: No trades; pos marked at 120 USD + // Final equity = cash 9,910 + 1*120 = 10,030 USD + // + // Trades: buy 2@100, sell 1@110 + // Positions: 1 unit NASDAQ:AAPL at last price 120 USD + // Total return = (10030 - 10000) / 10000 = 0.003 + + instID := market.InstrumentID("NASDAQ:AAPL") + bar1 := market.Bar{ + InstrumentID: instID, + Timeframe: market.TimeframeDaily, + Timestamp: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC), + Close: market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "100"}}, + } + bar2 := market.Bar{ + InstrumentID: instID, + Timeframe: market.TimeframeDaily, + Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC), + Close: market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "110"}}, + } + bar3 := market.Bar{ + InstrumentID: instID, + Timeframe: market.TimeframeDaily, + Timestamp: time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC), + Close: market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "120"}}, + } + + barSource := &inMemoryBarSource{bars: []market.Bar{bar1, bar2, bar3}} + strat := &deterministicStrategy{id: "det-strat-us"} + strategyPort := &inMemoryStrategyPort{strategy: strat} + resultStore := newInMemoryResultStore() + + engine := NewEngine(barSource, strategyPort, resultStore) + + run := backtest.Run{ + ID: "run-us-smoke", + Spec: backtest.RunSpec{ + StrategyID: "det-strat-us", + Market: market.MarketUS, + Timeframe: market.TimeframeDaily, + From: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC), + }, + } + + err := engine.Execute(context.Background(), run) + if err != nil { + t.Fatalf("US execution failed: %v", err) + } + + res, err := resultStore.GetResult(context.Background(), run.ID) + if err != nil { + t.Fatalf("failed to get result: %v", err) + } + + // Verify starting cash is USD 10,000 + if res.StartingCash.Currency != market.CurrencyUSD { + t.Errorf("starting cash currency: got %q, want %q", res.StartingCash.Currency, market.CurrencyUSD) + } + if res.StartingCash.Amount.Value != "10000" { + t.Errorf("expected starting cash 10000, got %s", res.StartingCash.Amount.Value) + } + + // Verify final equity + // Day 1: cash 9,800 + 2*100 = 10,000 + // Day 2: cash 9,910 + 1*110 = 10,020 + // Day 3: cash 9,910 + 1*120 = 10,030 + if res.EndingEquity.Currency != market.CurrencyUSD { + t.Errorf("ending equity currency: got %q, want %q", res.EndingEquity.Currency, market.CurrencyUSD) + } + if res.EndingEquity.Amount.Value != "10030" { + t.Errorf("expected ending equity 10030, got %s", res.EndingEquity.Amount.Value) + } + + // Verify Trades + if len(res.Trades) != 2 { + t.Fatalf("expected 2 trades, got %d", len(res.Trades)) + } + trade1 := res.Trades[0] + if trade1.Side != backtest.OrderSideBuy || trade1.Quantity.Amount.Value != "2" || + trade1.Price.Amount.Value != "100" { + t.Errorf("unexpected trade 1: %+v", trade1) + } + trade2 := res.Trades[1] + if trade2.Side != backtest.OrderSideSell || trade2.Quantity.Amount.Value != "1" || + trade2.Price.Amount.Value != "110" { + t.Errorf("unexpected trade 2: %+v", trade2) + } + + // Verify Positions + if len(res.Positions) != 1 { + t.Fatalf("expected 1 position, got %d", len(res.Positions)) + } + pos := res.Positions[0] + if pos.InstrumentID != instID || pos.Quantity.Amount.Value != "1" || + pos.LastPrice.Amount.Value != "120" { + t.Errorf("unexpected position: %+v", pos) + } + + // Verify derived summary metrics + if res.Summary.EndingEquity.Amount.Value != "10030" { + t.Errorf("summary ending equity: got %s, want 10030", res.Summary.EndingEquity.Amount.Value) + } + if res.Summary.TradeCount != 2 { + t.Errorf("summary trade count: got %d, want 2", res.Summary.TradeCount) + } + // (10030 - 10000) / 10000 = 0.003 + if res.Summary.TotalReturn.Value != "0.003" { + t.Errorf("summary total return: got %s, want 0.003", res.Summary.TotalReturn.Value) + } + + // Verify equity curve: one point per bar + if len(res.EquityCurve) != 3 { + t.Fatalf("equity curve length: got %d, want 3", len(res.EquityCurve)) + } + wantCurve := []struct { + ts time.Time + equity string + }{ + {bar1.Timestamp, "10000"}, + {bar2.Timestamp, "10020"}, + {bar3.Timestamp, "10030"}, + } + for i, want := range wantCurve { + point := res.EquityCurve[i] + if !point.Timestamp.Equal(want.ts) { + t.Errorf("equity curve point %d timestamp: got %v, want %v", i, point.Timestamp, want.ts) + } + if point.Equity.Amount.Value != want.equity { + t.Errorf("equity curve point %d equity: got %s, want %s", i, point.Equity.Amount.Value, want.equity) + } + if point.Equity.Currency != market.CurrencyUSD { + t.Errorf("equity curve point %d currency: got %q, want %q", i, point.Equity.Currency, market.CurrencyUSD) + } + } + + // Final equity curve point must match ending equity + finalPoint := res.EquityCurve[len(res.EquityCurve)-1] + if finalPoint.Equity.Amount.Value != res.EndingEquity.Amount.Value { + t.Errorf("final equity point %s does not match ending equity %s", + finalPoint.Equity.Amount.Value, res.EndingEquity.Amount.Value) + } +} + func TestEngineStoresAndQueriesFixtureResult(t *testing.T) { instID := market.InstrumentID("KRX:005930") bar1 := market.Bar{