Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions BacktestingKit/Engine/BKPresets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make crossover predicates mutually exclusive

When the 50- and 150-period averages are equal, the new entry condition (>=) and this exit condition (<=) are both true. On flat price histories or whenever the averages converge exactly, the state-based backtester can repeatedly enter and then exit positions even though no crossover occurred; the same overlapping pair is introduced in emaCrossoverPreset. Use non-overlapping predicates (or previous/current-value crossing detection) so equality alone cannot trigger both signals.

Useful? React with 👍 / 👎.

indicatorTwoName: "ruleTwoExit",
indicatorTwoType: .sma,
indicatorTwoFigure: [200, 0, 0]
indicatorTwoFigure: [150, 0, 0]
)
return SimulationPolicyConfig(
policy: .smaCrossover,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
31 changes: 15 additions & 16 deletions BacktestingKit/Simulation/V3/BKSimulationDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
137 changes: 137 additions & 0 deletions Tests/BacktestingKitTests/BacktestingKitPresetRuleTests.swift
Original file line number Diff line number Diff line change
@@ -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<String, Error> {
.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<Void, Error> {
savedConfig = config
return .success(())
}
func saveAnalysis(_ analysis: BKV3_AnalysisProfile) async -> Result<Void, Error> { .success(()) }
func saveTrades(_ trades: [BKV3_TradeEntry]) async -> Result<Void, Error> { .success(()) }
func saveSimulationRules(_ rules: [BKV3_SimulationRule]) async -> Result<Void, Error> { .success(()) }
func saveRisks(_ risks: [BKV3_RiskProfile]) async -> Result<Void, Error> { .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"
)
}
}
15 changes: 15 additions & 0 deletions tasks/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading