diff --git a/BacktestingKit/Engine/BKPresets.swift b/BacktestingKit/Engine/BKPresets.swift index 4c5660e..9cd3c34 100644 --- a/BacktestingKit/Engine/BKPresets.swift +++ b/BacktestingKit/Engine/BKPresets.swift @@ -6,7 +6,7 @@ public func bollingerPreset(trailing: Bool = true, stopLoss: Double = 15) -> Sim indicatorOneName: "ruleOneEntry", indicatorOneType: .bollingerLower, indicatorOneFigure: [20, 2, 2], - compare: .smallThan, + compare: .largerOrEqualTo, indicatorTwoName: "ruleOneExit", indicatorTwoType: .close, indicatorTwoFigure: [0, 0, 0] @@ -53,7 +53,7 @@ public func customPreset(trailing: Bool = true, stopLoss: Double = 15) -> Simula indicatorTwoFigure: [0, 0, 0] ) return SimulationPolicyConfig( - policy: .sma, + policy: .customStrategy, trailingStopLoss: trailing, stopLossFigure: stopLoss, profitFactor: pow(2.0, 32.0), @@ -266,19 +266,19 @@ public func smaCrossoverPreset(trailing: Bool = true, stopLoss: Double = 15) -> indicatorOneName: "ruleOneEntry", indicatorOneType: .sma, indicatorOneFigure: [50, 0, 0], - compare: .smallThan, + compare: .largerOrEqualTo, indicatorTwoName: "ruleOneExit", indicatorTwoType: .sma, - indicatorTwoFigure: [200, 0, 0] + indicatorTwoFigure: [150, 0, 0] ) let ruleTwo = SimulationRule( indicatorOneName: "ruleTwoEntry", indicatorOneType: .sma, indicatorOneFigure: [50, 0, 0], - compare: .largerOrEqualTo, + compare: .smallerOrEqualTo, indicatorTwoName: "ruleTwoExit", indicatorTwoType: .sma, - indicatorTwoFigure: [200, 0, 0] + indicatorTwoFigure: [150, 0, 0] ) return SimulationPolicyConfig( policy: .smaCrossover, @@ -298,19 +298,19 @@ public func emaCrossoverPreset(trailing: Bool = true, stopLoss: Double = 15) -> indicatorOneName: "ruleOneEntry", indicatorOneType: .ema, indicatorOneFigure: [50, 0, 0], - compare: .smallThan, + compare: .largerOrEqualTo, indicatorTwoName: "ruleOneExit", indicatorTwoType: .ema, - indicatorTwoFigure: [200, 0, 0] + indicatorTwoFigure: [150, 0, 0] ) let ruleTwo = SimulationRule( indicatorOneName: "ruleTwoEntry", indicatorOneType: .ema, indicatorOneFigure: [50, 0, 0], - compare: .largerOrEqualTo, + compare: .smallerOrEqualTo, indicatorTwoName: "ruleTwoExit", indicatorTwoType: .ema, - indicatorTwoFigure: [200, 0, 0] + indicatorTwoFigure: [150, 0, 0] ) return SimulationPolicyConfig( policy: .emaCrossover, @@ -396,7 +396,7 @@ public func stochasticFastPreset(trailing: Bool = true, stopLoss: Double = 15) - indicatorOneFigure: [14, 3, 0], compare: .largerOrEqualTo, indicatorTwoName: "ruleOneExit", - indicatorTwoType: .stochasticFastPercentK, + indicatorTwoType: .stochasticFastPercentD, indicatorTwoFigure: [14, 3, 0] ) let ruleTwo = SimulationRule( @@ -405,7 +405,7 @@ public func stochasticFastPreset(trailing: Bool = true, stopLoss: Double = 15) - indicatorOneFigure: [14, 3, 0], compare: .smallThan, indicatorTwoName: "ruleTwoExit", - indicatorTwoType: .stochasticFastPercentK, + indicatorTwoType: .stochasticFastPercentD, indicatorTwoFigure: [14, 3, 0] ) return SimulationPolicyConfig( diff --git a/BacktestingKit/Simulation/V3/BKSimulationDriver.swift b/BacktestingKit/Simulation/V3/BKSimulationDriver.swift index 25bf1ca..29c6856 100644 --- a/BacktestingKit/Simulation/V3/BKSimulationDriver.swift +++ b/BacktestingKit/Simulation/V3/BKSimulationDriver.swift @@ -716,22 +716,21 @@ public final class BKSimulationDriver: BKV3SimulationDriving { var entryRules: [BKV3_SimulationRule] = [] var exitRules: [BKV3_SimulationRule] = [] - if policy == .customStrategy { - switch mapDataStoreResult(await dataStore.getSimulationRules(configID: config.id, ruleType: "entry"), instrumentID: ticker) { - case .success(let rules): - entryRules = rules - case .failure(let error): - return .failure(error) - } - if entryRules.isEmpty { continue } - switch mapDataStoreResult(await dataStore.getSimulationRules(configID: config.id, ruleType: "exit"), instrumentID: ticker) { - case .success(let rules): - exitRules = rules - case .failure(let error): - return .failure(error) - } - if exitRules.isEmpty { continue } - } else { + switch mapDataStoreResult(await dataStore.getSimulationRules(configID: config.id, ruleType: "entry"), instrumentID: ticker) { + case .success(let rules): + entryRules = rules + case .failure(let error): + return .failure(error) + } + switch mapDataStoreResult(await dataStore.getSimulationRules(configID: config.id, ruleType: "exit"), instrumentID: ticker) { + case .success(let rules): + exitRules = rules + case .failure(let error): + return .failure(error) + } + + if entryRules.isEmpty || exitRules.isEmpty { + guard policy != .customStrategy else { continue } let presetRules = v3GetPresetRules(preset: policy) entryRules = presetRules.0.map { convertSimulationRule($0, type: "entry", configID: config.id, tickerID: ticker) } exitRules = presetRules.1.map { convertSimulationRule($0, type: "exit", configID: config.id, tickerID: ticker) } diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f15f1..a78a9b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ https://keepachangelog.com/en/1.1.0/ ## [Unreleased] +### Fixed +- V3 simulations now honor supplied rules for named policies and only fall back to built-in rules when persisted rules are absent. +- Corrected Bollinger, Fast Stochastic, SMA crossover, EMA crossover, and custom-policy preset definitions. + ### Added - Additive portfolio orchestration APIs for app and engine integrators: - `BKEngine.PortfolioRequest` diff --git a/Tests/BacktestingKitTests/BacktestingKitPresetRuleTests.swift b/Tests/BacktestingKitTests/BacktestingKitPresetRuleTests.swift new file mode 100644 index 0000000..7dbb585 --- /dev/null +++ b/Tests/BacktestingKitTests/BacktestingKitPresetRuleTests.swift @@ -0,0 +1,137 @@ +import XCTest +@testable import BacktestingKit + +final class BacktestingKitPresetRuleTests: XCTestCase { + private struct InlineCSVProvider: BKRawCsvProvider { + func getRawCsv(ticker: String, p1: Double, p2: Double) async -> Result { + .success(""" + timestamp,open,high,low,close,volume + 2026-01-05,10,11,9,10,1000 + 2026-01-06,11,12,10,11,1000 + 2026-01-07,12,13,11,12,1000 + 2026-01-08,13,14,12,13,1000 + """) + } + } + + private actor NamedRuleStore: BKV3DataStore { + let config: BKV3_Config + let rules: [BKV3_SimulationRule] + var savedConfig: BKV3_Config? + + init(config: BKV3_Config, rules: [BKV3_SimulationRule]) { + self.config = config + self.rules = rules + } + + func getConfigs(instrumentID: String) async -> Result<[BKV3_Config], Error> { .success([config]) } + func getSimulationRules(configID: String, ruleType: String) async -> Result<[BKV3_SimulationRule], Error> { + .success(rules.filter { $0.configId == configID && $0.ruleType?.rawValue == ruleType }) + } + func saveConfig(_ config: BKV3_Config) async -> Result { + savedConfig = config + return .success(()) + } + func saveAnalysis(_ analysis: BKV3_AnalysisProfile) async -> Result { .success(()) } + func saveTrades(_ trades: [BKV3_TradeEntry]) async -> Result { .success(()) } + func saveSimulationRules(_ rules: [BKV3_SimulationRule]) async -> Result { .success(()) } + func saveRisks(_ risks: [BKV3_RiskProfile]) async -> Result { .success(()) } + func latestConfig() -> BKV3_Config? { savedConfig } + } + + func testCorrectedPresetDefinitions() { + let bollinger = bollingerPreset() + XCTAssertEqual(bollinger.entryRules[0].compare, .largerOrEqualTo) + + let fastStochastic = stochasticFastPreset() + XCTAssertEqual(fastStochastic.entryRules[0].indicatorTwoType, .stochasticFastPercentD) + XCTAssertEqual(fastStochastic.exitRules[0].indicatorTwoType, .stochasticFastPercentD) + + for crossover in [smaCrossoverPreset(), emaCrossoverPreset()] { + XCTAssertEqual(crossover.entryRules[0].compare, .largerOrEqualTo) + XCTAssertEqual(crossover.exitRules[0].compare, .smallerOrEqualTo) + XCTAssertEqual(crossover.entryRules[0].indicatorTwoFigure[0], 150) + XCTAssertEqual(crossover.exitRules[0].indicatorTwoFigure[0], 150) + } + + XCTAssertEqual(customPreset().policy, .customStrategy) + } + + func testV3NamedPresetUsesSuppliedRulesWhenAvailable() async { + let configID = "named-sma" + let config = BKV3_Config( + id: configID, + active: true, + createdAt: nil, + instrumentId: "TEST", + lastUpdated: nil, + status: "pending", + optimizeResultId: nil, + policy: .sma, + lastStatus: nil, + trailingStopLoss: false, + stopLossFigure: 15, + profitFactor: 4_294_967_296, + t1: 730, + t2: -1 + ) + let entry = rule( + configID: configID, + type: .entry, + lhsName: "entryClose", + lhsType: .close, + comparison: .largerThan, + rhsName: "entryZero" + ) + let exit = rule( + configID: configID, + type: .exit, + lhsName: "exitClose", + lhsType: .close, + comparison: .smallThan, + rhsName: "exitZero" + ) + let store = NamedRuleStore(config: config, rules: [entry, exit]) + let driver = BKSimulationDriver(dataStore: store, csvProvider: InlineCSVProvider()) + + let result = await driver.simulateInstrumentDetailed(BKV3_InstrumentInfo( + id: "TEST", + name: nil, + exchange: nil, + quoteType: nil, + createdAt: nil, + lastUpdated: nil + )) + + guard case .success(let report) = result else { + return XCTFail("Expected named canonical rules to simulate successfully") + } + let savedConfig = await store.latestConfig() + XCTAssertEqual(report.configCountProcessed, 1) + XCTAssertEqual(savedConfig?.lastStatus, .position) + } + + private func rule( + configID: String, + type: RuleType, + lhsName: String, + lhsType: TechnicalIndicators, + comparison: CompareOption, + rhsName: String + ) -> BKV3_SimulationRule { + convertSimulationRule( + SimulationRule( + indicatorOneName: lhsName, + indicatorOneType: lhsType, + indicatorOneFigure: [0, 0, 0], + compare: comparison, + indicatorTwoName: rhsName, + indicatorTwoType: .constant, + indicatorTwoFigure: [0, 0, 0] + ), + type: type.rawValue, + configID: configID, + tickerID: "TEST" + ) + } +} diff --git a/tasks/todo.md b/tasks/todo.md index cb2761d..5d32939 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -128,3 +128,18 @@ - Verification scan: no undocumented public `class`/`struct`/`enum`/`protocol`/`actor`/`typealias`/`func`/`init`/`subscript`/`var`/`let` declarations remain in `BacktestingKit/`. - Coverage now includes large model and facade/helper surfaces that were previously missing field-level Quick Help. - Verification: `swift test` passed with 111 tests and 0 failures after the full documentation sweep. +## 2026-08-23 v0.2.3 Canonical Preset Correctness + +- [x] Correct active built-in preset directions, operands, periods, and custom identity. +- [x] Make V3 prefer supplied rules for named policies while retaining legacy fallback behavior. +- [x] Add focused regression coverage for preset definitions and named canonical-rule execution. +- [x] Run targeted and full package tests. +- [ ] Commit, publish, and verify the v0.2.3 tag; update GitHub issue #7. + +### Review + +- Named V3 policies now consume supplied canonical rules; legacy configurations without complete stored rules still use built-in presets. +- Corrected the active Bollinger, Fast Stochastic, SMA crossover, EMA crossover, and custom-policy definitions. +- Verification: `swift test --filter BacktestingKitPresetRuleTests` passed 2 tests with 0 failures. +- Verification: `swift test` passed 130 tests with 0 failures. +- Verification: `swift build -c release` completed successfully.