test(worker): 미국장 백테스트 스모크를 추가한다
US market daily bars가 backtest source와 engine fixture에서 USD 기준으로 동작하는지 잠그고, 완료된 agent-task 리뷰 산출물을 archive 상태로 정리한다.
This commit is contained in:
parent
9eef0b5460
commit
8335800de5
6 changed files with 432 additions and 112 deletions
|
|
@ -0,0 +1,175 @@
|
|||
<!-- task=m-us-market-expansion/04+02,03_us_backtest_smoke plan=0 tag=US_BACKTEST -->
|
||||
|
||||
# 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 이동.
|
||||
|
|
@ -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
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
<!-- task=m-us-market-expansion/04+02,03_us_backtest_smoke plan=0 tag=US_BACKTEST -->
|
||||
|
||||
# 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?**
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
Loading…
Reference in a new issue