diff --git a/BacktestingKit/Analysis/BKBacktestCoreModels.swift b/BacktestingKit/Analysis/BKBacktestCoreModels.swift index 0b3e886..ad3364b 100644 --- a/BacktestingKit/Analysis/BKBacktestCoreModels.swift +++ b/BacktestingKit/Analysis/BKBacktestCoreModels.swift @@ -16,13 +16,21 @@ public enum PositionStatus: String, Codable { /// Represents `BKBar` in the BacktestingKit public API. public struct BKBar: Codable, Equatable { + /// Timestamp associated with this value. public var time: Date + /// Open price for the bar. public var open: Double + /// High price for the bar. public var high: Double + /// Low price for the bar. public var low: Double + /// Close price for the bar. public var close: Double + /// Adjusted close price for the bar when available. public var adjustedClose: Double? + /// Trading volume for the bar. public var volume: Double + /// Indicators associated with this value. public var indicators: [String: Double] /// Creates a new instance. @@ -62,7 +70,9 @@ public struct BKBar: Codable, Equatable { /// Represents `BKBacktestOptions` in the BacktestingKit public API. public struct BKBacktestOptions { + /// Whether to record stop price. public var recordStopPrice: Bool + /// Whether to record risk. public var recordRisk: Bool /// Creates a new instance. @@ -74,21 +84,37 @@ public struct BKBacktestOptions { /// Represents `BKPosition` in the BacktestingKit public API. public struct BKPosition { + /// Direction associated with this value. public var direction: TradeDirection + /// Entry time associated with this value. public var entryTime: Date + /// Entry price associated with this value. public var entryPrice: Double + /// Profit associated with this value. public var profit: Double + /// Profit percentage associated with this value. public var profitPct: Double + /// Growth associated with this value. public var growth: Double + /// Initial unit risk associated with this value. public var initialUnitRisk: Double? + /// Initial risk percentage associated with this value. public var initialRiskPct: Double? + /// Current risk percentage associated with this value. public var curRiskPct: Double? + /// Current r multiple associated with this value. public var curRMultiple: Double? + /// Risk series associated with this value. public var riskSeries: [BKTimestampedValue]? + /// Holding period associated with this value. public var holdingPeriod: Int + /// Initial stop price associated with this value. public var initialStopPrice: Double? + /// Current stop price associated with this value. public var curStopPrice: Double? + /// Stop price series associated with this value. public var stopPriceSeries: [BKTimestampedValue]? + /// Profit target associated with this value. public var profitTarget: Double? } @@ -99,7 +125,9 @@ public typealias ExitPositionFn = () -> Void /// Represents `BKEnterPositionOptions` in the BacktestingKit public API. public struct BKEnterPositionOptions { + /// Direction associated with this value. public var direction: TradeDirection? + /// Entry price associated with this value. public var entryPrice: Double? /// Creates a new instance. @@ -111,8 +139,11 @@ public struct BKEnterPositionOptions { /// Represents `BKRuleParams` in the BacktestingKit public API. public struct BKRuleParams { + /// Bar associated with this value. public var bar: BKBar + /// Lookback associated with this value. public var lookback: [BKBar] + /// Parameters associated with this value. public var parameters: [String: Double] /// Creates a new instance. @@ -125,10 +156,15 @@ public struct BKRuleParams { /// Represents `BKOpenPositionRuleArgs` in the BacktestingKit public API. public struct BKOpenPositionRuleArgs { + /// Entry price associated with this value. public var entryPrice: Double + /// Position associated with this value. public var position: BKPosition + /// Bar associated with this value. public var bar: BKBar + /// Lookback associated with this value. public var lookback: [BKBar] + /// Parameters associated with this value. public var parameters: [String: Double] } @@ -143,13 +179,21 @@ public typealias BKProfitTargetFn = (_ args: BKOpenPositionRuleArgs) -> Double /// Represents `BKStrategy` in the BacktestingKit public API. public struct BKStrategy { + /// Parameters associated with this value. public var parameters: [String: Double] + /// Lookback period associated with this value. public var lookbackPeriod: Int + /// Prep indicators associated with this value. public var prepIndicators: ((_ input: [BKBar], _ parameters: [String: Double]) -> [BKBar])? + /// Entry rule associated with this value. public var entryRule: BKEntryRuleFn + /// Exit rule associated with this value. public var exitRule: BKExitRuleFn? + /// Stop loss associated with this value. public var stopLoss: BKStopLossFn? + /// Trailing stop loss associated with this value. public var trailingStopLoss: BKStopLossFn? + /// Profit target associated with this value. public var profitTarget: BKProfitTargetFn? /// Creates a new instance. diff --git a/BacktestingKit/Analysis/BKOptimizationModels.swift b/BacktestingKit/Analysis/BKOptimizationModels.swift index 79fb37a..ec160be 100644 --- a/BacktestingKit/Analysis/BKOptimizationModels.swift +++ b/BacktestingKit/Analysis/BKOptimizationModels.swift @@ -11,9 +11,13 @@ public enum OptimizeSearchDirection: String, Codable { /// Represents `ParameterDef` in the BacktestingKit public API. public struct ParameterDef: Codable, Equatable { + /// Name associated with this value. public var name: String + /// Starting value associated with this value. public var startingValue: Double + /// Ending value associated with this value. public var endingValue: Double + /// Step size associated with this value. public var stepSize: Double } @@ -25,11 +29,17 @@ public enum OptimizationType: String, Codable { /// Represents `OptimizationOptions` in the BacktestingKit public API. public struct OptimizationOptions: Codable, Equatable { + /// Search direction associated with this value. public var searchDirection: OptimizeSearchDirection? + /// Optimization type associated with this value. public var optimizationType: OptimizationType? + /// Whether to record all results. public var recordAllResults: Bool? + /// Random seed associated with this value. public var randomSeed: Double? + /// Number starting points represented by this value. public var numStartingPoints: Int? + /// Whether to record duration. public var recordDuration: Bool? /// Creates a new instance. @@ -52,26 +62,35 @@ public struct OptimizationOptions: Codable, Equatable { /// Represents `OptimizationIterationResult` in the BacktestingKit public API. public struct OptimizationIterationResult: Codable, Equatable { + /// Params associated with this value. public var params: ParameterT + /// Result associated with this value. public var result: Double + /// Number trades represented by this value. public var numTrades: Int } /// Represents `OptimizationResult` in the BacktestingKit public API. public struct OptimizationResult: Codable, Equatable { + /// Best result associated with this value. public var bestResult: Double + /// Best parameter values associated with this value. public var bestParameterValues: ParameterT + /// All results associated with this value. public var allResults: [OptimizationIterationResult]? + /// Duration ms associated with this value. public var durationMS: Double? } /// Represents `WalkForwardOptimizationResult` in the BacktestingKit public API. public struct WalkForwardOptimizationResult: Codable, Equatable { + /// Trades associated with this value. public var trades: [BKTrade] } /// Represents `MonteCarloOptions` in the BacktestingKit public API. public struct MonteCarloOptions: Codable, Equatable { + /// Random seed associated with this value. public var randomSeed: Double? /// Creates a new instance. public init(randomSeed: Double? = nil) { diff --git a/BacktestingKit/Analysis/BKRandom.swift b/BacktestingKit/Analysis/BKRandom.swift index 5b4f74f..fa38560 100644 --- a/BacktestingKit/Analysis/BKRandom.swift +++ b/BacktestingKit/Analysis/BKRandom.swift @@ -1,5 +1,6 @@ import Foundation +/// Deterministic pseudo-random number generator used by optimization helpers. public final class Random { private var state: UInt64 diff --git a/BacktestingKit/App/BKAppFacade.swift b/BacktestingKit/App/BKAppFacade.swift index 68024e3..8cf7dc4 100644 --- a/BacktestingKit/App/BKAppFacade.swift +++ b/BacktestingKit/App/BKAppFacade.swift @@ -602,6 +602,112 @@ public enum BKAppFacade { ) } + /// Builds the full review state for an app-facing portfolio basket before execution. + public static func buildPortfolioCSVImportScreenState( + portfolioID: String = "PORTFOLIO", + sleeves: [BKAppPortfolioImportItem], + allocation: BKPortfolioAllocationInput = .sleeveWeights, + rebalancePolicy: BKPortfolioRebalancePolicy = .none, + maxRows: Int = 5 + ) -> BKAppPortfolioImportScreenState { + let normalizedPortfolioID = portfolioID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + ? "PORTFOLIO" + : portfolioID + let sleeveStates = sleeves.map { sleeve in + BKAppPortfolioImportItemState( + request: sleeve, + screenState: buildCSVImportScreenState( + symbol: sleeve.symbol, + csv: sleeve.csv, + maxRows: maxRows + ) + ) + } + + let sleeveIssues = sleeveStates.flatMap { sleeveState in + sleeveState.screenState.issues.map { section in + BKAppPortfolioImportIssueSection( + symbol: sleeveState.request.symbol, + title: section.title, + items: section.items + ) + } + } + let portfolioIssues = buildPortfolioImportIssueSections( + portfolioID: normalizedPortfolioID, + sleeves: sleeveStates, + allocation: allocation + ) + let issues = sleeveIssues + portfolioIssues + + let status: BKAppCSVImportScreenStatus + if sleeveStates.isEmpty + || !portfolioIssues.isEmpty + || sleeveStates.contains(where: { $0.screenState.status == .invalid }) { + status = .invalid + } else if sleeveStates.contains(where: { $0.screenState.status == .needsReview }) { + status = .needsReview + } else { + status = .ready + } + + return BKAppPortfolioImportScreenState( + portfolioID: normalizedPortfolioID, + sleeves: sleeveStates, + allocation: allocation, + rebalancePolicy: rebalancePolicy, + issues: issues, + status: status, + isReadyToContinue: status == .ready + ) + } + + /// Runs a confirmed app-side basket import using either explicit overrides or inferred settings per sleeve. + public static func runConfirmedPortfolioCSVImport( + from screenState: BKAppPortfolioImportScreenState, + confirmedSettingsBySymbol: [String: BKAppCSVConfirmedImportSettings] = [:], + continueOnFailure: Bool = true, + log: @escaping @Sendable (String) -> Void = { _ in } + ) -> BKAppPortfolioConfirmedRunReport { + let sleeves = screenState.sleeves.map { sleeveState -> BKPortfolioSleeveRequest in + let confirmedSettings = confirmedSettingsBySymbol[sleeveState.request.symbol] + let settings = confirmedSettings ?? BKAppCSVConfirmedImportSettings( + columnMapping: sleeveState.screenState.inference.effectiveSettings.columnMapping, + dateFormat: sleeveState.screenState.inference.effectiveSettings.dateFormat, + reverse: sleeveState.screenState.inference.effectiveSettings.reverse + ) + let effectiveCSV = confirmedSettings == nil + ? autoPreparedCSV( + csv: sleeveState.request.csv, + inference: sleeveState.screenState.inference + ) + : sleeveState.request.csv + + return BKPortfolioSleeveRequest( + symbol: sleeveState.request.symbol, + csv: effectiveCSV, + preset: sleeveState.request.preset, + dateFormat: settings.dateFormat, + reverse: settings.reverse, + columnMapping: settings.columnMapping, + targetWeight: sleeveState.request.targetWeight + ) + } + + let request = BKEngine.PortfolioRequest( + portfolioID: screenState.portfolioID, + sleeves: sleeves, + allocation: screenState.allocation, + rebalancePolicy: screenState.rebalancePolicy, + continueOnFailure: continueOnFailure + ) + + return BKAppPortfolioConfirmedRunReport( + confirmedSettingsBySymbol: confirmedSettingsBySymbol, + run: BKEngine.runPortfolio(request, log: log) + ) + } + /// Runs CSV inspection, validation, normalization, and preset execution in one app-facing helper. public static func runCSVImport( symbol: String, @@ -825,6 +931,28 @@ public enum BKAppFacade { ) } + /// Exports a completed portfolio run into a portable bundle. + public static func exportPortfolioRunBundle( + _ report: BKPortfolioRunReport, + prettyPrinted: Bool = true + ) -> Result { + BKExportTool.exportPortfolioRunBundle( + report, + prettyPrinted: prettyPrinted + ) + } + + /// Exports a portfolio summary as human-readable Markdown. + public static func exportPortfolioMarkdownSummary( + _ report: BKPortfolioRunReport, + title: String? = nil + ) -> Result { + BKExportTool.exportPortfolioMarkdownSummary( + report, + title: title + ) + } + /// Compares two summaries and flags differences larger than the supplied tolerance. public static func compareRuns( baseline: BKRunSummary, @@ -838,7 +966,7 @@ public enum BKAppFacade { ) } - /// Throws when two summaries are not equivalent within the supplied tolerance. + /// Throws when two run summaries are not equivalent within the supplied tolerance. @discardableResult public static func assertEquivalent( baseline: BKRunSummary, @@ -1367,6 +1495,97 @@ public enum BKAppFacade { return sections } + private static func buildPortfolioImportIssueSections( + portfolioID: String, + sleeves: [BKAppPortfolioImportItemState], + allocation: BKPortfolioAllocationInput + ) -> [BKAppPortfolioImportIssueSection] { + var items: [BKAppCSVImportIssueItem] = [] + let duplicateSymbols = duplicatePortfolioImportSymbols(in: sleeves) + if !duplicateSymbols.isEmpty { + items.append( + BKAppCSVImportIssueItem( + severity: .error, + code: "portfolio_duplicate_symbols", + message: "Portfolio sleeve symbols must be unique. Duplicates: \(duplicateSymbols.joined(separator: ", ")).", + source: .validation + ) + ) + } + + switch allocation.mode { + case .explicit: + let weights = allocation.explicitWeights ?? [] + if weights.count != sleeves.count { + items.append( + BKAppCSVImportIssueItem( + severity: .error, + code: "portfolio_explicit_weight_count_mismatch", + message: "Explicit portfolio weights must match the sleeve count.", + source: .validation + ) + ) + } else { + let clampedTotal = weights.reduce(0.0) { partial, weight in + partial + max(weight, 0) + } + if clampedTotal <= 0 { + items.append( + BKAppCSVImportIssueItem( + severity: .error, + code: "portfolio_explicit_weight_total_invalid", + message: "Explicit portfolio weights must resolve to a positive total.", + source: .validation + ) + ) + } + } + + case .riskOnRiskOff: + let count = sleeves.count + let riskOnIndex = allocation.riskOnIndex ?? 0 + let riskOffIndex = allocation.riskOffIndex ?? 1 + if !(riskOnIndex >= 0 + && riskOnIndex < count + && riskOffIndex >= 0 + && riskOffIndex < count + && riskOnIndex != riskOffIndex) { + items.append( + BKAppCSVImportIssueItem( + severity: .error, + code: "portfolio_risk_on_risk_off_indices", + message: "Risk-on / risk-off allocation requires two distinct valid sleeve indices.", + source: .validation + ) + ) + } + + case .sleeveWeights, .riskParity: + break + } + + guard !items.isEmpty else { return [] } + return [BKAppPortfolioImportIssueSection(symbol: portfolioID, title: "Portfolio", items: items)] + } + + private static func duplicatePortfolioImportSymbols( + in sleeves: [BKAppPortfolioImportItemState] + ) -> [String] { + var seen: Set = [] + var duplicates: Set = [] + + for sleeve in sleeves { + let symbol = sleeve.request.symbol.trimmingCharacters(in: .whitespacesAndNewlines) + if seen.contains(symbol) { + duplicates.insert(symbol) + } else { + seen.insert(symbol) + } + } + + return duplicates.sorted() + } + private static func importScreenStatus( inspection: BKAppCSVInspectionReport, validation: BKAppCSVAutoValidationReport?, diff --git a/BacktestingKit/App/BKAppFacadeModels.swift b/BacktestingKit/App/BKAppFacadeModels.swift index dc3e8e9..ffb8b44 100644 --- a/BacktestingKit/App/BKAppFacadeModels.swift +++ b/BacktestingKit/App/BKAppFacadeModels.swift @@ -2,9 +2,13 @@ import Foundation /// Result of running a preset-backed inline CSV workflow and exporting the outcome as Markdown. public struct BKAppPresetMarkdownReport { + /// Run associated with this value. public var run: BKPreflightedRunSummary + /// Markdown export generated for this value. public var markdown: String? + /// Export failure associated with this value when generation does not succeed. public var exportError: BKExportError? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. @@ -23,10 +27,15 @@ public struct BKAppPresetMarkdownReport { /// Result of running a deterministic scenario and exporting the outcome as a portable bundle. public struct BKAppScenarioBundleReport { + /// Configuration associated with this value. public var config: BKScenarioConfig + /// High-level summary associated with this value. public var summary: BKRunSummary + /// Export bundle associated with this value. public var exportBundle: BKRunExportBundle? + /// Export failure associated with this value when generation does not succeed. public var exportError: BKExportError? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. diff --git a/BacktestingKit/App/BKAppImportModels.swift b/BacktestingKit/App/BKAppImportModels.swift index 62b1bbc..d76059c 100644 --- a/BacktestingKit/App/BKAppImportModels.swift +++ b/BacktestingKit/App/BKAppImportModels.swift @@ -2,12 +2,19 @@ import Foundation /// App-facing inspection result for pasted inline CSV workflows. public struct BKAppCSVInspectionReport: Codable, Equatable, Sendable { + /// Ticker symbol associated with this value. public var symbol: String + /// Column mapping associated with this value. public var columnMapping: BKCSVColumnMapping? + /// Preflight validation output associated with this value. public var preflight: BKToolPreflightReport + /// Number of issues represented by this value. public var issueCount: Int + /// Number of warnings represented by this value. public var warningCount: Int + /// Number of errors represented by this value. public var errorCount: Int + /// Whether this value is ready for the next workflow step. public var isReady: Bool /// Creates a new instance. @@ -32,9 +39,13 @@ public struct BKAppCSVInspectionReport: Codable, Equatable, Sendable { /// A single CSV inference finding for app-facing import flows. public struct BKAppCSVInferenceIssue: Codable, Equatable, Sendable { + /// Machine-readable code associated with this value. public var code: String + /// Human-readable message associated with this value. public var message: String + /// Severity associated with this value. public var severity: BKValidationSeverity + /// Additional metadata associated with this value. public var metadata: [String: String] /// Creates a new instance. @@ -53,8 +64,11 @@ public struct BKAppCSVInferenceIssue: Codable, Equatable, Sendable { /// Inferred CSV import settings before default fallback is applied. public struct BKAppCSVInferredSettings: Codable, Equatable, Sendable { + /// Column mapping associated with this value. public var columnMapping: BKCSVColumnMapping? + /// Date format string used while parsing or formatting input. public var dateFormat: String? + /// Whether the input should be reversed into chronological order. public var reverse: Bool? /// Creates a new instance. @@ -71,8 +85,11 @@ public struct BKAppCSVInferredSettings: Codable, Equatable, Sendable { /// Effective CSV import settings after inference fallback/default resolution. public struct BKAppCSVEffectiveSettings: Codable, Equatable, Sendable { + /// Column mapping associated with this value. public var columnMapping: BKCSVColumnMapping? + /// Date format string used while parsing or formatting input. public var dateFormat: String + /// Whether the input should be reversed into chronological order. public var reverse: Bool /// Creates a new instance. @@ -89,11 +106,17 @@ public struct BKAppCSVEffectiveSettings: Codable, Equatable, Sendable { /// Safe CSV import inference result for app-facing CSV workflows. public struct BKAppCSVInferenceReport: Codable, Equatable, Sendable { + /// Ticker symbol associated with this value. public var symbol: String + /// Inspection result associated with this value. public var inspection: BKAppCSVInspectionReport + /// Inferred settings associated with this value. public var inferredSettings: BKAppCSVInferredSettings + /// Effective settings associated with this value. public var effectiveSettings: BKAppCSVEffectiveSettings + /// Issues associated with this value. public var issues: [BKAppCSVInferenceIssue] + /// Whether all import settings were inferred without ambiguity. public var isFullyInferred: Bool /// Creates a new instance. @@ -116,12 +139,19 @@ public struct BKAppCSVInferenceReport: Codable, Equatable, Sendable { /// Lightweight preview row for app-side CSV inspection UIs. public struct BKAppCSVPreviewRow: Codable, Equatable, Sendable { + /// Date associated with this value. public var date: Date + /// Open price for the bar. public var open: Double + /// High price for the bar. public var high: Double + /// Low price for the bar. public var low: Double + /// Close price for the bar. public var close: Double + /// Adjusted close price for the bar when available. public var adjustedClose: Double? + /// Trading volume for the bar. public var volume: Double /// Creates a new instance. @@ -146,16 +176,27 @@ public struct BKAppCSVPreviewRow: Codable, Equatable, Sendable { /// Preview-oriented report for CSV parsing and bounded row inspection. public struct BKAppCSVPreviewReport: Codable, Equatable, Sendable { + /// Ticker symbol associated with this value. public var symbol: String + /// Date format string used while parsing or formatting input. public var dateFormat: String + /// Whether the input should be reversed into chronological order. public var reverse: Bool + /// Row limit associated with this value. public var rowLimit: Int + /// Inspection result associated with this value. public var inspection: BKAppCSVInspectionReport + /// Number of rows represented by this value. public var rowCount: Int + /// Start date represented by this value. public var startDate: Date? + /// End date represented by this value. public var endDate: Date? + /// Rows associated with this value. public var rows: [BKAppCSVPreviewRow] + /// Parse error associated with this value. public var parseError: String? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. @@ -188,9 +229,13 @@ public struct BKAppCSVPreviewReport: Codable, Equatable, Sendable { /// Compact preview summary for app-facing CSV diagnostics. public struct BKAppCSVPreviewSummary: Codable, Equatable, Sendable { + /// Number of rows represented by this value. public var rowCount: Int + /// Start date represented by this value. public var startDate: Date? + /// End date represented by this value. public var endDate: Date? + /// Effective settings associated with this value. public var effectiveSettings: BKAppCSVEffectiveSettings /// Creates a new instance. @@ -209,7 +254,9 @@ public struct BKAppCSVPreviewSummary: Codable, Equatable, Sendable { /// Auto-inference wrapper around `BKAppCSVPreviewReport`. public struct BKAppCSVAutoPreviewReport: Codable, Equatable, Sendable { + /// Inferred settings and issues associated with this value. public var inference: BKAppCSVInferenceReport + /// Preview output associated with this value. public var preview: BKAppCSVPreviewReport /// Creates a new instance. @@ -224,10 +271,15 @@ public struct BKAppCSVAutoPreviewReport: Codable, Equatable, Sendable { /// Validation bundle that combines structural CSV preflight with parse-stage validation. public struct BKAppCSVValidationReport: Codable, Equatable, Sendable { + /// Ticker symbol associated with this value. public var symbol: String + /// Inspection result associated with this value. public var inspection: BKAppCSVInspectionReport + /// Parse validation associated with this value. public var parseValidation: BKValidationReport + /// Parse error associated with this value. public var parseError: String? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. @@ -248,7 +300,9 @@ public struct BKAppCSVValidationReport: Codable, Equatable, Sendable { /// Auto-inference wrapper around `BKAppCSVValidationReport`. public struct BKAppCSVAutoValidationReport: Codable, Equatable, Sendable { + /// Inferred settings and issues associated with this value. public var inference: BKAppCSVInferenceReport + /// Validation output associated with this value. public var validation: BKAppCSVValidationReport /// Creates a new instance. @@ -263,14 +317,23 @@ public struct BKAppCSVAutoValidationReport: Codable, Equatable, Sendable { /// Normalized parse output for app-side CSV import flows. public struct BKAppCSVNormalizedReport: Codable, Equatable { + /// Ticker symbol associated with this value. public var symbol: String + /// Validation output associated with this value. public var validation: BKAppCSVValidationReport + /// Bars associated with this value. public var bars: [BKBar] + /// Candles associated with this value. public var candles: [Candlestick] + /// Number of rows represented by this value. public var rowCount: Int + /// Start date represented by this value. public var startDate: Date? + /// End date represented by this value. public var endDate: Date? + /// Parse error associated with this value. public var parseError: String? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. @@ -299,9 +362,13 @@ public struct BKAppCSVNormalizedReport: Codable, Equatable { /// Compact normalization summary for app-facing CSV diagnostics. public struct BKAppCSVNormalizationSummary: Codable, Equatable, Sendable { + /// Number of rows represented by this value. public var rowCount: Int + /// Start date represented by this value. public var startDate: Date? + /// End date represented by this value. public var endDate: Date? + /// Ordering normalized associated with this value. public var orderingNormalized: Bool /// Creates a new instance. @@ -320,7 +387,9 @@ public struct BKAppCSVNormalizationSummary: Codable, Equatable, Sendable { /// Auto-inference wrapper around `BKAppCSVNormalizedReport`. public struct BKAppCSVAutoNormalizedReport: Equatable { + /// Inferred settings and issues associated with this value. public var inference: BKAppCSVInferenceReport + /// Normalization output associated with this value. public var normalization: BKAppCSVNormalizedReport /// Creates a new instance. @@ -361,8 +430,11 @@ public enum BKAppCSVImportFailureStage: String, Codable, Equatable, Sendable { /// A single stage decision captured by the CSV import diagnostics helper. public struct BKAppCSVImportStageDecision: Codable, Equatable, Sendable { + /// Current processing stage associated with this value. public var stage: BKAppCSVImportDiagnosticStage + /// Outcome recorded for the associated stage. public var outcome: BKAppCSVImportStageOutcome + /// Human-readable message associated with this value. public var message: String /// Creates a new instance. @@ -379,8 +451,11 @@ public struct BKAppCSVImportStageDecision: Codable, Equatable, Sendable { /// A bounded concrete row failure example emitted by the CSV import diagnostics helper. public struct BKAppCSVRowFailureExample: Codable, Equatable, Sendable { + /// Row index associated with this value. public var rowIndex: Int + /// Raw row associated with this value. public var rawRow: String + /// Human-readable message associated with this value. public var message: String /// Creates a new instance. @@ -397,14 +472,23 @@ public struct BKAppCSVRowFailureExample: Codable, Equatable, Sendable { /// Developer-facing diagnostics report for the app-side CSV import pipeline. public struct BKAppCSVImportDiagnosticsReport: Codable, Equatable, Sendable { + /// Ticker symbol associated with this value. public var symbol: String + /// Inspection result associated with this value. public var inspection: BKAppCSVInspectionReport + /// Inferred settings and issues associated with this value. public var inference: BKAppCSVInferenceReport + /// Stage decisions associated with this value. public var stageDecisions: [BKAppCSVImportStageDecision] + /// Failure stage associated with this value. public var failureStage: BKAppCSVImportFailureStage? + /// Row failures associated with this value. public var rowFailures: [BKAppCSVRowFailureExample] + /// Preview summary associated with this value. public var previewSummary: BKAppCSVPreviewSummary? + /// Normalization summary associated with this value. public var normalizationSummary: BKAppCSVNormalizationSummary? + /// Whether the import can proceed without blocking issues. public var isImportViable: Bool /// Creates a new instance. @@ -433,13 +517,21 @@ public struct BKAppCSVImportDiagnosticsReport: Codable, Equatable, Sendable { /// Aggregated app-facing output for validate + normalize + preset execution workflows. public struct BKAppCSVImportRunReport { + /// Ticker symbol associated with this value. public var symbol: String + /// Preset associated with this value. public var preset: BKPresetCatalog + /// Validation output associated with this value. public var validation: BKAppCSVValidationReport + /// Normalization output associated with this value. public var normalization: BKAppCSVNormalizedReport? + /// Run associated with this value. public var run: BKPreflightedRunSummary? + /// High-level summary associated with this value. public var summary: BKRunSummary? + /// Human-readable failure description when execution does not succeed. public var failureDescription: String? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. @@ -466,7 +558,9 @@ public struct BKAppCSVImportRunReport { /// Auto-inference wrapper around `BKAppCSVImportRunReport`. public struct BKAppCSVAutoRunReport { + /// Inferred settings and issues associated with this value. public var inference: BKAppCSVInferenceReport + /// Run associated with this value. public var run: BKAppCSVImportRunReport /// Creates a new instance. @@ -481,8 +575,11 @@ public struct BKAppCSVAutoRunReport { /// Explicit CSV import settings confirmed by the app after review or user override. public struct BKAppCSVConfirmedImportSettings: Codable, Equatable, Sendable { + /// Column mapping associated with this value. public var columnMapping: BKCSVColumnMapping? + /// Date format string used while parsing or formatting input. public var dateFormat: String + /// Whether the input should be reversed into chronological order. public var reverse: Bool /// Creates a new instance. @@ -499,7 +596,9 @@ public struct BKAppCSVConfirmedImportSettings: Codable, Equatable, Sendable { /// Result of executing a confirmed CSV import after an app-side review step. public struct BKAppCSVConfirmedRunReport { + /// Confirmed settings associated with this value. public var confirmedSettings: BKAppCSVConfirmedImportSettings + /// Run associated with this value. public var run: BKAppCSVImportRunReport /// Creates a new instance. @@ -514,9 +613,13 @@ public struct BKAppCSVConfirmedRunReport { /// Result of running an app-facing CSV import workflow and exporting the outcome as Markdown. public struct BKAppCSVImportMarkdownReport { + /// Run associated with this value. public var run: BKAppCSVImportRunReport + /// Markdown export generated for this value. public var markdown: String? + /// Export failure associated with this value when generation does not succeed. public var exportError: BKExportError? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. @@ -535,7 +638,9 @@ public struct BKAppCSVImportMarkdownReport { /// Auto-inference wrapper around `BKAppCSVImportMarkdownReport`. public struct BKAppCSVAutoMarkdownReport { + /// Inferred settings and issues associated with this value. public var inference: BKAppCSVInferenceReport + /// Run associated with this value. public var run: BKAppCSVImportMarkdownReport /// Creates a new instance. @@ -564,9 +669,13 @@ public enum BKAppCSVImportIssueSource: String, Codable, Equatable, Sendable { /// A single user-displayable issue row for import-review UIs. public struct BKAppCSVImportIssueItem: Codable, Equatable, Sendable { + /// Severity associated with this value. public var severity: BKValidationSeverity + /// Machine-readable code associated with this value. public var code: String + /// Human-readable message associated with this value. public var message: String + /// Source associated with this value. public var source: BKAppCSVImportIssueSource /// Creates a new instance. @@ -585,7 +694,9 @@ public struct BKAppCSVImportIssueItem: Codable, Equatable, Sendable { /// A grouped section of issues for app-facing import-review UIs. public struct BKAppCSVImportIssueSection: Codable, Equatable, Sendable { + /// Title associated with this value. public var title: String + /// Items associated with this value. public var items: [BKAppCSVImportIssueItem] /// Creates a new instance. @@ -600,14 +711,23 @@ public struct BKAppCSVImportIssueSection: Codable, Equatable, Sendable { /// Aggregated screen-state payload for app-facing CSV import review flows. public struct BKAppCSVImportScreenState: Equatable { + /// Ticker symbol associated with this value. public var symbol: String + /// Inspection result associated with this value. public var inspection: BKAppCSVInspectionReport + /// Inferred settings and issues associated with this value. public var inference: BKAppCSVInferenceReport + /// Preview output associated with this value. public var preview: BKAppCSVAutoPreviewReport? + /// Validation output associated with this value. public var validation: BKAppCSVAutoValidationReport? + /// Normalization output associated with this value. public var normalization: BKAppCSVAutoNormalizedReport? + /// Issues associated with this value. public var issues: [BKAppCSVImportIssueSection] + /// Current status associated with this value. public var status: BKAppCSVImportScreenStatus + /// Whether the workflow can continue without additional user input. public var isReadyToContinue: Bool /// Creates a new instance. diff --git a/BacktestingKit/App/BKAppPortfolioImportModels.swift b/BacktestingKit/App/BKAppPortfolioImportModels.swift new file mode 100644 index 0000000..cddfeee --- /dev/null +++ b/BacktestingKit/App/BKAppPortfolioImportModels.swift @@ -0,0 +1,118 @@ +import Foundation + +/// App-facing basket input for portfolio CSV review and execution workflows. +public struct BKAppPortfolioImportItem: Codable, Equatable, Sendable { + /// Sleeve identifier associated with this value. + public var symbol: String + /// CSV payload associated with this value. + public var csv: String + /// Preset associated with this value. + public var preset: BKPresetCatalog + /// Caller-supplied target weight associated with this value. + public var targetWeight: Double? + + /// Creates a new instance. + public init( + symbol: String, + csv: String, + preset: BKPresetCatalog, + targetWeight: Double? = nil + ) { + self.symbol = symbol + self.csv = csv + self.preset = preset + self.targetWeight = targetWeight + } +} + +/// Per-sleeve review output for app-facing portfolio import flows. +public struct BKAppPortfolioImportItemState: Equatable { + /// Basket request associated with this value. + public var request: BKAppPortfolioImportItem + /// Resolved single-sleeve screen state associated with this value. + public var screenState: BKAppCSVImportScreenState + + /// Creates a new instance. + public init( + request: BKAppPortfolioImportItem, + screenState: BKAppCSVImportScreenState + ) { + self.request = request + self.screenState = screenState + } +} + +/// Grouped issue section for an app-facing portfolio import review screen. +public struct BKAppPortfolioImportIssueSection: Codable, Equatable, Sendable { + /// Sleeve identifier associated with this value. + public var symbol: String + /// Title associated with this value. + public var title: String + /// Items associated with this value. + public var items: [BKAppCSVImportIssueItem] + + /// Creates a new instance. + public init( + symbol: String, + title: String, + items: [BKAppCSVImportIssueItem] + ) { + self.symbol = symbol + self.title = title + self.items = items + } +} + +/// Aggregated screen-state payload for app-facing portfolio review flows. +public struct BKAppPortfolioImportScreenState: Equatable { + /// Portfolio identifier associated with this value. + public var portfolioID: String + /// Reviewed sleeve states associated with this value. + public var sleeves: [BKAppPortfolioImportItemState] + /// Allocation input associated with this value. + public var allocation: BKPortfolioAllocationInput + /// Rebalance policy associated with this value. + public var rebalancePolicy: BKPortfolioRebalancePolicy + /// Issues associated with this value. + public var issues: [BKAppPortfolioImportIssueSection] + /// Current status associated with this value. + public var status: BKAppCSVImportScreenStatus + /// Whether the workflow can continue without additional user input. + public var isReadyToContinue: Bool + + /// Creates a new instance. + public init( + portfolioID: String, + sleeves: [BKAppPortfolioImportItemState], + allocation: BKPortfolioAllocationInput, + rebalancePolicy: BKPortfolioRebalancePolicy, + issues: [BKAppPortfolioImportIssueSection], + status: BKAppCSVImportScreenStatus, + isReadyToContinue: Bool + ) { + self.portfolioID = portfolioID + self.sleeves = sleeves + self.allocation = allocation + self.rebalancePolicy = rebalancePolicy + self.issues = issues + self.status = status + self.isReadyToContinue = isReadyToContinue + } +} + +/// Result of confirming per-sleeve review settings and executing a portfolio run. +public struct BKAppPortfolioConfirmedRunReport: Equatable { + /// Confirmed per-symbol settings associated with this value. + public var confirmedSettingsBySymbol: [String: BKAppCSVConfirmedImportSettings] + /// Portfolio run associated with this value. + public var run: BKPortfolioRunReport + + /// Creates a new instance. + public init( + confirmedSettingsBySymbol: [String: BKAppCSVConfirmedImportSettings], + run: BKPortfolioRunReport + ) { + self.confirmedSettingsBySymbol = confirmedSettingsBySymbol + self.run = run + } +} diff --git a/BacktestingKit/BacktestingKit.docc/BKAppIntegrationTutorial.tutorial b/BacktestingKit/BacktestingKit.docc/BKAppIntegrationTutorial.tutorial index 356b655..e82def2 100644 --- a/BacktestingKit/BacktestingKit.docc/BKAppIntegrationTutorial.tutorial +++ b/BacktestingKit/BacktestingKit.docc/BKAppIntegrationTutorial.tutorial @@ -5,7 +5,7 @@ @Section(title: "Start with BKAppFacade for import-review screens") { @ContentAndMedia { - Prefer ``BKAppFacade`` when your app starts from pasted, uploaded, or edited CSV and needs one stable review payload before execution. + Prefer ``BKAppFacade`` when your app starts from pasted, uploaded, or edited CSV and needs one stable review payload before execution. Success looks like a review-state payload plus a confirmed handoff path that keeps parsing logic out of UI code. } @Steps { @@ -48,7 +48,7 @@ @Section(title: "Drop to BKEngine when you need direct request control") { @ContentAndMedia { - Prefer ``BKEngine`` when integrating the package into an app at the request-model layer. It is the stable top-level execution surface for v2 and v3 flows. + Prefer ``BKEngine`` when integrating the package into an app at the request-model layer. It is the stable top-level execution surface for v2 and v3 flows, and success looks like your app owning request construction directly. } } @@ -71,6 +71,10 @@ } @Section(title: "Run the canonical v3 flow") { + @ContentAndMedia { + Success looks like a direct `await BKEngine.runV3(...)` call that you can treat as the engine boundary in app code. + } + @Steps { @Step { Build a request: diff --git a/BacktestingKit/BacktestingKit.docc/BKCSVImportTutorial.tutorial b/BacktestingKit/BacktestingKit.docc/BKCSVImportTutorial.tutorial index f9c89a4..de1cbe2 100644 --- a/BacktestingKit/BacktestingKit.docc/BKCSVImportTutorial.tutorial +++ b/BacktestingKit/BacktestingKit.docc/BKCSVImportTutorial.tutorial @@ -5,7 +5,7 @@ @Section(title: "Start with one review-state helper") { @ContentAndMedia { - Use ``BKAppFacade/buildCSVImportScreenState(symbol:csv:maxRows:)`` when your app needs inspection, inferred settings, preview rows, grouped issues, validation, normalization, and a readiness flag in one payload. + Use ``BKAppFacade/buildCSVImportScreenState(symbol:csv:maxRows:)`` when your app needs inspection, inferred settings, preview rows, grouped issues, validation, normalization, and a readiness flag in one payload. Success looks like a single review-state model that the UI can render directly. } @Steps { @@ -34,7 +34,7 @@ @Section(title: "Gate execution on readiness") { @ContentAndMedia { - The import-review helper does not execute anything. It prepares the app-side state so your UI can decide whether to continue. + The import-review helper does not execute anything. It prepares the app-side state so your UI can decide whether to continue. Success looks like gating on `screenState.isReadyToContinue` before calling the confirmed execution handoff. } @Steps { diff --git a/BacktestingKit/BacktestingKit.docc/BKFirstBacktestTutorial.tutorial b/BacktestingKit/BacktestingKit.docc/BKFirstBacktestTutorial.tutorial index b9a1d3b..d0799a4 100644 --- a/BacktestingKit/BacktestingKit.docc/BKFirstBacktestTutorial.tutorial +++ b/BacktestingKit/BacktestingKit.docc/BKFirstBacktestTutorial.tutorial @@ -7,7 +7,7 @@ @ContentAndMedia { The fastest smoke test is ``BKEngine/runDemo(dataset:csv:log:)``. - It uses bundled historical CSV resources, so no API key is required. + It uses bundled historical CSV resources, so no API key is required. Success looks like a `BKRunSummary` that exposes `summary.metrics.totalReturn` and `summary.metrics.maxDrawdown`. } @Steps { @@ -37,8 +37,8 @@ case .success(let summary): print(summary.symbol) print(summary.barCount) - print(summary.result.totalReturn) - print(summary.result.maxDrawdown) + print(summary.metrics.totalReturn) + print(summary.metrics.maxDrawdown) case .failure(let error): print(error.localizedDescription) } diff --git a/BacktestingKit/BacktestingKit.docc/BKHelperWorkflowTutorial.tutorial b/BacktestingKit/BacktestingKit.docc/BKHelperWorkflowTutorial.tutorial index e527aa6..93f3cf3 100644 --- a/BacktestingKit/BacktestingKit.docc/BKHelperWorkflowTutorial.tutorial +++ b/BacktestingKit/BacktestingKit.docc/BKHelperWorkflowTutorial.tutorial @@ -36,7 +36,7 @@ switch result { case .success(let summary): print(summary.symbol) - print(summary.result.totalReturn) + print(summary.metrics.totalReturn) case .failure(let error): print(error.localizedDescription) } diff --git a/BacktestingKit/BacktestingKit.docc/BKInstallAndFirstSuccessTutorial.tutorial b/BacktestingKit/BacktestingKit.docc/BKInstallAndFirstSuccessTutorial.tutorial index b159416..aa03b92 100644 --- a/BacktestingKit/BacktestingKit.docc/BKInstallAndFirstSuccessTutorial.tutorial +++ b/BacktestingKit/BacktestingKit.docc/BKInstallAndFirstSuccessTutorial.tutorial @@ -4,6 +4,10 @@ } @Section(title: "Build the package") { + @ContentAndMedia { + Success looks like a clean local build with no provider wiring or external setup required yet. + } + @Steps { @Step { Run: @@ -16,6 +20,10 @@ } @Section(title: "Run the bundled demo") { + @ContentAndMedia { + Success looks like a `BKRunSummary` result that lets you inspect `summary.metrics.totalReturn`. + } + @Steps { @Step { Import the package: @@ -40,7 +48,7 @@ switch result { case .success(let summary): print(summary.symbol) - print(summary.result.totalReturn) + print(summary.metrics.totalReturn) case .failure(let error): print(error.localizedDescription) } diff --git a/BacktestingKit/BacktestingKit.docc/BKPackageOnboardingTutorial.tutorial b/BacktestingKit/BacktestingKit.docc/BKPackageOnboardingTutorial.tutorial index 6bd33c1..527e5a2 100644 --- a/BacktestingKit/BacktestingKit.docc/BKPackageOnboardingTutorial.tutorial +++ b/BacktestingKit/BacktestingKit.docc/BKPackageOnboardingTutorial.tutorial @@ -1,11 +1,11 @@ @Tutorial(time: 20) { @Intro(title: "Get Started with BacktestingKit") { - Follow a single beginner path from building the package to integrating the canonical engine entrypoints in an app. + Follow a single beginner path from first success to app integration, starting helper-first and only dropping into `BKEngine` when you need direct request control. } @Section(title: "Build and prove the package works") { @ContentAndMedia { - Start with a local build and a bundled demo. This gives you a first success without API keys, network calls, or custom providers. + Start with a local build and a bundled demo. Success looks like a `BKRunSummary` result from bundled data without API keys, network calls, or custom providers. } @Steps { @@ -34,12 +34,26 @@ } ``` } + + @Step { + Inspect the returned summary: + + ```swift + switch result { + case .success(let summary): + print(summary.symbol) + print(summary.metrics.totalReturn) + case .failure(let error): + print(error.localizedDescription) + } + ``` + } } } @Section(title: "Move from bundled data to your own CSV") { @ContentAndMedia { - Once the bundled demo works, inline CSV is the shortest path for tests, previews, onboarding UIs, and local experiments. + Once the bundled demo works, inline CSV is the shortest path for tests, previews, onboarding UIs, and local experiments. Success looks like a preflight-ready CSV and a helper-backed run without building a provider type. } @Steps { @@ -63,20 +77,20 @@ @Section(title: "Graduate into app integration") { @ContentAndMedia { - When you are ready for production-style usage, move to ``BKEngine`` and construct explicit v2 or v3 requests. + Start app integrations with ``BKAppFacade`` for CSV review and handoff, then move to ``BKEngine`` when you need direct request ownership. } @Steps { @Step { - Create a `BKRawCsvProvider` or use a closure-backed provider. + Use ``BKAppFacade/buildCSVImportScreenState(symbol:csv:maxRows:)`` when your app starts from reviewed CSV input. } @Step { - Build a request and run ``BKEngine/runV3(_:)`` or ``BKEngine/runV2(_:)``. + Use ``BKEngine/runV3(_:)`` or ``BKEngine/runV2(_:)`` once the app needs direct request-model control. } @Step { - Continue with the focused tutorials for helpers, manager workflows, and tooling. + Continue with the focused tutorials for CSV import, app integration, first backtests, helpers, manager workflows, and tooling. } } } diff --git a/BacktestingKit/BacktestingKit.docc/BacktestingKit.md b/BacktestingKit/BacktestingKit.docc/BacktestingKit.md index 1a6c835..2510ac7 100644 --- a/BacktestingKit/BacktestingKit.docc/BacktestingKit.md +++ b/BacktestingKit/BacktestingKit.docc/BacktestingKit.md @@ -14,6 +14,8 @@ Use BacktestingKit to run deterministic strategy simulations with: - v2 and v3 simulation compatibility paths - Extensible indicator, strategy, and metrics layers +Start with the tutorials below if you are new to the package. The recommended order is install and first success, onboarding, CSV import, app integration, first backtest, and then the helper, manager, and tooling follow-ons. + ## Tutorials - diff --git a/BacktestingKit/BacktestingKit.docc/BacktestingKitTutorials.tutorial b/BacktestingKit/BacktestingKit.docc/BacktestingKitTutorials.tutorial index 82ae1c8..2d23fa8 100644 --- a/BacktestingKit/BacktestingKit.docc/BacktestingKitTutorials.tutorial +++ b/BacktestingKit/BacktestingKit.docc/BacktestingKitTutorials.tutorial @@ -1,11 +1,11 @@ @Tutorials(name: "BacktestingKit Tutorials") { @Intro(title: "Build with BacktestingKit") { - Start with the beginner onboarding path, then move into the canonical flows for running v3 and v2 simulations, helper workflows, manager recipes, and tooling utilities. + Start with the first-success path, then move through onboarding, CSV import, app integration, and the follow-on helper, engine, manager, and tooling flows. } @Chapter(name: "Onboarding") { - @TutorialReference(tutorial: "doc:BKPackageOnboardingTutorial") @TutorialReference(tutorial: "doc:BKInstallAndFirstSuccessTutorial") + @TutorialReference(tutorial: "doc:BKPackageOnboardingTutorial") @TutorialReference(tutorial: "doc:BKCSVImportTutorial") @TutorialReference(tutorial: "doc:BKAppIntegrationTutorial") } diff --git a/BacktestingKit/Core/BKAdvancedPerformanceMetrics.swift b/BacktestingKit/Core/BKAdvancedPerformanceMetrics.swift index 21157dd..dc89cb4 100644 --- a/BacktestingKit/Core/BKAdvancedPerformanceMetrics.swift +++ b/BacktestingKit/Core/BKAdvancedPerformanceMetrics.swift @@ -2,16 +2,27 @@ import Foundation /// Represents `BKAdvancedPerformanceMetrics` in the BacktestingKit public API. public struct BKAdvancedPerformanceMetrics: Equatable, Codable { + /// MAR ratio associated with this value. public let marRatio: Double + /// Omega ratio associated with this value. public let omegaRatio: Double + /// Skewness associated with this value. public let skewness: Double + /// Kurtosis associated with this value. public let kurtosis: Double + /// Tail ratio associated with this value. public let tailRatio: Double + /// Var95 associated with this value. public let var95: Double + /// Cvar95 associated with this value. public let cvar95: Double + /// Exposure percent associated with this value. public let exposurePercent: Double + /// Turnover approx associated with this value. public let turnoverApprox: Double + /// Average trade duration days associated with this value. public let averageTradeDurationDays: Double + /// Time under water percent associated with this value. public let timeUnderWaterPercent: Double /// Creates a new instance. diff --git a/BacktestingKit/Core/BKExecutionModel.swift b/BacktestingKit/Core/BKExecutionModel.swift index c78bf42..d1ae59f 100644 --- a/BacktestingKit/Core/BKExecutionModel.swift +++ b/BacktestingKit/Core/BKExecutionModel.swift @@ -22,6 +22,7 @@ public struct BKNoSlippageModel: BKSlippageModel { /// Represents `BKFixedBpsSlippageModel` in the BacktestingKit public API. public struct BKFixedBpsSlippageModel: BKSlippageModel { + /// Bps associated with this value. public let bps: Double /// Creates a new instance. @@ -46,7 +47,9 @@ public struct BKNoCommissionModel: BKCommissionModel { /// Represents `BKFixedPlusPercentCommissionModel` in the BacktestingKit public API. public struct BKFixedPlusPercentCommissionModel: BKCommissionModel { + /// Fixed per order associated with this value. public let fixedPerOrder: Double + /// Percent of notional associated with this value. public let percentOfNotional: Double /// Creates a new instance. @@ -63,13 +66,21 @@ public struct BKFixedPlusPercentCommissionModel: BKCommissionModel { /// Represents `BKExecutionAdjustedTrade` in the BacktestingKit public API. public struct BKExecutionAdjustedTrade: Equatable, Codable { + /// Entry date associated with this value. public let entryDate: Date + /// Exit date associated with this value. public let exitDate: Date + /// Entry price associated with this value. public let entryPrice: Double + /// Exit price associated with this value. public let exitPrice: Double + /// Quantity associated with this value. public let quantity: Double + /// Gross pnl associated with this value. public let grossPnl: Double + /// Net pnl associated with this value. public let netPnl: Double + /// Commissions associated with this value. public let commissions: Double /// Creates a new instance. diff --git a/BacktestingKit/Core/BKPositionSizing.swift b/BacktestingKit/Core/BKPositionSizing.swift index 264c413..d01c08e 100644 --- a/BacktestingKit/Core/BKPositionSizing.swift +++ b/BacktestingKit/Core/BKPositionSizing.swift @@ -7,7 +7,9 @@ public protocol BKPositionSizingModel { /// Represents `BKVolatilityTargetingSizer` in the BacktestingKit public API. public struct BKVolatilityTargetingSizer: BKPositionSizingModel { + /// Target annualized volatility associated with this value. public let targetAnnualizedVolatility: Double + /// Maximum gross leverage associated with this value. public let maxGrossLeverage: Double /// Creates a new instance. @@ -27,7 +29,9 @@ public struct BKVolatilityTargetingSizer: BKPositionSizingModel { /// Represents `BKFixedFractionalSizer` in the BacktestingKit public API. public struct BKFixedFractionalSizer: BKPositionSizingModel { + /// Risk fraction associated with this value. public let riskFraction: Double + /// Fallback risk per unit fraction associated with this value. public let fallbackRiskPerUnitFraction: Double /// Creates a new instance. @@ -47,8 +51,11 @@ public struct BKFixedFractionalSizer: BKPositionSizingModel { /// Represents `BKKellyCappedSizer` in the BacktestingKit public API. public struct BKKellyCappedSizer: BKPositionSizingModel { + /// Expected return associated with this value. public let expectedReturn: Double + /// Return variance associated with this value. public let returnVariance: Double + /// Maximum weight associated with this value. public let maxWeight: Double /// Creates a new instance. diff --git a/BacktestingKit/Core/BKTypes.swift b/BacktestingKit/Core/BKTypes.swift index f3d5f90..d286001 100644 --- a/BacktestingKit/Core/BKTypes.swift +++ b/BacktestingKit/Core/BKTypes.swift @@ -1,58 +1,96 @@ import Foundation +/// User profile aggregate containing tracked devices and instruments. public struct User: Codable, Equatable { + /// Devices associated with this value. public var devices: [Device] + /// Stable identifier for this value. public var id: String + /// Instruments associated with this value. public var instruments: [Instrument] + /// Identifier associated with this value. public var user_id: String + /// Tier associated with this value. public var tier: BKEntitlement + /// Last updated associated with this value. public var last_updated: Double } /// Represents `Device` in the BacktestingKit public API. public struct Device: Codable, Equatable { + /// Identifier associated with this value. public var device_id: String + /// Device type associated with this value. public var device_type: String + /// Stable identifier for this value. public var id: String + /// Last updated associated with this value. public var last_updated: Double + /// Identifier associated with this value. public var user_id: String } /// Represents `Instrument` in the BacktestingKit public API. public struct Instrument: Codable, Equatable { + /// Pin associated with this value. public var pin: Bool + /// Exch associated with this value. public var exch: String + /// Exch disp associated with this value. public var exchDisp: String + /// Name associated with this value. public var name: String + /// Type associated with this value. public var type: String + /// Type disp associated with this value. public var typeDisp: String + /// Configuration associated with this value. public var config_history: [Config] + /// Optimize history associated with this value. public var optimize_history: [OptimizeResult] + /// Stable identifier for this value. public var id: String + /// Instrument associated with this value. public var instrument: String + /// Last updated associated with this value. public var last_updated: Double } /// Represents `Config` in the BacktestingKit public API. public struct Config: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Transactions associated with this value. public var transactions: [BKTrade] + /// Active associated with this value. public var active: Bool + /// Created associated with this value. public var created: Double + /// Instrument associated with this value. public var instrument: String + /// Last updated associated with this value. public var last_updated: Double + /// Configuration associated with this value. public var policyConfig: SimulationPolicyConfig + /// Analysis associated with this value. public var analysis: BKAnalysis + /// Current status associated with this value. public var status: SimulationStatus } /// Represents `OptimizeResult` in the BacktestingKit public API. public struct OptimizeResult: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Result associated with this value. public var result: [Config] + /// Configuration associated with this value. public var policyConfig: OptimizePolicyConfig + /// Last updated associated with this value. public var last_updated: Double + /// Created associated with this value. public var created: Double + /// Current status associated with this value. public var status: SimulationStatus } @@ -64,12 +102,19 @@ public enum TriggerType: String, Codable { /// Represents `DynamodbTrigger` in the BacktestingKit public API. public struct DynamodbTrigger: Codable, Equatable { + /// Stable identifier for this value. public var uuid: String + /// Type associated with this value. public var type: TriggerType + /// Item ID associated with this value. public var itemId: String + /// Optimize associated with this value. public var optimize: [OptimizePolicyConfig] + /// Simulate associated with this value. public var simulate: [SimulationPolicyConfig] + /// Instrument associated with this value. public var instrument: String + /// Identifier associated with this value. public var user_id: String } diff --git a/BacktestingKit/Core/BKTypesSimulation.swift b/BacktestingKit/Core/BKTypesSimulation.swift index c7cbdfb..29cfe5d 100644 --- a/BacktestingKit/Core/BKTypesSimulation.swift +++ b/BacktestingKit/Core/BKTypesSimulation.swift @@ -1,30 +1,56 @@ import Foundation +/// Summary statistics produced by a completed backtest run. public struct BKAnalysis: Codable, Equatable { + /// Backtest maximum down draw associated with this value. public var BKMaxDownDraw: Double + /// Backtest maximum down draw percentage associated with this value. public var BKMaxDownDrawPct: Double + /// Starting capital associated with this value. public var startingCapital: Double + /// Final capital associated with this value. public var finalCapital: Double + /// Profit associated with this value. public var profit: Double + /// Profit percentage associated with this value. public var profitPct: Double + /// Growth associated with this value. public var growth: Double + /// Total trades represented by this value. public var totalTrades: Int + /// Number of bars represented by this value. public var barCount: Int + /// Maximum drawdown associated with this value. public var maxDrawdown: Double + /// Maximum drawdown percentage associated with this value. public var maxDrawdownPct: Double + /// Maximum risk percentage associated with this value. public var maxRiskPct: Double? + /// Expectency associated with this value. public var expectency: Double + /// Rmultiple standard deviation associated with this value. public var rmultipleStdDev: Double + /// System quality associated with this value. public var systemQuality: Double? + /// Profit factor associated with this value. public var profitFactor: Double? + /// Proportion profitable associated with this value. public var proportionProfitable: Double + /// Percent profitable associated with this value. public var percentProfitable: Double + /// Return on account represented by this value. public var returnOnAccount: Double + /// Average profit per trade associated with this value. public var averageProfitPerTrade: Double + /// Number winning trades represented by this value. public var numWinningTrades: Int + /// Number losing trades represented by this value. public var numLosingTrades: Int + /// Average winning trade associated with this value. public var averageWinningTrade: Double + /// Average losing trade associated with this value. public var averageLosingTrade: Double + /// Expected value associated with this value. public var expectedValue: Double /// Creates a new instance. @@ -85,7 +111,9 @@ public struct BKAnalysis: Codable, Equatable { /// Represents `BKTimestampedValue` in the BacktestingKit public API. public struct BKTimestampedValue: Codable, Equatable { + /// Timestamp associated with this value. public var time: Date + /// Value associated with this value. public var value: Double /// Creates a new instance. @@ -97,21 +125,37 @@ public struct BKTimestampedValue: Codable, Equatable { /// Represents `BKTrade` in the BacktestingKit public API. public struct BKTrade: Codable, Equatable { + /// Direction associated with this value. public var direction: TradeDirection + /// Entry time associated with this value. public var entryTime: Date + /// Entry price associated with this value. public var entryPrice: Double + /// Exit time associated with this value. public var exitTime: Date + /// Exit price associated with this value. public var exitPrice: Double + /// Profit associated with this value. public var profit: Double + /// Profit percentage associated with this value. public var profitPct: Double + /// Growth associated with this value. public var growth: Double + /// Risk percentage associated with this value. public var riskPct: Double + /// Rmultiple associated with this value. public var rmultiple: Double + /// Risk series associated with this value. public var riskSeries: [BKTimestampedValue] + /// Holding period associated with this value. public var holdingPeriod: Int + /// Exit reason associated with this value. public var exitReason: String + /// Stop price associated with this value. public var stopPrice: Double + /// Stop price series associated with this value. public var stopPriceSeries: [BKTimestampedValue] + /// Profit target associated with this value. public var profitTarget: Double /// Creates a new instance. @@ -154,12 +198,19 @@ public struct BKTrade: Codable, Equatable { /// Represents `SimulationRule` in the BacktestingKit public API. public struct SimulationRule: Codable, Equatable { + /// Indicator one name associated with this value. public var indicatorOneName: String + /// Indicator one type associated with this value. public var indicatorOneType: TechnicalIndicators + /// Indicator one figure associated with this value. public var indicatorOneFigure: [Double] + /// Compare associated with this value. public var compare: CompareOption + /// Indicator two name associated with this value. public var indicatorTwoName: String + /// Indicator two type associated with this value. public var indicatorTwoType: TechnicalIndicators + /// Indicator two figure associated with this value. public var indicatorTwoFigure: [Double] /// Creates a new instance. @@ -184,13 +235,21 @@ public struct SimulationRule: Codable, Equatable { /// Represents `SimulationPolicyConfig` in the BacktestingKit public API. public struct SimulationPolicyConfig: Codable, Equatable { + /// Policy associated with this value. public var policy: SimulationPolicy + /// Trailing stop loss associated with this value. public var trailingStopLoss: Bool + /// Stop loss figure associated with this value. public var stopLossFigure: Double + /// Profit factor associated with this value. public var profitFactor: Double + /// Rules associated with this value. public var entryRules: [SimulationRule] + /// Rules associated with this value. public var exitRules: [SimulationRule] + /// T1 associated with this value. public var t1: Double + /// T2 associated with this value. public var t2: Double /// Creates a new instance. @@ -217,14 +276,23 @@ public struct SimulationPolicyConfig: Codable, Equatable { /// Represents `OptimizePolicyConfig` in the BacktestingKit public API. public struct OptimizePolicyConfig: Codable, Equatable { + /// Step size associated with this value. public var stepSize: Double + /// Simple policy associated with this value. public var simplePolicy: Bool + /// Trailing stop loss associated with this value. public var trailingStopLoss: Bool + /// Stop loss figure associated with this value. public var stopLossFigure: Double + /// Profit factor associated with this value. public var profitFactor: Double + /// Rules associated with this value. public var entryRules: [OptimizeRule] + /// Rules associated with this value. public var exitRules: [OptimizeRule] + /// T1 associated with this value. public var t1: Double + /// T2 associated with this value. public var t2: Double /// Creates a new instance. @@ -253,14 +321,23 @@ public struct OptimizePolicyConfig: Codable, Equatable { /// Represents `OptimizeRule` in the BacktestingKit public API. public struct OptimizeRule: Codable, Equatable { + /// Indicator one name associated with this value. public var indicatorOneName: String + /// Indicator one type associated with this value. public var indicatorOneType: TechnicalIndicators + /// Indicator one figure lower associated with this value. public var indicatorOneFigureLower: [Double] + /// Indicator one figure upper associated with this value. public var indicatorOneFigureUpper: [Double] + /// Compare associated with this value. public var compare: CompareOption + /// Indicator two name associated with this value. public var indicatorTwoName: String + /// Indicator two type associated with this value. public var indicatorTwoType: TechnicalIndicators + /// Indicator two figure lower associated with this value. public var indicatorTwoFigureLower: [Double] + /// Indicator two figure upper associated with this value. public var indicatorTwoFigureUpper: [Double] /// Creates a new instance. @@ -289,6 +366,7 @@ public struct OptimizeRule: Codable, Equatable { /// Represents `OptimizeRulesContainer` in the BacktestingKit public API. public struct OptimizeRulesContainer: Codable, Equatable { + /// Rules associated with this value. public var rules: [String: [SimulationRule]] /// Creates a new instance. diff --git a/BacktestingKit/Core/BacktestMetricsReport.swift b/BacktestingKit/Core/BacktestMetricsReport.swift index 0a90422..715d28f 100644 --- a/BacktestingKit/Core/BacktestMetricsReport.swift +++ b/BacktestingKit/Core/BacktestMetricsReport.swift @@ -2,15 +2,25 @@ import Foundation /// Represents `BacktestMetricsReport` in the BacktestingKit public API. public struct BacktestMetricsReport { + /// Result associated with this value. public let result: BacktestResult + /// Total compounded return represented by this value. public let totalCompoundedReturn: Double + /// Maximum drawdown percent associated with this value. public let maxDrawdownPercent: Double + /// Average drawdown percent associated with this value. public let averageDrawdownPercent: Double + /// Downside deviation associated with this value. public let downsideDeviation: Double + /// Payoff ratio associated with this value. public let payoffRatio: Double + /// Recovery factor associated with this value. public let recoveryFactor: Double + /// Trade returns associated with this value. public let tradeReturns: [Double] + /// Additive equity curve associated with this value. public let additiveEquityCurve: [Double] + /// Compounded equity curve associated with this value. public let compoundedEquityCurve: [Double] /// Creates a new instance. diff --git a/BacktestingKit/Core/Candlestick.swift b/BacktestingKit/Core/Candlestick.swift index 3b5d182..4a27208 100644 --- a/BacktestingKit/Core/Candlestick.swift +++ b/BacktestingKit/Core/Candlestick.swift @@ -5,13 +5,21 @@ import Foundation /// Represents `Candlestick` in the BacktestingKit public API. public struct Candlestick: Equatable, Codable, Sendable { + /// Date associated with this value. public let date: Date + /// Open price for the bar. public let open: Double + /// High price for the bar. public let high: Double + /// Low price for the bar. public let low: Double + /// Close price for the bar. public let close: Double + /// Adjusted close price for the bar when available. public let adjustedClose: Double? + /// Trading volume for the bar. public let volume: Double + /// Technical indicator values keyed by indicator name. public var technicalIndicators: [String: Double] /// Creates a new instance. @@ -35,6 +43,12 @@ public struct Candlestick: Equatable, Codable, Sendable { self.technicalIndicators = technicalIndicators } + /// Parses a simple OHLCV CSV payload into candlesticks. + /// + /// - Parameters: + /// - csv: CSV text whose first row is the header. + /// - dateFormat: `DateFormatter` pattern used to decode the first column. + /// - Returns: Parsed candlesticks. Rows that fail validation are skipped. public static func fromCSV(_ csv: String, dateFormat: String = "yyyy-MM-dd") -> [Candlestick] { var candlesticks: [Candlestick] = [] let lines = csv.split(separator: "\n").map { String($0) } diff --git a/BacktestingKit/Core/Trade.swift b/BacktestingKit/Core/Trade.swift index b4c2c42..f5f0141 100644 --- a/BacktestingKit/Core/Trade.swift +++ b/BacktestingKit/Core/Trade.swift @@ -7,10 +7,15 @@ import Foundation public struct Trade { /// Represents `TradeType` in the BacktestingKit public API. public enum TradeType { case buy, sell } + /// Type associated with this value. public let type: TradeType + /// Entry date associated with this value. public let entryDate: Date + /// Entry price associated with this value. public let entryPrice: Double + /// Exit date associated with this value. public let exitDate: Date? + /// Exit price associated with this value. public let exitPrice: Double? /// Creates a new instance. diff --git a/BacktestingKit/Data/BKAlphaVantageClient.swift b/BacktestingKit/Data/BKAlphaVantageClient.swift index 2ea0b9e..aced11a 100644 --- a/BacktestingKit/Data/BKAlphaVantageClient.swift +++ b/BacktestingKit/Data/BKAlphaVantageClient.swift @@ -1,9 +1,14 @@ import Foundation +/// `BKRawCsvProvider` implementation backed by the Alpha Vantage HTTP API. public struct AlphaVantageClient: BKRawCsvProvider { + /// API key associated with this value. public var apiKey: String + /// Session associated with this value. public var session: URLSession + /// Retry policy associated with this value. public var retryPolicy: AlphaVantageRetryPolicy + /// Rate limiter associated with this value. public var rateLimiter: BKRequestRateLimiter? /// Creates a new instance. diff --git a/BacktestingKit/Data/BKCSVParserFunctions.swift b/BacktestingKit/Data/BKCSVParserFunctions.swift index 31f8806..8e2798f 100644 --- a/BacktestingKit/Data/BKCSVParserFunctions.swift +++ b/BacktestingKit/Data/BKCSVParserFunctions.swift @@ -1,5 +1,13 @@ import Foundation +/// Parses a CSV payload into chronological `BKBar` values using the package defaults. +/// +/// - Parameters: +/// - csv: Raw CSV payload to decode. +/// - dateFormat: Reserved for compatibility with older call sites. ISO-8601 parsing is used. +/// - reverse: When `true`, returns bars in reverse chronological order for legacy workflows. +/// - columnMapping: Optional column-name overrides for non-standard CSV headers. +/// - Returns: Parsed bars or a `BKCSVParsingError` describing the first failure encountered. public func csvToBars( _ csv: String, dateFormat: String = "yyyy-MM-dd", diff --git a/BacktestingKit/Data/BKCSVParsingSupport.swift b/BacktestingKit/Data/BKCSVParsingSupport.swift index 3b83881..0ee5889 100644 --- a/BacktestingKit/Data/BKCSVParsingSupport.swift +++ b/BacktestingKit/Data/BKCSVParsingSupport.swift @@ -54,6 +54,7 @@ public enum AlphaVantageClientError: LocalizedError, Equatable { case apiError(String) case emptyResponse + /// Localized description of the error. public var errorDescription: String? { switch self { case .invalidTicker: @@ -86,6 +87,7 @@ public enum BKCSVParsingError: LocalizedError, Equatable { case invalidNumeric(value: String, line: Int) case nonChronologicalDate(previous: String, current: String, line: Int) + /// Localized description of the error. public var errorDescription: String? { switch self { case .missingHeader: @@ -108,12 +110,19 @@ public enum BKCSVParsingError: LocalizedError, Equatable { /// Represents `BKCSVColumnMapping` in the BacktestingKit public API. public struct BKCSVColumnMapping: Equatable, Codable, Sendable { + /// Date associated with this value. public var date: String + /// Open price for the bar. public var open: String + /// High price for the bar. public var high: String + /// Low price for the bar. public var low: String + /// Close price for the bar. public var close: String + /// Adjusted close price for the bar when available. public var adjustedClose: String? + /// Trading volume for the bar. public var volume: String /// Creates a new instance. diff --git a/BacktestingKit/Data/BKCacheMetrics.swift b/BacktestingKit/Data/BKCacheMetrics.swift index 56dddc7..d31bfd2 100644 --- a/BacktestingKit/Data/BKCacheMetrics.swift +++ b/BacktestingKit/Data/BKCacheMetrics.swift @@ -2,7 +2,9 @@ import Foundation /// Represents `BKCacheMetricsSnapshot` in the BacktestingKit public API. public struct BKCacheMetricsSnapshot: Equatable, Codable { + /// Stats associated with this value. public let stats: BKCsvCacheStats + /// Timestamp associated with this value. public let timestamp: Date /// Creates a new instance. @@ -18,6 +20,7 @@ public protocol BKCacheMetricsReporting { func streamUpdates() -> AsyncStream } +/// Observes a cached CSV provider and exposes point-in-time or streaming cache metrics. public final class BKCacheMetricsReporter: BKCacheMetricsReporting { private let provider: BKCachedCsvProvider private let lock = NSLock() diff --git a/BacktestingKit/Data/BKCacheMetricsHistory.swift b/BacktestingKit/Data/BKCacheMetricsHistory.swift index ee25e54..a4e81a6 100644 --- a/BacktestingKit/Data/BKCacheMetricsHistory.swift +++ b/BacktestingKit/Data/BKCacheMetricsHistory.swift @@ -7,6 +7,7 @@ public protocol BKCacheMetricsSnapshotStoring { func appendSnapshot(_ snapshot: BKCacheMetricsSnapshot) } +/// Ring-buffer store for retaining and exporting recent cache metrics snapshots. public final class BKCacheMetricsHistory: BKCacheMetricsSnapshotStoring { private let capacity: Int private var snapshots: [BKCacheMetricsSnapshot] diff --git a/BacktestingKit/Data/BKCsvCacheProvider.swift b/BacktestingKit/Data/BKCsvCacheProvider.swift index dad1d3d..45a9ccc 100644 --- a/BacktestingKit/Data/BKCsvCacheProvider.swift +++ b/BacktestingKit/Data/BKCsvCacheProvider.swift @@ -1,7 +1,10 @@ import Foundation +/// Configuration controlling the size and retention policy of the in-memory CSV cache. public struct BKCsvCacheConfiguration: Equatable, Codable { + /// Maximum entries associated with this value. public var maxEntries: Int + /// Time to live seconds associated with this value. public var timeToLiveSeconds: TimeInterval /// Creates a new instance. @@ -13,10 +16,14 @@ public struct BKCsvCacheConfiguration: Equatable, Codable { /// Represents `BKCsvCacheStats` in the BacktestingKit public API. public struct BKCsvCacheStats: Equatable, Codable { + /// Number of cache hits recorded. public var hits: Int + /// Number of cache misses recorded. public var misses: Int + /// Number of cache entries currently stored. public var entries: Int + /// Cache hit rate derived from hits and misses. public var hitRate: Double { let total = hits + misses guard total > 0 else { return 0 } @@ -116,6 +123,7 @@ actor BKInMemoryCsvCache { } } +/// `BKRawCsvProvider` decorator that caches fetched CSV payloads in memory. public final class BKCachedCsvProvider: BKRawCsvProvider { private let wrapped: BKRawCsvProvider let cache: BKInMemoryCsvCache @@ -123,8 +131,10 @@ public final class BKCachedCsvProvider: BKRawCsvProvider { private var observerID: UUID? private var lastMetrics: BKCsvCacheStats private let metricsLock = NSLock() + /// Snapshot store used to retain cache metrics history. public let metricsHistory: any BKCacheMetricsSnapshotStoring + /// Headline metrics associated with this value. public var metrics: BKCsvCacheStats { metricsLock.lock() defer { metricsLock.unlock() } diff --git a/BacktestingKit/Data/BKDataProviders.swift b/BacktestingKit/Data/BKDataProviders.swift index 35a47e8..82ab0c4 100644 --- a/BacktestingKit/Data/BKDataProviders.swift +++ b/BacktestingKit/Data/BKDataProviders.swift @@ -1,7 +1,10 @@ import Foundation +/// Retry policy applied to Alpha Vantage requests before surfacing a provider failure. public struct AlphaVantageRetryPolicy: Equatable, Sendable { + /// Maximum attempts associated with this value. public var maxAttempts: Int + /// Initial backoff seconds associated with this value. public var initialBackoffSeconds: Double /// Creates a new instance. @@ -59,6 +62,7 @@ public protocol BKRawCsvProvider { /// Inline CSV provider for app-facing helper workflows and tests. public struct BKInlineCsvProvider: BKRawCsvProvider, Sendable { + /// CSV associated with this value. public let csv: String /// Creates a new instance. diff --git a/BacktestingKit/Engine/BKEngine.swift b/BacktestingKit/Engine/BKEngine.swift index d92423c..e066e78 100644 --- a/BacktestingKit/Engine/BKEngine.swift +++ b/BacktestingKit/Engine/BKEngine.swift @@ -7,25 +7,32 @@ public enum BKEngine { public typealias V2Request = BKEngineOneLiner.BKV2Request /// Provides the `V3Request` typealias for BacktestingKit interoperability. public typealias V3Request = BKEngineOneLiner.BKV3Request + /// Provides the `PortfolioRequest` typealias for additive portfolio orchestration. + public typealias PortfolioRequest = BKPortfolioRequest + /// Factory used to build the v3 simulation driver behind `runV3`. public static var makeV3Driver: @Sendable (_ dataStore: BKV3DataStore, _ csvProvider: BKRawCsvProvider) -> any BKV3SimulationDriving { get { BKEngineOneLiner.makeV3Driver } set { BKEngineOneLiner.makeV3Driver = newValue } } + /// Factory used to build the v2 simulation driver behind `runV2`. public static var makeV2Driver: @Sendable (_ csvProvider: BKRawCsvProvider) -> any BKV2SimulationDriving { get { BKEngineOneLiner.makeV2Driver } set { BKEngineOneLiner.makeV2Driver = newValue } } + /// Runs a v3 request through the configured simulation driver. public static func runV3(_ request: V3Request) async -> Result { await BKEngineOneLiner.runBKV3(request) } + /// Runs a v2 request through the configured simulation driver. public static func runV2(_ request: V2Request) async -> Result<(BKV2.SimulateConfigOutput, PositionStatus), BKEngineFailure> { await BKEngineOneLiner.runBKV2(request) } + /// Executes the package's bundled SMA crossover demo workflow. public static func runDemo( dataset: BKQuickDemoDataset = .aapl, csv: String? = nil, diff --git a/BacktestingKit/Engine/BKEngineComponents.swift b/BacktestingKit/Engine/BKEngineComponents.swift index ed7a116..ccf1253 100644 --- a/BacktestingKit/Engine/BKEngineComponents.swift +++ b/BacktestingKit/Engine/BKEngineComponents.swift @@ -27,6 +27,7 @@ public struct BKDefaultSimulationDriverFactory: BKSimulationDriverFactory { /// Single source of truth for engine components. public struct BKEngineComponentGraph: Sendable { + /// Simulation factory associated with this value. public var simulationFactory: any BKSimulationDriverFactory /// Creates a new instance. diff --git a/BacktestingKit/Engine/BKEngineHelperModels.swift b/BacktestingKit/Engine/BKEngineHelperModels.swift index 5d8914d..ba3f26a 100644 --- a/BacktestingKit/Engine/BKEngineHelperModels.swift +++ b/BacktestingKit/Engine/BKEngineHelperModels.swift @@ -2,12 +2,19 @@ import Foundation /// Presentation-friendly headline metrics derived from a backtest result. public struct BKRunHeadlineMetrics: Codable, Equatable, Sendable { + /// Number of trades represented by this value. public var tradeCount: Int + /// Win rate associated with this value. public var winRate: Double + /// Total return represented by this value. public var totalReturn: Double + /// Annualized return associated with this value. public var annualizedReturn: Double + /// Maximum drawdown associated with this value. public var maxDrawdown: Double + /// Sharpe ratio associated with this value. public var sharpeRatio: Double + /// Profit factor associated with this value. public var profitFactor: Double /// Creates a new instance. @@ -73,10 +80,15 @@ public struct BKRunHeadlineMetrics: Codable, Equatable, Sendable { /// Compact run summary suitable for onboarding flows and smoke-test output. public struct BKRunSummary: Codable, Equatable, Sendable { + /// Ticker symbol associated with this value. public var symbol: String + /// Number of bars represented by this value. public var barCount: Int + /// Start date represented by this value. public var startDate: Date? + /// End date represented by this value. public var endDate: Date? + /// Headline metrics associated with this value. public var metrics: BKRunHeadlineMetrics /// Creates a new instance. @@ -97,11 +109,17 @@ public struct BKRunSummary: Codable, Equatable, Sendable { /// Preflight-aware preset run result for inline CSV onboarding workflows. public struct BKPreflightedRunSummary { + /// Ticker symbol associated with this value. public var symbol: String + /// Preset associated with this value. public var preset: BKPresetCatalog + /// Preflight validation output associated with this value. public var preflight: BKToolPreflightReport + /// High-level summary associated with this value. public var summary: BKRunSummary? + /// Typed failure associated with this value when execution does not succeed. public var failure: BKEngineFailure? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. @@ -124,12 +142,19 @@ public struct BKPreflightedRunSummary { /// Structured validation + execution result for v2 inline CSV workflows. public struct BKV2ValidatedRunReport { + /// Identifier associated with this value. public var instrumentID: String + /// Preflight validation output associated with this value. public var preflight: BKToolPreflightReport + /// Request validation associated with this value. public var requestValidation: BKValidationReport + /// Output associated with this value. public var output: BKV2.SimulateConfigOutput? + /// Position status associated with this value. public var positionStatus: PositionStatus? + /// Typed failure associated with this value when execution does not succeed. public var failure: BKEngineFailure? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. @@ -154,11 +179,17 @@ public struct BKV2ValidatedRunReport { /// Structured validation + execution result for v3 inline CSV workflows. public struct BKV3ValidatedRunReport { + /// Identifier associated with this value. public var instrumentID: String + /// Preflight validation output associated with this value. public var preflight: BKToolPreflightReport + /// Request validation associated with this value. public var requestValidation: BKValidationReport + /// Detailed report associated with this value. public var report: BKSimulationInstrumentReport? + /// Typed failure associated with this value when execution does not succeed. public var failure: BKEngineFailure? + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. diff --git a/BacktestingKit/Engine/BKEngineOneLiner.swift b/BacktestingKit/Engine/BKEngineOneLiner.swift index f54a1a8..ef58a03 100644 --- a/BacktestingKit/Engine/BKEngineOneLiner.swift +++ b/BacktestingKit/Engine/BKEngineOneLiner.swift @@ -2,27 +2,49 @@ import Foundation /// Represents `BKEngineOneLiner` in the BacktestingKit public API. public enum BKEngineOneLiner { + /// Shared component graph used by the one-liner engine entrypoints. public static var components = BKEngineComponentGraph() + /// Factory closure used to build v3 drivers for one-liner execution. public static var makeV3Driver: @Sendable (_ dataStore: BKV3DataStore, _ csvProvider: BKRawCsvProvider) -> any BKV3SimulationDriving = { dataStore, csvProvider in components.simulationFactory.makeV3Driver(dataStore: dataStore, csvProvider: csvProvider) } + /// Factory closure used to build v2 drivers for one-liner execution. public static var makeV2Driver: @Sendable (_ csvProvider: BKRawCsvProvider) -> any BKV2SimulationDriving = { csvProvider in components.simulationFactory.makeV2Driver(csvProvider: csvProvider) } /// Represents `BKV3Request` in the BacktestingKit public API. public struct BKV3Request { + /// Instrument associated with this value. public var instrument: BKV3_InstrumentInfo + /// P1 associated with this value. public var p1: Double + /// P2 associated with this value. public var p2: Double + /// Date format string used while parsing or formatting input. public var dateFormat: String + /// Execution options associated with this value. public var executionOptions: BKSimulationExecutionOptions + /// Data store associated with this value. public var dataStore: BKV3DataStore + /// CSV provider associated with this value. public var csvProvider: BKRawCsvProvider + /// Log associated with this value. public var log: (@Sendable (String) -> Void)? + /// Creates a request object for a single v3 simulation run. + /// + /// - Parameters: + /// - instrument: Instrument metadata and configuration lookup key. + /// - p1: Lower bound used by provider-backed date window selection. + /// - p2: Upper bound used by provider-backed date window selection. + /// - dateFormat: Date format hint forwarded to the simulation driver. + /// - executionOptions: Driver options controlling parsing and run behavior. + /// - dataStore: v3 persistence layer used to load configs and save results. + /// - csvProvider: Raw CSV provider used to fetch market data for the run. + /// - log: Optional log sink for lifecycle messages. public init( instrument: BKV3_InstrumentInfo, p1: Double = 5.0, @@ -46,15 +68,34 @@ public enum BKEngineOneLiner { /// Represents `BKV2Request` in the BacktestingKit public API. public struct BKV2Request { + /// Identifier associated with this value. public var instrumentID: String + /// Configuration associated with this value. public var config: BKV2.SimulationPolicyConfig + /// P1 associated with this value. public var p1: Double + /// P2 associated with this value. public var p2: Double + /// Date format string used while parsing or formatting input. public var dateFormat: String + /// CSV column mapping associated with this value. public var csvColumnMapping: BKCSVColumnMapping? + /// CSV provider associated with this value. public var csvProvider: BKRawCsvProvider + /// Log associated with this value. public var log: (@Sendable (String) -> Void)? + /// Creates a request object for a single v2 simulation run. + /// + /// - Parameters: + /// - instrumentID: Identifier or ticker to simulate. + /// - config: v2 policy configuration passed to the simulation engine. + /// - p1: Lower bound used by provider-backed date window selection. + /// - p2: Upper bound used by provider-backed date window selection. + /// - dateFormat: Date format hint forwarded to the simulation driver. + /// - csvColumnMapping: Optional CSV header overrides for custom datasets. + /// - csvProvider: Raw CSV provider used to fetch market data for the run. + /// - log: Optional log sink for lifecycle messages. public init( instrumentID: String, config: BKV2.SimulationPolicyConfig, @@ -76,6 +117,7 @@ public enum BKEngineOneLiner { } } + /// Runs a v3 request and returns the instrument-level simulation report. public static func runBKV3(_ request: BKV3Request) async -> Result { request.log?("[OneLiner][V3] Start instrument=\(request.instrument.id)") let driver = makeV3Driver(request.dataStore, request.csvProvider) @@ -96,6 +138,7 @@ public enum BKEngineOneLiner { } } + /// Runs a v2 request and returns the raw simulation output and ending position status. public static func runBKV2(_ request: BKV2Request) async -> Result<(BKV2.SimulateConfigOutput, PositionStatus), BKEngineFailure> { request.log?("[OneLiner][V2] Start instrument=\(request.instrumentID)") let driver = makeV2Driver(request.csvProvider) @@ -117,18 +160,22 @@ public enum BKEngineOneLiner { } } + /// Convenience alias for `runBKV3(_:)`. public static func runV3(_ request: BKV3Request) async -> Result { await runBKV3(request) } + /// Convenience alias for `runBKV2(_:)`. public static func runV2(_ request: BKV2Request) async -> Result<(BKV2.SimulateConfigOutput, PositionStatus), BKEngineFailure> { await runBKV2(request) } + /// Backward-compatible alias for `runBKV3(_:)`. public static func runATV3(_ request: BKV3Request) async -> Result { await runBKV3(request) } + /// Backward-compatible alias for `runBKV2(_:)`. public static func runATV2(_ request: BKV2Request) async -> Result<(BKV2.SimulateConfigOutput, PositionStatus), BKEngineFailure> { await runBKV2(request) } diff --git a/BacktestingKit/Engine/BKEngineRuleModels.swift b/BacktestingKit/Engine/BKEngineRuleModels.swift index 615eeaa..e1a1396 100644 --- a/BacktestingKit/Engine/BKEngineRuleModels.swift +++ b/BacktestingKit/Engine/BKEngineRuleModels.swift @@ -4,16 +4,19 @@ import Foundation public enum TechnicalIndicator: Hashable, Identifiable, Equatable, Codable, CustomStringConvertible, CaseIterable { case sma(period: Int) + /// All supported cases for this type. public static var allCases: [TechnicalIndicator] { // Only representative cases without associated values // (users can extend as needed) [.sma(period: 0)] } + /// Stable identifier for this value. public var id: Int { switch self { case .sma(let period): return period.hashValue ^ 0x1000 } } + /// Human-readable description for this value. public var description: String { switch self { case .sma(let period): return "SMA(\(period))" @@ -27,9 +30,11 @@ public enum StrategyOperand: Hashable, Identifiable, Equatable, Codable, CustomS case price(TechnicalIndicatorValueProvider.PriceType) case constant(Double) + /// All supported cases for this type. public static var allCases: [StrategyOperand] { [.indicator(.sma(period: 0)), .price(.open), .constant(0)] } + /// Stable identifier for this value. public var id: Int { switch self { case .indicator(let ti): return ti.id ^ 0x2000 @@ -37,6 +42,7 @@ public enum StrategyOperand: Hashable, Identifiable, Equatable, Codable, CustomS case .constant(let d): return Int(truncatingIfNeeded: d.bitPattern) } } + /// Human-readable description for this value. public var description: String { switch self { case .indicator(let ti): return "Indicator(\(ti))" @@ -52,9 +58,11 @@ public enum StrategyOperator: Hashable, Identifiable, Equatable, Codable, Custom case lessThan case greaterThanOrEqual case lessThanOrEqual + /// All supported cases for this type. public static var allCases: [StrategyOperator] { [.greaterThan, .lessThan, .greaterThanOrEqual, .lessThanOrEqual] } + /// Stable identifier for this value. public var id: Int { switch self { case .greaterThan: return 0 @@ -63,6 +71,7 @@ public enum StrategyOperator: Hashable, Identifiable, Equatable, Codable, Custom case .lessThanOrEqual: return 3 } } + /// Human-readable description for this value. public var description: String { switch self { case .greaterThan: return ">" @@ -75,8 +84,11 @@ public enum StrategyOperator: Hashable, Identifiable, Equatable, Codable, Custom // Nested enum extension TechnicalIndicatorValueProvider.PriceType: Hashable, Identifiable, Equatable, Codable, CustomStringConvertible, CaseIterable { + /// All supported cases for this type. public static var allCases: [Self] { [.open, .high, .low, .close, .volume] } + /// Stable identifier for this value. public var id: Int { self.hashValue } + /// Human-readable description for this value. public var description: String { switch self { case .open: return "Open" @@ -90,8 +102,11 @@ extension TechnicalIndicatorValueProvider.PriceType: Hashable, Identifiable, Equ /// Represents `StrategyCondition` in the BacktestingKit public API. public struct StrategyCondition: Hashable, Identifiable, Equatable, Codable { + /// Lhs associated with this value. public let lhs: StrategyOperand + /// Op associated with this value. public let op: StrategyOperator + /// Rhs associated with this value. public let rhs: StrategyOperand /// Creates a new instance. public init(_ lhs: StrategyOperand, _ op: StrategyOperator, _ rhs: StrategyOperand) { @@ -99,13 +114,17 @@ public struct StrategyCondition: Hashable, Identifiable, Equatable, Codable { self.op = op self.rhs = rhs } + /// Stable identifier for this value. public var id: Int { lhs.id ^ op.id ^ rhs.id } } /// Represents `TechnicalIndicatorValueProvider` in the BacktestingKit public API. public struct TechnicalIndicatorValueProvider: Identifiable, Equatable, Codable { + /// Index associated with this value. public let index: Int + /// Candles associated with this value. public let candles: [Candlestick] + /// Indicator name map associated with this value. public let indicatorNameMap: [TechnicalIndicator: String] /// Executes `value`. public func value(for indicator: TechnicalIndicator) -> Double? { @@ -127,5 +146,6 @@ public struct TechnicalIndicatorValueProvider: Identifiable, Equatable, Codable } /// Represents `PriceType` in the BacktestingKit public API. public enum PriceType { case open, high, low, close, volume } + /// Stable identifier for this value. public var id: Int { index } } diff --git a/BacktestingKit/Engine/BKManagerHelperModels.swift b/BacktestingKit/Engine/BKManagerHelperModels.swift index 48bd885..bd7217a 100644 --- a/BacktestingKit/Engine/BKManagerHelperModels.swift +++ b/BacktestingKit/Engine/BKManagerHelperModels.swift @@ -18,7 +18,9 @@ public enum BKStrategyRecipe: Codable, Equatable, Sendable { /// Result bundle for manager-level indicator composition helpers. public struct BKIndicatorBundleResult: Codable, Equatable, Sendable { + /// Candles associated with this value. public var candles: [Candlestick] + /// Applied indicator keys associated with this value. public var appliedIndicatorKeys: [String] /// Creates a new instance. @@ -30,9 +32,13 @@ public struct BKIndicatorBundleResult: Codable, Equatable, Sendable { /// Lightweight snapshot of a manager-built metrics report. public struct BKManagerReportSnapshot: Codable, Equatable, Sendable { + /// Headline metrics associated with this value. public var metrics: BKRunHeadlineMetrics + /// Trade return count represented by this value. public var tradeReturnCount: Int + /// Additive equity point count represented by this value. public var additiveEquityPointCount: Int + /// Compounded equity point count represented by this value. public var compoundedEquityPointCount: Int /// Creates a new instance. @@ -61,11 +67,17 @@ public struct BKManagerReportSnapshot: Codable, Equatable, Sendable { /// Report bundle produced by a manager-owned recipe workflow. public struct BKStrategyRecipeReport { + /// Recipe associated with this value. public var recipe: BKStrategyRecipe + /// Ticker symbol associated with this value. public var symbol: String + /// Result associated with this value. public var result: BacktestResult + /// High-level summary associated with this value. public var summary: BKRunSummary + /// Snapshot associated with this value. public var snapshot: BKManagerReportSnapshot + /// Advanced metrics associated with this value. public var advancedMetrics: BKAdvancedPerformanceMetrics /// Creates a new instance. diff --git a/BacktestingKit/Engine/BKPortfolioModels.swift b/BacktestingKit/Engine/BKPortfolioModels.swift new file mode 100644 index 0000000..8762f54 --- /dev/null +++ b/BacktestingKit/Engine/BKPortfolioModels.swift @@ -0,0 +1,308 @@ +import Foundation + +/// Allocation mode applied to portfolio sleeves before aggregation. +public enum BKPortfolioAllocationMode: String, Codable, Equatable, Sendable { + case explicit + case sleeveWeights + case riskParity + case riskOnRiskOff +} + +/// Input describing how the portfolio should resolve sleeve weights. +public struct BKPortfolioAllocationInput: Codable, Equatable, Sendable { + /// Allocation mode associated with this value. + public var mode: BKPortfolioAllocationMode + /// Explicit sleeve weights when `.explicit` is used. + public var explicitWeights: [Double]? + /// Index of the risk-on sleeve when `.riskOnRiskOff` is used. + public var riskOnIndex: Int? + /// Index of the defensive sleeve when `.riskOnRiskOff` is used. + public var riskOffIndex: Int? + + /// Creates a new instance. + public init( + mode: BKPortfolioAllocationMode, + explicitWeights: [Double]? = nil, + riskOnIndex: Int? = nil, + riskOffIndex: Int? = nil + ) { + self.mode = mode + self.explicitWeights = explicitWeights + self.riskOnIndex = riskOnIndex + self.riskOffIndex = riskOffIndex + } +} + +public extension BKPortfolioAllocationInput { + /// Portfolio weights are resolved from caller-supplied explicit weights. + static func explicit(_ weights: [Double]) -> BKPortfolioAllocationInput { + BKPortfolioAllocationInput( + mode: .explicit, + explicitWeights: weights + ) + } + + /// Portfolio weights are resolved from each sleeve's `targetWeight`. + static var sleeveWeights: BKPortfolioAllocationInput { + BKPortfolioAllocationInput(mode: .sleeveWeights) + } + + /// Portfolio weights are resolved from inverse realized volatility. + static var riskParity: BKPortfolioAllocationInput { + BKPortfolioAllocationInput(mode: .riskParity) + } + + /// Portfolio weights are resolved from the selected risk-on and defensive sleeves. + static func riskOnRiskOff( + riskOnIndex: Int = 0, + riskOffIndex: Int = 1 + ) -> BKPortfolioAllocationInput { + BKPortfolioAllocationInput( + mode: .riskOnRiskOff, + riskOnIndex: riskOnIndex, + riskOffIndex: riskOffIndex + ) + } +} + +/// Rebalance mode for additive portfolio aggregation workflows. +public enum BKPortfolioRebalanceMode: String, Codable, Equatable, Sendable { + case none + case periodic + case manual +} + +/// Supported periodic rebalance frequencies for additive portfolio workflows. +public enum BKPortfolioRebalanceFrequency: String, Codable, Equatable, Sendable { + case weekly + case monthly + case quarterly +} + +/// Policy describing if and when portfolio weights should be re-applied. +public struct BKPortfolioRebalancePolicy: Codable, Equatable, Sendable { + /// Rebalance mode associated with this value. + public var mode: BKPortfolioRebalanceMode + /// Periodic rebalance frequency when `.periodic` is used. + public var frequency: BKPortfolioRebalanceFrequency? + /// Explicit rebalance dates when `.manual` is used. + public var manualDates: [Date] + + /// Creates a new instance. + public init( + mode: BKPortfolioRebalanceMode, + frequency: BKPortfolioRebalanceFrequency? = nil, + manualDates: [Date] = [] + ) { + self.mode = mode + self.frequency = frequency + self.manualDates = manualDates.sorted() + } +} + +public extension BKPortfolioRebalancePolicy { + /// Leaves resolved weights untouched after the initial allocation. + static var none: BKPortfolioRebalancePolicy { + BKPortfolioRebalancePolicy(mode: .none) + } + + /// Re-applies resolved weights on a periodic cadence. + static func periodic(_ frequency: BKPortfolioRebalanceFrequency) -> BKPortfolioRebalancePolicy { + BKPortfolioRebalancePolicy( + mode: .periodic, + frequency: frequency + ) + } + + /// Re-applies resolved weights on a caller-defined set of dates. + static func manual(_ dates: [Date]) -> BKPortfolioRebalancePolicy { + BKPortfolioRebalancePolicy( + mode: .manual, + manualDates: dates + ) + } +} + +/// A single preset-backed sleeve request for additive portfolio orchestration. +public struct BKPortfolioSleeveRequest: Codable, Equatable, Sendable { + /// Sleeve identifier associated with this value. + public var symbol: String + /// CSV payload associated with this value. + public var csv: String + /// Preset associated with this value. + public var preset: BKPresetCatalog + /// Date format string used while parsing or formatting input. + public var dateFormat: String + /// Whether the input should be reversed into chronological order. + public var reverse: Bool + /// Column mapping associated with this value. + public var columnMapping: BKCSVColumnMapping? + /// Caller-supplied target weight associated with this value. + public var targetWeight: Double? + + /// Creates a new instance. + public init( + symbol: String, + csv: String, + preset: BKPresetCatalog, + dateFormat: String = "yyyy-MM-dd", + reverse: Bool = false, + columnMapping: BKCSVColumnMapping? = nil, + targetWeight: Double? = nil + ) { + self.symbol = symbol + self.csv = csv + self.preset = preset + self.dateFormat = dateFormat + self.reverse = reverse + self.columnMapping = columnMapping + self.targetWeight = targetWeight + } +} + +/// Canonical request payload for additive portfolio orchestration. +public struct BKPortfolioRequest: Codable, Equatable, Sendable { + /// Portfolio identifier associated with this value. + public var portfolioID: String + /// Sleeve requests associated with this value. + public var sleeves: [BKPortfolioSleeveRequest] + /// Allocation input associated with this value. + public var allocation: BKPortfolioAllocationInput + /// Rebalance policy associated with this value. + public var rebalancePolicy: BKPortfolioRebalancePolicy + /// Whether to keep aggregating after a sleeve failure. + public var continueOnFailure: Bool + + /// Creates a new instance. + public init( + portfolioID: String = "PORTFOLIO", + sleeves: [BKPortfolioSleeveRequest], + allocation: BKPortfolioAllocationInput = .sleeveWeights, + rebalancePolicy: BKPortfolioRebalancePolicy = .none, + continueOnFailure: Bool = true + ) { + self.portfolioID = portfolioID + self.sleeves = sleeves + self.allocation = allocation + self.rebalancePolicy = rebalancePolicy + self.continueOnFailure = continueOnFailure + } +} + +/// Execution status for a single sleeve inside a portfolio run. +public enum BKPortfolioSleeveStatus: String, Codable, Equatable, Sendable { + case succeeded + case failed +} + +/// One resolved rebalance application event. +public struct BKPortfolioRebalanceEvent: Codable, Equatable, Sendable { + /// Event date associated with this value. + public var date: Date + /// Human-readable source associated with this value. + public var source: String + + /// Creates a new instance. + public init(date: Date, source: String) { + self.date = date + self.source = source + } +} + +/// Per-sleeve result captured by the additive portfolio workflow. +public struct BKPortfolioSleeveRunReport: Codable, Equatable, Sendable { + /// Sleeve identifier associated with this value. + public var symbol: String + /// Preset associated with this value. + public var preset: BKPresetCatalog + /// Execution status associated with this value. + public var status: BKPortfolioSleeveStatus + /// Requested target weight associated with this value. + public var requestedWeight: Double? + /// Resolved normalized weight associated with this value. + public var resolvedWeight: Double + /// Realized annualized volatility associated with this value. + public var annualizedVolatility: Double? + /// Momentum score associated with this value. + public var momentumScore: Double? + /// High-level summary associated with this value. + public var summary: BKRunSummary? + /// Structured failure associated with this value. + public var failure: BKEngineFailure? + + /// Creates a new instance. + public init( + symbol: String, + preset: BKPresetCatalog, + status: BKPortfolioSleeveStatus, + requestedWeight: Double? = nil, + resolvedWeight: Double = 0, + annualizedVolatility: Double? = nil, + momentumScore: Double? = nil, + summary: BKRunSummary? = nil, + failure: BKEngineFailure? = nil + ) { + self.symbol = symbol + self.preset = preset + self.status = status + self.requestedWeight = requestedWeight + self.resolvedWeight = resolvedWeight + self.annualizedVolatility = annualizedVolatility + self.momentumScore = momentumScore + self.summary = summary + self.failure = failure + } +} + +/// Aggregate result emitted by additive portfolio orchestration. +public struct BKPortfolioRunReport: Codable, Equatable, Sendable { + /// Portfolio identifier associated with this value. + public var portfolioID: String + /// Allocation input associated with this value. + public var allocation: BKPortfolioAllocationInput + /// Rebalance policy associated with this value. + public var rebalancePolicy: BKPortfolioRebalancePolicy + /// High-level aggregate summary associated with this value. + public var summary: BKRunSummary? + /// Per-sleeve reports associated with this value. + public var sleeveReports: [BKPortfolioSleeveRunReport] + /// Structured rebalance events associated with this value. + public var rebalanceEvents: [BKPortfolioRebalanceEvent] + /// Failures associated with this value. + public var failures: [BKEngineFailure] + /// Number of successful sleeves represented by this value. + public var succeededSleeveCount: Int + /// Number of failed sleeves represented by this value. + public var failedSleeveCount: Int + /// Whether the run succeeded for at least one sleeve but not all sleeves. + public var isPartialSuccess: Bool + /// Whether the run produced an aggregate portfolio summary. + public var isSuccessful: Bool + + /// Creates a new instance. + public init( + portfolioID: String, + allocation: BKPortfolioAllocationInput, + rebalancePolicy: BKPortfolioRebalancePolicy, + summary: BKRunSummary? = nil, + sleeveReports: [BKPortfolioSleeveRunReport], + rebalanceEvents: [BKPortfolioRebalanceEvent] = [], + failures: [BKEngineFailure] = [], + succeededSleeveCount: Int, + failedSleeveCount: Int, + isPartialSuccess: Bool, + isSuccessful: Bool + ) { + self.portfolioID = portfolioID + self.allocation = allocation + self.rebalancePolicy = rebalancePolicy + self.summary = summary + self.sleeveReports = sleeveReports + self.rebalanceEvents = rebalanceEvents + self.failures = failures + self.succeededSleeveCount = succeededSleeveCount + self.failedSleeveCount = failedSleeveCount + self.isPartialSuccess = isPartialSuccess + self.isSuccessful = isSuccessful + } +} diff --git a/BacktestingKit/Engine/BKPortfolioOrchestration.swift b/BacktestingKit/Engine/BKPortfolioOrchestration.swift new file mode 100644 index 0000000..6e48517 --- /dev/null +++ b/BacktestingKit/Engine/BKPortfolioOrchestration.swift @@ -0,0 +1,552 @@ +import Foundation + +public extension BKEngine { + /// Runs a preset-backed additive portfolio workflow and returns aggregate plus sleeve-level output. + static func runPortfolio( + _ request: PortfolioRequest, + log: @escaping @Sendable (String) -> Void = { _ in } + ) -> BKPortfolioRunReport { + let portfolioID = normalizedPortfolioID(request.portfolioID) + guard !request.sleeves.isEmpty else { + let failure = makePortfolioValidationFailure( + instrumentID: portfolioID, + stage: "portfolio-validation", + message: "Portfolio request must contain at least one sleeve." + ) + return BKPortfolioRunReport( + portfolioID: portfolioID, + allocation: request.allocation, + rebalancePolicy: request.rebalancePolicy, + sleeveReports: [], + failures: [failure], + succeededSleeveCount: 0, + failedSleeveCount: 0, + isPartialSuccess: false, + isSuccessful: false + ) + } + + let duplicateSymbols = duplicateSleeveSymbols(in: request.sleeves) + guard duplicateSymbols.isEmpty else { + let failure = makePortfolioValidationFailure( + instrumentID: portfolioID, + stage: "portfolio-validation", + message: "Portfolio sleeve symbols must be unique.", + metadata: ["duplicates": duplicateSymbols.joined(separator: ",")] + ) + return BKPortfolioRunReport( + portfolioID: portfolioID, + allocation: request.allocation, + rebalancePolicy: request.rebalancePolicy, + sleeveReports: [], + failures: [failure], + succeededSleeveCount: 0, + failedSleeveCount: 0, + isPartialSuccess: false, + isSuccessful: false + ) + } + + var sleeveReports: [BKPortfolioSleeveRunReport] = [] + sleeveReports.reserveCapacity(request.sleeves.count) + + var successfulContexts: [Int: BKPortfolioSuccessfulSleeveContext] = [:] + var failures: [BKEngineFailure] = [] + var stoppedOnFailure = false + + for (index, sleeve) in request.sleeves.enumerated() { + let run = preflightAndRunCSV( + symbol: sleeve.symbol, + csv: sleeve.csv, + preset: sleeve.preset, + dateFormat: sleeve.dateFormat, + reverse: sleeve.reverse, + columnMapping: sleeve.columnMapping, + log: log + ) + + if let summary = run.summary, + let bars = try? successfulPortfolioBars(for: sleeve) { + let context = BKPortfolioSuccessfulSleeveContext( + summary: summary, + annualizedVolatility: annualizedVolatility(from: bars), + momentumScore: summary.metrics.totalReturn + ) + successfulContexts[index] = context + sleeveReports.append( + BKPortfolioSleeveRunReport( + symbol: sleeve.symbol, + preset: sleeve.preset, + status: .succeeded, + requestedWeight: sleeve.targetWeight, + annualizedVolatility: context.annualizedVolatility, + momentumScore: context.momentumScore, + summary: summary + ) + ) + } else { + let failure = run.failure + ?? makePortfolioValidationFailure( + instrumentID: sleeve.symbol, + stage: "portfolio-sleeve-run", + message: "Sleeve execution did not produce a summary." + ) + failures.append(failure) + sleeveReports.append( + BKPortfolioSleeveRunReport( + symbol: sleeve.symbol, + preset: sleeve.preset, + status: .failed, + requestedWeight: sleeve.targetWeight, + failure: failure + ) + ) + if !request.continueOnFailure { + stoppedOnFailure = true + break + } + } + } + + let weightResolution = resolvePortfolioWeights( + request: request, + successfulContexts: successfulContexts + ) + failures.append(contentsOf: weightResolution.failures) + + let updatedSleeves = sleeveReports.enumerated().map { index, sleeveReport in + var updated = sleeveReport + updated.resolvedWeight = weightResolution.weights[index] + return updated + } + + if stoppedOnFailure { + return finalizePortfolioRun( + portfolioID: portfolioID, + allocation: request.allocation, + rebalancePolicy: request.rebalancePolicy, + sleeveReports: updatedSleeves, + successfulContexts: successfulContexts, + failures: failures, + requestLevelFailures: weightResolution.failures + ) + } + + return finalizePortfolioRun( + portfolioID: portfolioID, + allocation: request.allocation, + rebalancePolicy: request.rebalancePolicy, + sleeveReports: updatedSleeves, + successfulContexts: successfulContexts, + failures: failures, + requestLevelFailures: weightResolution.failures + ) + } +} + +private struct BKPortfolioSuccessfulSleeveContext { + let summary: BKRunSummary + let annualizedVolatility: Double + let momentumScore: Double +} + +private struct BKPortfolioWeightResolution { + let weights: [Double] + let failures: [BKEngineFailure] +} + +private func normalizedPortfolioID(_ portfolioID: String) -> String { + let trimmed = portfolioID.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? "PORTFOLIO" : trimmed +} + +private func duplicateSleeveSymbols(in sleeves: [BKPortfolioSleeveRequest]) -> [String] { + var seen: Set = [] + var duplicates: Set = [] + + for sleeve in sleeves { + let symbol = sleeve.symbol.trimmingCharacters(in: .whitespacesAndNewlines) + if seen.contains(symbol) { + duplicates.insert(symbol) + } else { + seen.insert(symbol) + } + } + + return duplicates.sorted() +} + +private func successfulPortfolioBars(for sleeve: BKPortfolioSleeveRequest) throws -> [BKBar] { + switch BKQuickDemo.parseBars( + csv: sleeve.csv, + dateFormat: sleeve.dateFormat, + reverse: sleeve.reverse, + columnMapping: sleeve.columnMapping + ) { + case .success(let bars): + return bars + case .failure(let error): + throw error + } +} + +private func annualizedVolatility(from bars: [BKBar]) -> Double { + guard bars.count > 2 else { return 0 } + + let closes = bars.map(\.close) + let returns = zip(closes, closes.dropFirst()).compactMap { previous, current -> Double? in + guard previous > 0, current > 0 else { return nil } + return (current / previous) - 1.0 + } + + guard returns.count > 1 else { return 0 } + + let mean = returns.reduce(0, +) / Double(returns.count) + let variance = returns.reduce(0) { partial, value in + let delta = value - mean + return partial + (delta * delta) + } / Double(returns.count - 1) + + return sqrt(max(variance, 0)) * sqrt(252) +} + +private func resolvePortfolioWeights( + request: BKPortfolioRequest, + successfulContexts: [Int: BKPortfolioSuccessfulSleeveContext] +) -> BKPortfolioWeightResolution { + let count = request.sleeves.count + guard count > 0 else { + return BKPortfolioWeightResolution(weights: [], failures: []) + } + + var failures: [BKEngineFailure] = [] + let successIndices = Set(successfulContexts.keys) + + enum ZeroTotalBehavior { + case equalWeight + case keepZero + case fail(String) + } + + func normalizedSuccessfulWeights( + from rawWeights: [Double], + zeroTotalBehavior: ZeroTotalBehavior + ) -> BKPortfolioWeightResolution { + var filtered = rawWeights.enumerated().map { index, weight -> Double in + guard successIndices.contains(index) else { return 0 } + return max(weight, 0) + } + + let total = filtered.reduce(0, +) + guard total > 0 else { + switch zeroTotalBehavior { + case .equalWeight: + let equalWeight = successIndices.isEmpty ? 0 : 1.0 / Double(successIndices.count) + return BKPortfolioWeightResolution( + weights: filtered.enumerated().map { index, _ in + successIndices.contains(index) ? equalWeight : 0 + }, + failures: [] + ) + case .keepZero: + return BKPortfolioWeightResolution(weights: filtered, failures: []) + case .fail(let message): + return BKPortfolioWeightResolution( + weights: filtered, + failures: [ + makePortfolioValidationFailure( + instrumentID: normalizedPortfolioID(request.portfolioID), + stage: "portfolio-allocation", + message: message, + metadata: [ + "successfulSleeveCount": String(successIndices.count), + "allocationMode": request.allocation.mode.rawValue, + ] + ) + ] + ) + } + } + + for index in filtered.indices { + filtered[index] /= total + } + return BKPortfolioWeightResolution(weights: filtered, failures: []) + } + + let rawWeights: [Double] + let zeroTotalBehavior: ZeroTotalBehavior + switch request.allocation.mode { + case .explicit: + guard let explicitWeights = request.allocation.explicitWeights, + explicitWeights.count == count else { + failures.append( + makePortfolioValidationFailure( + instrumentID: normalizedPortfolioID(request.portfolioID), + stage: "portfolio-allocation", + message: "Explicit portfolio weights must match the sleeve count.", + metadata: [ + "sleeveCount": String(count), + "weightCount": String(request.allocation.explicitWeights?.count ?? 0), + ] + ) + ) + rawWeights = Array(repeating: 0, count: count) + zeroTotalBehavior = .keepZero + break + } + rawWeights = explicitWeights + zeroTotalBehavior = .fail("Explicit portfolio weights must resolve to a positive total across successful sleeves.") + + case .sleeveWeights: + rawWeights = request.sleeves.map { $0.targetWeight ?? 0 } + zeroTotalBehavior = .equalWeight + + case .riskParity: + let volatilities = (0.. Double in + guard successIndices.contains(index) else { return 0 } + return successfulContexts[index]?.annualizedVolatility ?? 0 + } + rawWeights = BKPortfolioPresets.riskParityWeights(annualizedVolatilities: volatilities) + zeroTotalBehavior = .equalWeight + + case .riskOnRiskOff: + let riskOnIndex = request.allocation.riskOnIndex ?? 0 + let riskOffIndex = request.allocation.riskOffIndex ?? 1 + guard riskOnIndex >= 0, + riskOnIndex < count, + riskOffIndex >= 0, + riskOffIndex < count, + riskOnIndex != riskOffIndex else { + failures.append( + makePortfolioValidationFailure( + instrumentID: normalizedPortfolioID(request.portfolioID), + stage: "portfolio-allocation", + message: "Risk-on / risk-off allocation requires two distinct valid sleeve indices." + ) + ) + rawWeights = Array(repeating: 0, count: count) + zeroTotalBehavior = .keepZero + break + } + + let riskOnMomentum = successfulContexts[riskOnIndex]?.momentumScore ?? 0 + let riskOffMomentum = successfulContexts[riskOffIndex]?.momentumScore ?? 0 + let pairWeights = BKPortfolioPresets.riskOnRiskOffWeights( + riskOnMomentum: riskOnMomentum, + riskOffMomentum: riskOffMomentum + ) + + var working = Array(repeating: 0.0, count: count) + working[riskOnIndex] = successIndices.contains(riskOnIndex) ? pairWeights[0] : 0 + working[riskOffIndex] = successIndices.contains(riskOffIndex) ? pairWeights[1] : 0 + rawWeights = working + zeroTotalBehavior = .equalWeight + } + + let normalizedWeights = normalizedSuccessfulWeights( + from: rawWeights, + zeroTotalBehavior: zeroTotalBehavior + ) + failures.append(contentsOf: normalizedWeights.failures) + return BKPortfolioWeightResolution(weights: normalizedWeights.weights, failures: failures) +} + +private func finalizePortfolioRun( + portfolioID: String, + allocation: BKPortfolioAllocationInput, + rebalancePolicy: BKPortfolioRebalancePolicy, + sleeveReports: [BKPortfolioSleeveRunReport], + successfulContexts: [Int: BKPortfolioSuccessfulSleeveContext], + failures: [BKEngineFailure], + requestLevelFailures: [BKEngineFailure] +) -> BKPortfolioRunReport { + let succeededSleeveCount = sleeveReports.filter { $0.status == .succeeded }.count + let failedSleeveCount = sleeveReports.filter { $0.status == .failed }.count + + guard succeededSleeveCount > 0 else { + return BKPortfolioRunReport( + portfolioID: portfolioID, + allocation: allocation, + rebalancePolicy: rebalancePolicy, + sleeveReports: sleeveReports, + failures: failures, + succeededSleeveCount: succeededSleeveCount, + failedSleeveCount: failedSleeveCount, + isPartialSuccess: false, + isSuccessful: false + ) + } + + guard requestLevelFailures.isEmpty else { + return BKPortfolioRunReport( + portfolioID: portfolioID, + allocation: allocation, + rebalancePolicy: rebalancePolicy, + sleeveReports: sleeveReports, + failures: failures, + succeededSleeveCount: succeededSleeveCount, + failedSleeveCount: failedSleeveCount, + isPartialSuccess: true, + isSuccessful: false + ) + } + + let successfulSleeves = sleeveReports.enumerated().compactMap { index, report -> BKPortfolioSleeveRunReport? in + guard successfulContexts[index] != nil else { return nil } + return report + } + + let aggregateMetrics = aggregatePortfolioMetrics(from: successfulSleeves) + let startDate = successfulSleeves.compactMap { $0.summary?.startDate }.min() + let endDate = successfulSleeves.compactMap { $0.summary?.endDate }.max() + let barCount = successfulSleeves.reduce(0) { partial, report in + partial + (report.summary?.barCount ?? 0) + } + let rebalanceEvents = buildRebalanceEvents( + policy: rebalancePolicy, + startDate: startDate, + endDate: endDate + ) + + let summary = BKRunSummary( + symbol: portfolioID, + barCount: barCount, + startDate: startDate, + endDate: endDate, + metrics: aggregateMetrics + ) + + return BKPortfolioRunReport( + portfolioID: portfolioID, + allocation: allocation, + rebalancePolicy: rebalancePolicy, + summary: summary, + sleeveReports: sleeveReports, + rebalanceEvents: rebalanceEvents, + failures: failures, + succeededSleeveCount: succeededSleeveCount, + failedSleeveCount: failedSleeveCount, + isPartialSuccess: succeededSleeveCount > 0 && failedSleeveCount > 0, + isSuccessful: true + ) +} + +private func aggregatePortfolioMetrics( + from sleeveReports: [BKPortfolioSleeveRunReport] +) -> BKRunHeadlineMetrics { + let totalWeight = sleeveReports.reduce(0) { partial, report in + partial + max(report.resolvedWeight, 0) + } + let normalizedReports: [(BKPortfolioSleeveRunReport, Double)] = sleeveReports.map { report in + let normalizedWeight = totalWeight > 0 ? report.resolvedWeight / totalWeight : 0 + return (report, normalizedWeight) + } + + let totalTrades = normalizedReports.reduce(0) { partial, pair in + partial + (pair.0.summary?.metrics.tradeCount ?? 0) + } + let weightedWinRate: Double + if totalTrades > 0 { + weightedWinRate = normalizedReports.reduce(0) { partial, pair in + let trades = pair.0.summary?.metrics.tradeCount ?? 0 + let wins = Double(trades) * (pair.0.summary?.metrics.winRate ?? 0) + return partial + wins + } / Double(totalTrades) + } else { + weightedWinRate = normalizedReports.reduce(0) { partial, pair in + partial + ((pair.0.summary?.metrics.winRate ?? 0) * pair.1) + } + } + + return BKRunHeadlineMetrics( + tradeCount: totalTrades, + winRate: weightedWinRate, + totalReturn: normalizedReports.reduce(0) { partial, pair in + partial + ((pair.0.summary?.metrics.totalReturn ?? 0) * pair.1) + }, + annualizedReturn: normalizedReports.reduce(0) { partial, pair in + partial + ((pair.0.summary?.metrics.annualizedReturn ?? 0) * pair.1) + }, + maxDrawdown: normalizedReports.reduce(0) { partial, pair in + partial + ((pair.0.summary?.metrics.maxDrawdown ?? 0) * pair.1) + }, + sharpeRatio: normalizedReports.reduce(0) { partial, pair in + partial + ((pair.0.summary?.metrics.sharpeRatio ?? 0) * pair.1) + }, + profitFactor: normalizedReports.reduce(0) { partial, pair in + partial + ((pair.0.summary?.metrics.profitFactor ?? 0) * pair.1) + } + ) +} + +private func buildRebalanceEvents( + policy: BKPortfolioRebalancePolicy, + startDate: Date?, + endDate: Date? +) -> [BKPortfolioRebalanceEvent] { + guard let startDate, let endDate, startDate <= endDate else { return [] } + + switch policy.mode { + case .none: + return [] + + case .manual: + return policy.manualDates + .filter { $0 >= startDate && $0 <= endDate } + .sorted() + .map { BKPortfolioRebalanceEvent(date: $0, source: "manual") } + + case .periodic: + guard let frequency = policy.frequency else { return [] } + var events: [BKPortfolioRebalanceEvent] = [] + let calendar = Calendar(identifier: .gregorian) + var cursor = startDate + + while let next = nextRebalanceDate( + after: cursor, + frequency: frequency, + calendar: calendar + ), next <= endDate { + events.append( + BKPortfolioRebalanceEvent( + date: next, + source: "periodic:\(frequency.rawValue)" + ) + ) + cursor = next + } + + return events + } +} + +private func nextRebalanceDate( + after date: Date, + frequency: BKPortfolioRebalanceFrequency, + calendar: Calendar +) -> Date? { + switch frequency { + case .weekly: + return calendar.date(byAdding: .day, value: 7, to: date) + case .monthly: + return calendar.date(byAdding: .month, value: 1, to: date) + case .quarterly: + return calendar.date(byAdding: .month, value: 3, to: date) + } +} + +private func makePortfolioValidationFailure( + instrumentID: String, + stage: String, + message: String, + metadata: [String: String] = [:] +) -> BKEngineFailure { + BKEngineFailure( + instrumentID: instrumentID, + code: .invalidInput, + stage: stage, + message: message, + metadata: metadata + ) +} diff --git a/BacktestingKit/Engine/BKPresetProfiles.swift b/BacktestingKit/Engine/BKPresetProfiles.swift index 044ecb3..b93c6fc 100644 --- a/BacktestingKit/Engine/BKPresetProfiles.swift +++ b/BacktestingKit/Engine/BKPresetProfiles.swift @@ -1,7 +1,10 @@ import Foundation +/// Reusable execution-model bundle combining slippage and commission assumptions. public struct BKExecutionPresetProfile { + /// Slippage model associated with this value. public let slippageModel: BKSlippageModel + /// Commission model associated with this value. public let commissionModel: BKCommissionModel /// Creates a custom execution profile. @@ -27,6 +30,7 @@ public extension BKExecutionPresetProfile { /// Wrapper for reusable position-sizing model profiles. public struct BKPositionSizingPresetProfile { + /// Sizing model associated with this value. public let sizingModel: BKPositionSizingModel /// Creates a custom position-sizing profile. @@ -62,9 +66,13 @@ public extension BKPositionSizingPresetProfile { /// Prebuilt stop/risk policy bundles for strategy-level composition. public struct BKRiskControlPresetProfile { + /// ATR period associated with this value. public let atrPeriod: Int + /// ATR stop multiplier associated with this value. public let atrStopMultiplier: Double + /// Whether to use trailing stop. public let useTrailingStop: Bool + /// Maximum holding bars associated with this value. public let maxHoldingBars: Int? /// Creates a new instance. @@ -95,9 +103,13 @@ public extension BKRiskControlPresetProfile { /// Evaluation preset bundle for walk-forward and stability-oriented runs. public struct BKEvaluationPresetProfile { + /// Walk forward folds associated with this value. public let walkForwardFolds: Int + /// In sample ratio associated with this value. public let inSampleRatio: Double + /// Whether to include out of sample stability score. public let includeOutOfSampleStabilityScore: Bool + /// Bootstrap runs associated with this value. public let bootstrapRuns: Int /// Creates a new instance. diff --git a/BacktestingKit/Engine/BKQuickDemo.swift b/BacktestingKit/Engine/BKQuickDemo.swift index 251fd2d..0dd64b5 100644 --- a/BacktestingKit/Engine/BKQuickDemo.swift +++ b/BacktestingKit/Engine/BKQuickDemo.swift @@ -13,6 +13,7 @@ public enum BKQuickDemoDataset: String, CaseIterable, Sendable { case wmt = "WMT_10Y_1D" case ko = "KO_10Y_1D" + /// Ticker symbol associated with this value. public var symbol: String { switch self { case .aapl: return "AAPL" @@ -28,6 +29,7 @@ public enum BKQuickDemoDataset: String, CaseIterable, Sendable { } } + /// Exchange code associated with this value. public var exchange: String { switch self { case .aapl, .msft, .googl, .nvda, .tsla, .amzn: @@ -44,6 +46,7 @@ public enum BKQuickDemoError: LocalizedError { case emptyCSV case unsupportedPreset(BKPresetCatalog) + /// Localized description of the error. public var errorDescription: String? { switch self { case .missingBundledCSV(let dataset): @@ -58,10 +61,15 @@ public enum BKQuickDemoError: LocalizedError { /// Represents `BKQuickDemoSummary` in the BacktestingKit public API. public struct BKQuickDemoSummary { + /// Ticker symbol associated with this value. public let symbol: String + /// Number of bars represented by this value. public let barCount: Int + /// Start of the date range represented by this summary. public let dateRangeStart: Date + /// End of the date range represented by this summary. public let dateRangeEnd: Date + /// Result associated with this value. public let result: BacktestResult /// Creates a new instance. @@ -157,6 +165,12 @@ public enum BKQuickDemo { /// One-line quick demo for Playground/SPM users. /// Runs bundled 10y/1d SMA crossover (5/20) from bundled CSV (offline). + /// + /// - Parameters: + /// - dataset: Bundled sample dataset to load when inline CSV is not provided. + /// - csv: Optional inline CSV override for the demo input. + /// - log: Callback that receives progress messages as the demo runs. + /// - Returns: A result containing the quick-demo summary or the error that stopped the demo. @discardableResult public static func runBundledSMACrossoverDemo( dataset: BKQuickDemoDataset = .aapl, @@ -222,7 +236,15 @@ public enum BKQuickDemo { )) } - /// Runs an SMA crossover workflow directly from inline CSV. + /// Runs an SMA crossover workflow directly from inline CSV input. + /// + /// - Parameters: + /// - symbol: Symbol to associate with the inline dataset. + /// - csv: CSV payload containing OHLCV rows for the demo. + /// - fast: Fast SMA window length. + /// - slow: Slow SMA window length. + /// - log: Callback that receives progress messages as the demo runs. + /// - Returns: A result containing the quick-demo summary or the error that stopped the demo. @discardableResult public static func runSMACrossoverDemo( symbol: String, @@ -261,6 +283,12 @@ public enum BKQuickDemo { ) } + /// Runs the default bundled AAPL 10Y/1D SMA crossover demo. + /// + /// - Parameters: + /// - csv: Optional inline CSV override for the bundled demo input. + /// - log: Callback that receives progress messages as the demo runs. + /// - Returns: A result containing the quick-demo summary or the error that stopped the demo. @discardableResult public static func runAAPL10Y1DSMACrossover( csv: String? = nil, diff --git a/BacktestingKit/Engine/BacktestingKit.swift b/BacktestingKit/Engine/BacktestingKit.swift index 94d3d31..1364462 100644 --- a/BacktestingKit/Engine/BacktestingKit.swift +++ b/BacktestingKit/Engine/BacktestingKit.swift @@ -1,5 +1,6 @@ import Foundation +/// High-level façade for indicator calculation, strategy helpers, and metrics reporting. public final class BacktestingKitManager: BKBacktestingEngine { private let backtestMetricsCalculator: any BKBacktestMetricsCalculating diff --git a/BacktestingKit/Models/External/BKSeriesModels.swift b/BacktestingKit/Models/External/BKSeriesModels.swift index fec6cc4..c016ae4 100644 --- a/BacktestingKit/Models/External/BKSeriesModels.swift +++ b/BacktestingKit/Models/External/BKSeriesModels.swift @@ -2,7 +2,9 @@ import Foundation /// Represents `DFSeriesWindow` in the BacktestingKit public API. public struct DFSeriesWindow { + /// Indices associated with this value. public var indices: [Index] + /// Values associated with this value. public var values: [Value] /// Executes `last`. @@ -18,7 +20,9 @@ public struct DFSeriesWindow { /// Represents `DFSeries` in the BacktestingKit public API. public struct DFSeries { + /// Indices associated with this value. public var indices: [Index] + /// Values associated with this value. public var values: [Value] /// Creates a new instance. @@ -322,25 +326,37 @@ extension DFSeries where Value == Double { /// Represents `DFBollingerRow` in the BacktestingKit public API. public struct DFBollingerRow { + /// Value associated with this value. public var value: Double + /// Upper associated with this value. public var upper: Double + /// Middle associated with this value. public var middle: Double + /// Lower associated with this value. public var lower: Double + /// Stddev associated with this value. public var stddev: Double } /// Represents `DFMacdRow` in the BacktestingKit public API. public struct DFMacdRow { + /// Short EMA associated with this value. public var shortEMA: Double + /// Long EMA associated with this value. public var longEMA: Double + /// MACD associated with this value. public var macd: Double + /// Signal associated with this value. public var signal: Double + /// Histogram associated with this value. public var histogram: Double } /// Represents `DFDataFrame` in the BacktestingKit public API. public struct DFDataFrame { + /// Indices associated with this value. public var indices: [Index] + /// Rows associated with this value. public var rows: [Row] /// Creates a new instance. @@ -412,11 +428,13 @@ public struct DFDataFrame { /// Represents `DFIndex` in the BacktestingKit public API. public struct DFIndex: Comparable, Codable { + /// Value associated with this value. public var value: Value /// Creates a new instance. public init(_ value: Value) { self.value = value } + /// Compares two typed index values using the wrapped comparable payload. public static func < (lhs: DFIndex, rhs: DFIndex) -> Bool { return lhs.value < rhs.value } @@ -424,7 +442,9 @@ public struct DFIndex: Comparable, Codable { /// Represents `DFPair` in the BacktestingKit public API. public struct DFPair { + /// Index associated with this value. public var index: Index + /// Value associated with this value. public var value: Value } @@ -487,6 +507,7 @@ extension DFDataFrame { } extension DFDataFrame where Row == [String: Double] { + /// Merges row dictionaries by position, preferring values from later frames on key conflicts. public static func merge(_ frames: [DFDataFrame]) -> DFDataFrame { guard let first = frames.first else { return DFDataFrame(indices: [], rows: []) @@ -502,6 +523,7 @@ extension DFDataFrame where Row == [String: Double] { return DFDataFrame(indices: first.indices, rows: mergedRows) } + /// Merges row dictionaries by index, preserving the ordering of the first frame. public static func mergeAligned(_ frames: [DFDataFrame]) -> DFDataFrame where Index: Hashable { guard let first = frames.first else { return DFDataFrame(indices: [], rows: []) @@ -614,6 +636,7 @@ extension DFDataFrame where Row == BKBar { /// Represents `DFDataFrameAny` in the BacktestingKit public API. public struct DFDataFrameAny { + /// Rows associated with this value. public var rows: [[String: Any]] /// Creates a new instance. public init(rows: [[String: Any]]) { @@ -654,6 +677,7 @@ extension DFDataFrame { /// Represents `DFPivot` in the BacktestingKit public API. public struct DFPivot { + /// Rows associated with this value. public var rows: [RowKey: [ColumnKey: Value]] /// Creates a new instance. public init(rows: [RowKey: [ColumnKey: Value]]) { @@ -683,6 +707,7 @@ extension DFDataFrame { /// Represents `DFCSV` in the BacktestingKit public API. public enum DFCSV { + /// Parses CSV text into a string-valued data frame using the header row as column names. public static func parse(_ csv: String) -> DFDataFrame { let lines = csv.split(whereSeparator: \.isNewline).map { String($0) } guard let header = lines.first else { return DFDataFrame(indices: [], rows: []) } @@ -700,6 +725,7 @@ public enum DFCSV { return DFDataFrame(indices: indices, rows: rows) } + /// Parses CSV text into a numeric data frame, coercing non-numeric cells to `0`. public static func parseDoubles(_ csv: String) -> DFDataFrame { let df = parse(csv) let rows = df.rows.map { row in @@ -712,6 +738,7 @@ public enum DFCSV { return DFDataFrame(indices: df.indices, rows: rows) } + /// Serializes a string-valued data frame into CSV text. public static func toCSV(_ df: DFDataFrame) -> String { guard let first = df.rows.first else { return "" } let columns = Array(first.keys) @@ -724,6 +751,7 @@ public enum DFCSV { return lines.joined(separator: "\n") } + /// Serializes a numeric data frame into CSV text. public static func toCSV(_ df: DFDataFrame) -> String { guard let first = df.rows.first else { return "" } let columns = Array(first.keys) diff --git a/BacktestingKit/Models/V2/BKV2Models.swift b/BacktestingKit/Models/V2/BKV2Models.swift index c2ab4af..53480ae 100644 --- a/BacktestingKit/Models/V2/BKV2Models.swift +++ b/BacktestingKit/Models/V2/BKV2Models.swift @@ -4,197 +4,329 @@ import Foundation public enum BKV2 { /// Represents `Config` in the BacktestingKit public API. public struct Config: Codable, Equatable, Hashable { + /// Stable identifier for this value. public var id: String = UUID().uuidString + /// Transactions associated with this value. public var transactions: [BKTrade] = [] + /// Active associated with this value. public var active: Bool = false + /// Created associated with this value. public var created: Double = 0 + /// Instrument associated with this value. public var instrument: String = "" + /// Last updated associated with this value. public var last_updated: Double = 0 + /// Configuration associated with this value. public var policyConfig: SimulationPolicyConfig = SimulationPolicyConfig() + /// Analysis associated with this value. public var analysis: BKAnalysis = BKAnalysis() + /// Current status associated with this value. public var status: SimulationStatus = .pending } /// Represents `OptimizeResult` in the BacktestingKit public API. public struct OptimizeResult: Codable, Equatable, Hashable { + /// Stable identifier for this value. public var id: String = UUID().uuidString + /// Result associated with this value. public var result: [Config] = [] + /// Configuration associated with this value. public var policyConfig: OptimizePolicyConfig = OptimizePolicyConfig() + /// Current status associated with this value. public var status: SimulationStatus = .pending + /// Created associated with this value. public var created: Double = 0 + /// Last updated associated with this value. public var last_updated: Double = 0 } /// Represents `Instrument` in the BacktestingKit public API. public struct Instrument: Codable, Equatable, Hashable { + /// Pin associated with this value. public var pin: Bool + /// Exch associated with this value. public var exch: String + /// Exch disp associated with this value. public var exchDisp: String + /// Name associated with this value. public var name: String + /// Type associated with this value. public var type: String + /// Type disp associated with this value. public var typeDisp: String + /// Stable identifier for this value. public var id: String = UUID().uuidString + /// Instrument associated with this value. public var instrument: String = "" + /// Configuration associated with this value. public var config_history: [Config] = [] + /// Optimize history associated with this value. public var optimize_history: [OptimizeResult] = [] + /// Last updated associated with this value. public var last_updated: Double = 0 } /// Represents `FIRUser` in the BacktestingKit public API. public struct FIRUser: Codable, Equatable, Hashable { + /// Identifier associated with this value. public var user_id: String = "TEST_WITH_DEFAULT_USER_ID" + /// Tier associated with this value. public var tier: TierSet = .standard + /// Instruments associated with this value. public var instruments: [String] = [] + /// Devices associated with this value. public var devices: String = "" + /// Last updated associated with this value. public var last_updated: Double = 0 + /// Stable identifier for this value. public var id: String = UUID().uuidString } /// Represents `User` in the BacktestingKit public API. public struct User: Codable, Equatable, Hashable { + /// Identifier associated with this value. public var user_id: String = "TEST_WITH_DEFAULT_USER_ID" + /// Tier associated with this value. public var tier: TierSet = .standard + /// Instruments associated with this value. public var instruments: [Instrument] = [] + /// Devices associated with this value. public var devices: [Device] = [] + /// Last updated associated with this value. public var last_updated: Double = 0 + /// Stable identifier for this value. public var id: String = UUID().uuidString } /// Represents `Device` in the BacktestingKit public API. public struct Device: Codable, Equatable, Hashable { + /// Stable identifier for this value. public var id: String = UUID().uuidString + /// Identifier associated with this value. public var user_id: String = "TEST_WITH_DEFAULT_USER_ID" + /// Identifier associated with this value. public var device_id: String = "" + /// Device type associated with this value. public var device_type: String = "" + /// Last updated associated with this value. public var last_updated: Double = 0 } /// Represents `BKTrade` in the BacktestingKit public API. public struct BKTrade: Codable, Equatable, Hashable { + /// Direction associated with this value. public var direction: TradeDirection + /// Entry time associated with this value. public var entryTime: String + /// Entry price associated with this value. public var entryPrice: Double + /// Exit time associated with this value. public var exitTime: String + /// Exit price associated with this value. public var exitPrice: Double + /// Profit associated with this value. public var profit: Double + /// Profit percentage associated with this value. public var profitPct: Double + /// Growth associated with this value. public var growth: Double + /// Risk percentage associated with this value. public var riskPct: Double? + /// Rmultiple associated with this value. public var rmultiple: Double? + /// Risk series associated with this value. public var riskSeries: [BKTimestampedValue]? + /// Holding period associated with this value. public var holdingPeriod: Double + /// Exit reason associated with this value. public var exitReason: String + /// Stop price associated with this value. public var stopPrice: Double? + /// Stop price series associated with this value. public var stopPriceSeries: [BKTimestampedValue]? + /// Profit target associated with this value. public var profitTarget: Double? } /// Represents `BKTimestampedValue` in the BacktestingKit public API. public struct BKTimestampedValue: Codable, Equatable, Hashable { + /// Timestamp associated with this value. public var time: String + /// Value associated with this value. public var value: Double } /// Represents `BKAnalysis` in the BacktestingKit public API. public struct BKAnalysis: Codable, Equatable, Hashable { + /// Starting capital associated with this value. public var startingCapital: Double = 0 + /// Final capital associated with this value. public var finalCapital: Double = 0 + /// Profit associated with this value. public var profit: Double = 0 + /// Profit percentage associated with this value. public var profitPct: Double = 0 + /// Growth associated with this value. public var growth: Double = 0 + /// Total trades represented by this value. public var totalTrades: Double = 0 + /// Number of bars represented by this value. public var barCount: Double = 0 + /// Maximum drawdown associated with this value. public var maxDrawdown: Double = 0 + /// Maximum drawdown percentage associated with this value. public var maxDrawdownPct: Double = 0 + /// Maximum risk percentage associated with this value. public var maxRiskPct: Double? = 0 + /// Expectency associated with this value. public var expectency: Double? = 0 + /// Rmultiple standard deviation associated with this value. public var rmultipleStdDev: Double? = 0 + /// System quality associated with this value. public var systemQuality: Double? = 0 + /// Profit factor associated with this value. public var profitFactor: Double? = 0 + /// Proportion profitable associated with this value. public var proportionProfitable: Double = 0 + /// Percent profitable associated with this value. public var percentProfitable: Double = 0 + /// Return on account represented by this value. public var returnOnAccount: Double = 0 + /// Average profit per trade associated with this value. public var averageProfitPerTrade: Double = 0 + /// Number winning trades represented by this value. public var numWinningTrades: Double = 0 + /// Number losing trades represented by this value. public var numLosingTrades: Double = 0 + /// Average winning trade associated with this value. public var averageWinningTrade: Double = 0 + /// Average losing trade associated with this value. public var averageLosingTrade: Double = 0 + /// Expected value associated with this value. public var expectedValue: Double = 0 + /// Backtest maximum down draw associated with this value. public var BKMaxDownDraw: Double = 0 + /// Backtest maximum down draw percentage associated with this value. public var BKMaxDownDrawPct: Double = 0 } /// Represents `SimulationPolicyConfig` in the BacktestingKit public API. public struct SimulationPolicyConfig: Codable, Equatable, Hashable { + /// Policy associated with this value. public var policy: SimulationPolicy = .custom + /// Trailing stop loss associated with this value. public var trailingStopLoss: Bool = true + /// Stop loss figure associated with this value. public var stopLossFigure: Double = 15 + /// Profit factor associated with this value. public var profitFactor: Double = pow(2, 30) + /// Rules associated with this value. public var entryRules: [SimulationRule] = [] + /// Rules associated with this value. public var exitRules: [SimulationRule] = [] + /// T1 associated with this value. public var t1: Double = 2 + /// T2 associated with this value. public var t2: Double = 6 } /// Represents `SimulationRule` in the BacktestingKit public API. public struct SimulationRule: Codable, Equatable, Hashable { + /// Indicator one name associated with this value. public var indicatorOneName: String = UUID().uuidString.replacing("-", with: "g") + /// Indicator one type associated with this value. public var indicatorOneType: TechnicalIndicators = .sma + /// Indicator one figure associated with this value. public var indicatorOneFigure: [Int] = [15, 0, 0] + /// Compare associated with this value. public var compare: CompareOption = .largerOrEqualTo + /// Indicator two name associated with this value. public var indicatorTwoName: String = UUID().uuidString.replacing("-", with: "g") + /// Indicator two type associated with this value. public var indicatorTwoType: TechnicalIndicators = .close + /// Indicator two figure associated with this value. public var indicatorTwoFigure: [Int] = [0, 0, 0] } /// Represents `OptimizePolicyConfig` in the BacktestingKit public API. public struct OptimizePolicyConfig: Codable, Equatable, Hashable { + /// Step size associated with this value. public var stepSize: Int = 1 + /// Simple policy associated with this value. public var simplePolicy: Bool = true + /// Trailing stop loss associated with this value. public var trailingStopLoss: Bool = true + /// Stop loss figure associated with this value. public var stopLossFigure: Double = 15 + /// Profit factor associated with this value. public var profitFactor: Double = pow(2, 30) + /// Rules associated with this value. public var entryRules: [OptimizeRule] = [] + /// Rules associated with this value. public var exitRules: [OptimizeRule] = [] + /// T1 associated with this value. public var t1: Double = 2 + /// T2 associated with this value. public var t2: Double = 6 } /// Represents `OptimizeRule` in the BacktestingKit public API. public struct OptimizeRule: Codable, Equatable, Hashable { + /// Indicator one name associated with this value. public var indicatorOneName: String = UUID().uuidString.replacing("-", with: "g") + /// Indicator one type associated with this value. public var indicatorOneType: TechnicalIndicators = .sma + /// Indicator one figure lower associated with this value. public var indicatorOneFigureLower: [Int] = [3, 0, 0] + /// Indicator one figure upper associated with this value. public var indicatorOneFigureUpper: [Int] = [150, 0, 0] + /// Compare associated with this value. public var compare: CompareOption = .largerOrEqualTo + /// Indicator two name associated with this value. public var indicatorTwoName: String = UUID().uuidString.replacing("-", with: "g") + /// Indicator two type associated with this value. public var indicatorTwoType: TechnicalIndicators = .close + /// Indicator two figure lower associated with this value. public var indicatorTwoFigureLower: [Int] = [0, 0, 0] + /// Indicator two figure upper associated with this value. public var indicatorTwoFigureUpper: [Int] = [0, 0, 0] } /// Represents `DynamodbTrigger` in the BacktestingKit public API. public struct DynamodbTrigger: Codable, Equatable, Hashable { + /// Stable identifier for this value. public var uuid: String = UUID().uuidString + /// Type associated with this value. public var type: TriggerType + /// Item ID associated with this value. public var itemId: String + /// Optimize associated with this value. public var optimize: [OptimizePolicyConfig] + /// Simulate associated with this value. public var simulate: [SimulationPolicyConfig] + /// Instrument associated with this value. public var instrument: String + /// Identifier associated with this value. public var user_id: String } /// Represents `SimulateConfigOutput` in the BacktestingKit public API. public struct SimulateConfigOutput: Codable, Hashable { + /// Analysis associated with this value. public var analysis: BKAnalysis + /// Trades associated with this value. public var trades: [BKTrade] + /// Configuration associated with this value. public var config: SimulationPolicyConfig } /// Represents `OptimizeConfigOutput` in the BacktestingKit public API. public struct OptimizeConfigOutput: Codable, Hashable { + /// Analysis associated with this value. public var analysis: BKAnalysis + /// Trades associated with this value. public var trades: [BKTrade] + /// Configuration associated with this value. public var config: SimulationPolicyConfig } diff --git a/BacktestingKit/Models/V3/BKV3InstrumentDetail.swift b/BacktestingKit/Models/V3/BKV3InstrumentDetail.swift index ca33583..9628998 100644 --- a/BacktestingKit/Models/V3/BKV3InstrumentDetail.swift +++ b/BacktestingKit/Models/V3/BKV3InstrumentDetail.swift @@ -2,58 +2,111 @@ import Foundation /// Represents `BKV3_InstrumentDetail` in the BacktestingKit public API. public struct BKV3_InstrumentDetail: Codable, Equatable { + /// Symbol associated with this value. public var Symbol: String + /// Asset type associated with this value. public var AssetType: String + /// Name associated with this value. public var Name: String + /// Description associated with this value. public var Description: String + /// CIK associated with this value. public var CIK: Int + /// Exchange associated with this value. public var Exchange: String + /// Currency associated with this value. public var Currency: String + /// Country associated with this value. public var Country: String + /// Sector associated with this value. public var Sector: String + /// Industry associated with this value. public var Industry: String + /// Address associated with this value. public var Address: String + /// Official site associated with this value. public var OfficialSite: String + /// Fiscal year end associated with this value. public var FiscalYearEnd: String + /// Latest quarter associated with this value. public var LatestQuarter: String + /// Market capitalization associated with this value. public var MarketCapitalization: Double + /// EBITDA associated with this value. public var EBITDA: Double + /// Price-to-earnings ratio associated with this value. public var PERatio: Double + /// PEG ratio associated with this value. public var PEGRatio: Double + /// Book value associated with this value. public var BookValue: Double + /// Dividend per share associated with this value. public var DividendPerShare: Double + /// Dividend yield associated with this value. public var DividendYield: Double + /// EPS associated with this value. public var EPS: Double + /// Revenue per share trailing twelve month associated with this value. public var RevenuePerShareTTM: Double + /// Profit margin associated with this value. public var ProfitMargin: Double + /// Operating margin trailing twelve month associated with this value. public var OperatingMarginTTM: Double + /// Return on assets trailing twelve month associated with this value. public var ReturnOnAssetsTTM: Double + /// Return on equity trailing twelve month associated with this value. public var ReturnOnEquityTTM: Double + /// Revenue trailing twelve month associated with this value. public var RevenueTTM: Double + /// Gross profit trailing twelve month associated with this value. public var GrossProfitTTM: Double + /// Diluted epsttm associated with this value. public var DilutedEPSTTM: Double + /// Quarterly earnings growth year-over-year associated with this value. public var QuarterlyEarningsGrowthYOY: Double + /// Quarterly revenue growth year-over-year associated with this value. public var QuarterlyRevenueGrowthYOY: Double + /// Analyst target price associated with this value. public var AnalystTargetPrice: Double + /// Analyst rating strong buy associated with this value. public var AnalystRatingStrongBuy: Double + /// Analyst rating buy associated with this value. public var AnalystRatingBuy: Double + /// Analyst rating hold associated with this value. public var AnalystRatingHold: Double + /// Analyst rating sell associated with this value. public var AnalystRatingSell: Double + /// Analyst rating strong sell associated with this value. public var AnalystRatingStrongSell: Double + /// Trailing price-to-earnings associated with this value. public var TrailingPE: Double + /// Forward price-to-earnings associated with this value. public var ForwardPE: Double + /// Price to sales ratio trailing twelve month associated with this value. public var PriceToSalesRatioTTM: Double + /// Price to book ratio associated with this value. public var PriceToBookRatio: Double + /// Enterprise value to revenue associated with this value. public var EVToRevenue: Double + /// Enterprise value to EBITDA associated with this value. public var EVToEBITDA: Double + /// Beta associated with this value. public var Beta: Double + /// 52 week high associated with this value. public var _52WeekHigh: Double + /// 52 week low associated with this value. public var _52WeekLow: Double + /// 50 day moving average associated with this value. public var _50DayMovingAverage: Double + /// 200 day moving average associated with this value. public var _200DayMovingAverage: Double + /// Shares outstanding associated with this value. public var SharesOutstanding: Double + /// Dividend date associated with this value. public var DividendDate: String + /// Ex dividend date associated with this value. public var ExDividendDate: String + /// Information associated with this value. public var Information: String enum CodingKeys: String, CodingKey { @@ -64,6 +117,7 @@ public struct BKV3_InstrumentDetail: Codable, Equatable { case _200DayMovingAverage = "200DayMovingAverage" } + /// Performs a field-by-field equality check across the decoded Alpha Vantage payload. public static func == (lhs: BKV3_InstrumentDetail, rhs: BKV3_InstrumentDetail) -> Bool { if lhs.Symbol != rhs.Symbol || lhs.AssetType != rhs.AssetType || lhs.Name != rhs.Name || lhs.Description != rhs.Description || lhs.CIK != rhs.CIK || lhs.Exchange != rhs.Exchange || lhs.Currency != rhs.Currency || lhs.Country != rhs.Country || lhs.Sector != rhs.Sector || lhs.Industry != rhs.Industry { return false diff --git a/BacktestingKit/Models/V3/BKV3Models.swift b/BacktestingKit/Models/V3/BKV3Models.swift index 2bab1f0..4c0cc6e 100644 --- a/BacktestingKit/Models/V3/BKV3Models.swift +++ b/BacktestingKit/Models/V3/BKV3Models.swift @@ -2,35 +2,65 @@ import Foundation /// Represents `BKV3_AnalysisProfile` in the BacktestingKit public API. public struct BKV3_AnalysisProfile: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Starting capital associated with this value. public var startingCapital: Double? + /// Final capital associated with this value. public var finalCapital: Double? + /// Profit associated with this value. public var profit: Double? + /// Profit percentage associated with this value. public var profitPct: Double? + /// Growth associated with this value. public var growth: Double? + /// Total trades represented by this value. public var totalTrades: Double? + /// Number of bars represented by this value. public var barCount: Double? + /// Maximum drawdown associated with this value. public var maxDrawdown: Double? + /// Maximum drawdown percentage associated with this value. public var maxDrawdownPct: Double? + /// Maximum risk percentage associated with this value. public var maxRiskPct: Double? + /// Expectency associated with this value. public var expectency: Double? + /// Rmultiple standard deviation associated with this value. public var rmultipleStdDev: Double? + /// System quality associated with this value. public var systemQuality: Double? + /// Profit factor associated with this value. public var profitFactor: Double? + /// Proportion profitable associated with this value. public var proportionProfitable: Double? + /// Percent profitable associated with this value. public var percentProfitable: Double? + /// Return on account represented by this value. public var returnOnAccount: Double? + /// Average profit per trade associated with this value. public var averageProfitPerTrade: Double? + /// Number winning trades represented by this value. public var numWinningTrades: Double? + /// Number losing trades represented by this value. public var numLosingTrades: Double? + /// Average winning trade associated with this value. public var averageWinningTrade: Double? + /// Average losing trade associated with this value. public var averageLosingTrade: Double? + /// Expected value associated with this value. public var expectedValue: Double? + /// Maximum down draw new associated with this value. public var maxDownDrawNew: Double? + /// Maximum down draw percentage new associated with this value. public var maxDownDrawPctNew: Double? + /// Created at associated with this value. public var createdAt: Date? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Configuration associated with this value. public var configId: String? + /// Instrument ID associated with this value. public var instrumentId: String? enum CodingKeys: String, CodingKey { @@ -69,19 +99,33 @@ public struct BKV3_AnalysisProfile: Codable, Equatable { /// Represents `BKV3_Config` in the BacktestingKit public API. public struct BKV3_Config: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Active associated with this value. public var active: Bool? + /// Created at associated with this value. public var createdAt: Date? + /// Instrument ID associated with this value. public var instrumentId: String? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Current status associated with this value. public var status: String? + /// Optimize result ID associated with this value. public var optimizeResultId: String? + /// Policy associated with this value. public var policy: SimulationPolicy? + /// Last status associated with this value. public var lastStatus: PositionStatus? + /// Trailing stop loss associated with this value. public var trailingStopLoss: Bool? + /// Stop loss figure associated with this value. public var stopLossFigure: Double? + /// Profit factor associated with this value. public var profitFactor: Double? + /// T1 associated with this value. public var t1: Double? + /// T2 associated with this value. public var t2: Double? enum CodingKeys: String, CodingKey { @@ -104,11 +148,17 @@ public struct BKV3_Config: Codable, Equatable { /// Represents `BKV3_InstrumentInfo` in the BacktestingKit public API. public struct BKV3_InstrumentInfo: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Name associated with this value. public var name: String? + /// Exchange code associated with this value. public var exchange: String? + /// Quote type associated with this value. public var quoteType: String? + /// Created at associated with this value. public var createdAt: Date? + /// Last updated associated with this value. public var lastUpdated: Date? enum CodingKeys: String, CodingKey { @@ -123,17 +173,29 @@ public struct BKV3_InstrumentInfo: Codable, Equatable { /// Represents `BKV3_OptimizePolicyConfig` in the BacktestingKit public API. public struct BKV3_OptimizePolicyConfig: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Step size associated with this value. public var stepSize: Int? + /// Simple policy associated with this value. public var simplePolicy: Bool? + /// Trailing stop loss associated with this value. public var trailingStopLoss: Bool? + /// Stop loss figure associated with this value. public var stopLossFigure: Double? + /// Profit factor associated with this value. public var profitFactor: Double? + /// T1 associated with this value. public var t1: Double? + /// T2 associated with this value. public var t2: Double? + /// Created at associated with this value. public var createdAt: Date? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Optimize result ID associated with this value. public var optimizeResultId: String? + /// Instrument ID associated with this value. public var instrumentId: String? enum CodingKeys: String, CodingKey { @@ -154,10 +216,15 @@ public struct BKV3_OptimizePolicyConfig: Codable, Equatable { /// Represents `BKV3_OptimizeResult` in the BacktestingKit public API. public struct BKV3_OptimizeResult: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Current status associated with this value. public var status: String? + /// Created at associated with this value. public var createdAt: Date? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Instrument ID associated with this value. public var instrumentId: String? enum CodingKeys: String, CodingKey { @@ -171,29 +238,53 @@ public struct BKV3_OptimizeResult: Codable, Equatable { /// Represents `BKV3_OptimizeRule` in the BacktestingKit public API. public struct BKV3_OptimizeRule: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Indicator one name associated with this value. public var indicatorOneName: String? + /// Indicator one type associated with this value. public var indicatorOneType: String? + /// Indicator one figure lower one associated with this value. public var indicatorOneFigureLowerOne: Double? + /// Indicator one figure lower two associated with this value. public var indicatorOneFigureLowerTwo: Double? + /// Indicator one figure lower three associated with this value. public var indicatorOneFigureLowerThree: Double? + /// Indicator one figure upper one associated with this value. public var indicatorOneFigureUpperOne: Double? + /// Indicator one figure upper two associated with this value. public var indicatorOneFigureUpperTwo: Double? + /// Indicator one figure upper three associated with this value. public var indicatorOneFigureUpperThree: Double? + /// Compare associated with this value. public var compare: String? + /// Indicator two name associated with this value. public var indicatorTwoName: String? + /// Indicator two type associated with this value. public var indicatorTwoType: String? + /// Indicator two figure lower one associated with this value. public var indicatorTwoFigureLowerOne: Double? + /// Indicator two figure lower two associated with this value. public var indicatorTwoFigureLowerTwo: Double? + /// Indicator two figure lower three associated with this value. public var indicatorTwoFigureLowerThree: Double? + /// Indicator two figure upper one associated with this value. public var indicatorTwoFigureUpperOne: Double? + /// Indicator two figure upper two associated with this value. public var indicatorTwoFigureUpperTwo: Double? + /// Indicator two figure upper three associated with this value. public var indicatorTwoFigureUpperThree: Double? + /// Created at associated with this value. public var createdAt: Date? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Configuration associated with this value. public var optimizePolicyConfigId: String? + /// Rule type associated with this value. public var ruleType: RuleType? + /// Instrument ID associated with this value. public var instrumentId: String? + /// Optimize result ID associated with this value. public var optimizeResultId: String? enum CodingKeys: String, CodingKey { @@ -226,14 +317,23 @@ public struct BKV3_OptimizeRule: Codable, Equatable { /// Represents `BKV3_RiskProfile` in the BacktestingKit public API. public struct BKV3_RiskProfile: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Series type associated with this value. public var seriesType: String? + /// Timestamp associated with this value. public var time: String? + /// Value associated with this value. public var value: Double? + /// Created at associated with this value. public var createdAt: Date? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Trade entry ID associated with this value. public var tradeEntryId: String? + /// Instrument ID associated with this value. public var instrumentId: String? + /// Configuration associated with this value. public var configId: String? enum CodingKeys: String, CodingKey { @@ -251,22 +351,39 @@ public struct BKV3_RiskProfile: Codable, Equatable { /// Represents `BKV3_SimulationRule` in the BacktestingKit public API. public struct BKV3_SimulationRule: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Indicator one name associated with this value. public var indicatorOneName: String? + /// Indicator one type associated with this value. public var indicatorOneType: String? + /// Indicator one figure one associated with this value. public var indicatorOneFigureOne: Double? + /// Indicator one figure two associated with this value. public var indicatorOneFigureTwo: Double? + /// Indicator one figure three associated with this value. public var indicatorOneFigureThree: Double? + /// Compare associated with this value. public var compare: String? + /// Indicator two name associated with this value. public var indicatorTwoName: String? + /// Indicator two type associated with this value. public var indicatorTwoType: String? + /// Indicator two figure one associated with this value. public var indicatorTwoFigureOne: Double? + /// Indicator two figure two associated with this value. public var indicatorTwoFigureTwo: Double? + /// Indicator two figure three associated with this value. public var indicatorTwoFigureThree: Double? + /// Created at associated with this value. public var createdAt: Date? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Configuration associated with this value. public var configId: String? + /// Instrument ID associated with this value. public var instrumentId: String? + /// Rule type associated with this value. public var ruleType: RuleType? enum CodingKeys: String, CodingKey { @@ -298,24 +415,43 @@ public enum RuleType: String, CaseIterable, Codable { /// Represents `BKV3_TradeEntry` in the BacktestingKit public API. public struct BKV3_TradeEntry: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Direction associated with this value. public var direction: String? + /// Entry time associated with this value. public var entryTime: String? + /// Entry price associated with this value. public var entryPrice: Double? + /// Exit time associated with this value. public var exitTime: String? + /// Exit price associated with this value. public var exitPrice: Double? + /// Profit associated with this value. public var profit: Double? + /// Profit percentage associated with this value. public var profitPct: Double? + /// Growth associated with this value. public var growth: Double? + /// Risk percentage associated with this value. public var riskPct: Double? + /// Rmultiple associated with this value. public var rmultiple: Double? + /// Holding period associated with this value. public var holdingPeriod: Double? + /// Exit reason associated with this value. public var exitReason: String? + /// Stop price associated with this value. public var stopPrice: Double? + /// Profit target associated with this value. public var profitTarget: Double? + /// Configuration associated with this value. public var configId: String? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Created at associated with this value. public var createdAt: Date? + /// Instrument ID associated with this value. public var instrumentId: String? enum CodingKeys: String, CodingKey { @@ -343,10 +479,15 @@ public struct BKV3_TradeEntry: Codable, Equatable { /// Represents `BKV3_UserDevice` in the BacktestingKit public API. public struct BKV3_UserDevice: Codable, Equatable { + /// User ID associated with this value. public var userId: String? + /// Device ID associated with this value. public var deviceId: String? + /// Device type associated with this value. public var deviceType: DeviceType? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Created at associated with this value. public var createdAt: Date? enum CodingKeys: String, CodingKey { @@ -366,10 +507,15 @@ public enum DeviceType: String, CaseIterable, Codable { /// Represents `BKV3_UserProfile` in the BacktestingKit public API. public struct BKV3_UserProfile: Codable, Equatable { + /// User ID associated with this value. public var userId: UUID + /// User ID text associated with this value. public var userIdText: String? + /// Tier associated with this value. public var tier: TierSet? + /// Last updated associated with this value. public var lastUpdated: Date? + /// Created at associated with this value. public var createdAt: Date? enum CodingKeys: String, CodingKey { @@ -383,12 +529,19 @@ public struct BKV3_UserProfile: Codable, Equatable { /// Represents `BKV3_UserSubscription` in the BacktestingKit public API. public struct BKV3_UserSubscription: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// Created at associated with this value. public var createdAt: Date + /// Last updated associated with this value. public var lastUpdated: Date + /// Configuration associated with this value. public var configId: String + /// Pin associated with this value. public var pin: Bool + /// User ID associated with this value. public var userId: String + /// Instrument ID associated with this value. public var instrumentId: String enum CodingKeys: String, CodingKey { @@ -404,10 +557,15 @@ public struct BKV3_UserSubscription: Codable, Equatable { /// Represents `BKV3_UserDeletion` in the BacktestingKit public API. public struct BKV3_UserDeletion: Codable, Equatable { + /// Stable identifier for this value. public var id: String + /// User ID associated with this value. public var userId: String + /// Email associated with this value. public var email: String + /// Last updated associated with this value. public var lastUpdated: Date? + /// Created at associated with this value. public var createdAt: Date? enum CodingKeys: String, CodingKey { diff --git a/BacktestingKit/Simulation/V2/BKV2SimulationDriver.swift b/BacktestingKit/Simulation/V2/BKV2SimulationDriver.swift index 47a5612..20c8dd3 100644 --- a/BacktestingKit/Simulation/V2/BKV2SimulationDriver.swift +++ b/BacktestingKit/Simulation/V2/BKV2SimulationDriver.swift @@ -1,5 +1,6 @@ import Foundation +/// Adapter that runs legacy v2 simulations from a `BKRawCsvProvider`. public final class BKV2SimulationDriver: BKV2SimulationDriving { private let csvProvider: BKRawCsvProvider private let barParser: any BKBarParsing diff --git a/BacktestingKit/Simulation/V3/BKSimulation.swift b/BacktestingKit/Simulation/V3/BKSimulation.swift index f4638e0..f3cfa44 100644 --- a/BacktestingKit/Simulation/V3/BKSimulation.swift +++ b/BacktestingKit/Simulation/V3/BKSimulation.swift @@ -2,8 +2,11 @@ import Foundation /// Represents `V3SimulateConfigOutput` in the BacktestingKit public API. public struct V3SimulateConfigOutput: Codable, Equatable { + /// Analysis associated with this value. public var analysis: BKAnalysis + /// Trades associated with this value. public var trades: [BKTrade] + /// Configuration associated with this value. public var config: BKV3_Config /// Creates a new instance. diff --git a/BacktestingKit/Simulation/V3/BKSimulationDriver.swift b/BacktestingKit/Simulation/V3/BKSimulationDriver.swift index b11a0e7..8734344 100644 --- a/BacktestingKit/Simulation/V3/BKSimulationDriver.swift +++ b/BacktestingKit/Simulation/V3/BKSimulationDriver.swift @@ -9,6 +9,7 @@ public enum BKSimulationDriverError: LocalizedError, Equatable { case emptyBars(String) case invalidConcurrency(Int) + /// Localized description of the error. public var errorDescription: String? { switch self { case .emptyInstrumentID: @@ -33,7 +34,9 @@ public enum BKEngineErrorCode: String, Codable, Equatable { /// Represents `BKSimulationRunFailure` in the BacktestingKit public API. public struct BKSimulationRunFailure: Error, Equatable { + /// Identifier associated with this value. public var instrumentID: String + /// Human-readable message associated with this value. public var message: String /// Creates a new instance. @@ -45,13 +48,21 @@ public struct BKSimulationRunFailure: Error, Equatable { /// Represents `BKEngineFailure` in the BacktestingKit public API. public struct BKEngineFailure: Error, Equatable, Codable { + /// Identifier associated with this value. public var instrumentID: String + /// Machine-readable code associated with this value. public var code: BKEngineErrorCode + /// Current processing stage associated with this value. public var stage: String + /// Human-readable message associated with this value. public var message: String + /// Whether is retryable. public var isRetryable: Bool + /// Timestamp associated with this value. public var timestamp: Date + /// Additional metadata associated with this value. public var metadata: [String: String] + /// Recovery suggestion associated with this value. public var recoverySuggestion: String? /// Creates a new instance. @@ -78,10 +89,15 @@ public struct BKEngineFailure: Error, Equatable, Codable { /// Represents `BKSimulationBatchOptions` in the BacktestingKit public API. public struct BKSimulationBatchOptions: Equatable, Codable { + /// Maximum concurrency associated with this value. public var maxConcurrency: Int + /// Continue on failure associated with this value. public var continueOnFailure: Bool + /// Whether to use streaming CSV parser. public var useStreamingCsvParser: Bool + /// Strict CSV parsing associated with this value. public var strictCsvParsing: Bool + /// CSV column mapping associated with this value. public var csvColumnMapping: BKCSVColumnMapping? /// Creates a new instance. @@ -102,11 +118,17 @@ public struct BKSimulationBatchOptions: Equatable, Codable { /// Represents `BKSimulationBatchReport` in the BacktestingKit public API. public struct BKSimulationBatchReport: Equatable, Codable { + /// Total instruments represented by this value. public var totalInstruments: Int + /// Succeeded associated with this value. public var succeeded: Int + /// Failed associated with this value. public var failed: Int + /// Elapsed ms associated with this value. public var elapsedMS: Double + /// Memory delta bytes associated with this value. public var memoryDeltaBytes: UInt64? + /// Failures associated with this value. public var failures: [BKEngineFailure] /// Creates a new instance. @@ -136,8 +158,11 @@ public enum BKCSVParserMode: String, Codable, Equatable { /// Represents `BKSimulationExecutionOptions` in the BacktestingKit public API. public struct BKSimulationExecutionOptions: Equatable, Codable { + /// Parser mode associated with this value. public var parserMode: BKCSVParserMode + /// Maximum bars per instrument associated with this value. public var maxBarsPerInstrument: Int? + /// CSV column mapping associated with this value. public var csvColumnMapping: BKCSVColumnMapping? /// Creates a new instance. @@ -154,10 +179,15 @@ public struct BKSimulationExecutionOptions: Equatable, Codable { /// Represents `BKSimulationInstrumentReport` in the BacktestingKit public API. public struct BKSimulationInstrumentReport: Equatable, Codable { + /// Identifier associated with this value. public var instrumentID: String + /// Elapsed ms associated with this value. public var elapsedMS: Double + /// Configuration associated with this value. public var configCountProcessed: Int + /// Number of trades represented by this value. public var tradeCount: Int + /// Risk point count represented by this value. public var riskPointCount: Int /// Creates a new instance. @@ -178,10 +208,15 @@ public struct BKSimulationInstrumentReport: Equatable, Codable { /// Represents `BKSimulationProgress` in the BacktestingKit public API. public struct BKSimulationProgress: Equatable, Codable { + /// Completed associated with this value. public var completed: Int + /// Total associated with this value. public var total: Int + /// Succeeded associated with this value. public var succeeded: Int + /// Failed associated with this value. public var failed: Int + /// Identifier associated with this value. public var lastInstrumentID: String /// Creates a new instance. @@ -202,7 +237,9 @@ public struct BKSimulationProgress: Equatable, Codable { /// Represents `BKSimulationBatchDetailedReport` in the BacktestingKit public API. public struct BKSimulationBatchDetailedReport: Equatable, Codable { + /// High-level summary associated with this value. public var summary: BKSimulationBatchReport + /// Instrument reports associated with this value. public var instrumentReports: [BKSimulationInstrumentReport] /// Creates a new instance. @@ -214,7 +251,9 @@ public struct BKSimulationBatchDetailedReport: Equatable, Codable { /// Represents `BKSimulationBatchRunHandle` in the BacktestingKit public API. public struct BKSimulationBatchRunHandle { + /// Progress updates emitted while the batch run is active. public var progressStream: AsyncStream + /// Task that resolves to the final batch report. public var resultTask: Task /// Creates a new instance. @@ -227,6 +266,7 @@ public struct BKSimulationBatchRunHandle { } } +/// Default v3 simulation driver that coordinates data loading, parsing, and persistence. public final class BKSimulationDriver: BKV3SimulationDriving { private let dataStore: BKV3DataStore private let csvProvider: BKRawCsvProvider diff --git a/BacktestingKit/Support/BKCoders.swift b/BacktestingKit/Support/BKCoders.swift index 63de769..d520465 100644 --- a/BacktestingKit/Support/BKCoders.swift +++ b/BacktestingKit/Support/BKCoders.swift @@ -2,12 +2,14 @@ import Foundation /// Represents `BKCoders` in the BacktestingKit public API. public enum BKCoders { + /// Builds a JSON decoder configured for ISO-8601 date decoding. public static func iso8601Decoder() -> JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder } + /// Builds a JSON encoder configured for ISO-8601 date encoding. public static func iso8601Encoder() -> JSONEncoder { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 diff --git a/BacktestingKit/Support/BKPresentationErrorAdapter.swift b/BacktestingKit/Support/BKPresentationErrorAdapter.swift index af948e4..44a5174 100644 --- a/BacktestingKit/Support/BKPresentationErrorAdapter.swift +++ b/BacktestingKit/Support/BKPresentationErrorAdapter.swift @@ -11,18 +11,22 @@ public struct BKAnyPresentationError: BKUserPresentableError { self.wrapped = base as? BKUserPresentableError } + /// Short title used when presenting this value to people. public var uiTitle: String { wrapped?.uiTitle ?? "Engine Error" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { wrapped?.uiSummary ?? (base as NSError).localizedDescription } + /// Detailed description used when presenting this value to people. public var uiDescription: String { wrapped?.uiDescription ?? String(describing: base) } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { var metadata = wrapped?.uiMetadata ?? [:] let nsError = base as NSError @@ -31,10 +35,12 @@ public struct BKAnyPresentationError: BKUserPresentableError { return metadata } + /// Stable error code used for presentation and diagnostics. public var uiErrorCode: String { wrapped?.uiErrorCode ?? "unknown_error" } + /// Whether the operation can be retried safely. public var uiRetryable: Bool { wrapped?.uiRetryable ?? false } diff --git a/BacktestingKit/Support/BKPresentationErrorConformances.swift b/BacktestingKit/Support/BKPresentationErrorConformances.swift index 157458f..f015695 100644 --- a/BacktestingKit/Support/BKPresentationErrorConformances.swift +++ b/BacktestingKit/Support/BKPresentationErrorConformances.swift @@ -2,8 +2,11 @@ import Foundation /// UI presentation mapping for top-level engine failures. extension BKEngineFailure: BKUserPresentableError { + /// Short title used when presenting this value to people. public var uiTitle: String { "Simulation Error" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { message } + /// Detailed description used when presenting this value to people. public var uiDescription: String { var parts: [String] = [] parts.append("[\(code.rawValue)] stage=\(stage)") @@ -13,6 +16,7 @@ extension BKEngineFailure: BKUserPresentableError { } return parts.joined(separator: " | ") } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { var base: [String: String] = [ "instrumentID": instrumentID, @@ -26,18 +30,25 @@ extension BKEngineFailure: BKUserPresentableError { } return base } + /// Stable error code used for presentation and diagnostics. public var uiErrorCode: String { code.rawValue } + /// Whether the operation can be retried safely. public var uiRetryable: Bool { isRetryable } } /// UI presentation mapping for CSV parsing failures. extension BKCSVParsingError: BKUserPresentableError { + /// Short title used when presenting this value to people. public var uiTitle: String { "CSV Parsing Error" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { errorDescription ?? String(describing: self) } + /// Detailed description used when presenting this value to people. public var uiDescription: String { uiSummary } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { ["kind": String(describing: self)] } + /// Stable error code used for presentation and diagnostics. public var uiErrorCode: String { switch self { case .missingHeader: return "missing_header" @@ -53,10 +64,15 @@ extension BKCSVParsingError: BKUserPresentableError { /// UI presentation mapping for AlphaVantage provider failures. extension AlphaVantageClientError: BKUserPresentableError { + /// Short title used when presenting this value to people. public var uiTitle: String { "Data Provider Error" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { errorDescription ?? String(describing: self) } + /// Detailed description used when presenting this value to people. public var uiDescription: String { uiSummary } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { ["provider": "alphavantage"] } + /// Stable error code used for presentation and diagnostics. public var uiErrorCode: String { switch self { case .invalidTicker: return "invalid_ticker" @@ -69,6 +85,7 @@ extension AlphaVantageClientError: BKUserPresentableError { case .emptyResponse: return "empty_response" } } + /// Whether the operation can be retried safely. public var uiRetryable: Bool { switch self { case .throttled, .badStatusCode: @@ -81,10 +98,15 @@ extension AlphaVantageClientError: BKUserPresentableError { /// UI presentation mapping for quick demo failures. extension BKQuickDemoError: BKUserPresentableError { + /// Short title used when presenting this value to people. public var uiTitle: String { "Demo Error" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { errorDescription ?? String(describing: self) } + /// Detailed description used when presenting this value to people. public var uiDescription: String { uiSummary } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { ["surface": "quick_demo"] } + /// Stable error code used for presentation and diagnostics. public var uiErrorCode: String { switch self { case .missingBundledCSV: return "missing_bundled_csv" @@ -96,10 +118,15 @@ extension BKQuickDemoError: BKUserPresentableError { /// UI presentation mapping for simulation input/runtime validation failures. extension BKSimulationDriverError: BKUserPresentableError { + /// Short title used when presenting this value to people. public var uiTitle: String { "Simulation Input Error" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { errorDescription ?? String(describing: self) } + /// Detailed description used when presenting this value to people. public var uiDescription: String { uiSummary } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { ["source": "driver"] } + /// Stable error code used for presentation and diagnostics. public var uiErrorCode: String { switch self { case .emptyInstrumentID: return "empty_instrument_id" diff --git a/BacktestingKit/Support/BKPresentationSuccessConformances.swift b/BacktestingKit/Support/BKPresentationSuccessConformances.swift index 9d225d3..e2ea78b 100644 --- a/BacktestingKit/Support/BKPresentationSuccessConformances.swift +++ b/BacktestingKit/Support/BKPresentationSuccessConformances.swift @@ -2,13 +2,17 @@ import Foundation /// UI presentation mapping for per-instrument simulation reports. extension BKSimulationInstrumentReport: BKUserPresentablePayload { + /// Short title used when presenting this value to people. public var uiTitle: String { "Instrument Simulation Report" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { "\(instrumentID): \(configCountProcessed) configs, \(tradeCount) trades, \(riskPointCount) risk points" } + /// Detailed description used when presenting this value to people. public var uiDescription: String { "\(uiSummary), elapsed \(elapsedMS.formatted(.number.precision(.fractionLength(2)))) ms" } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { [ "instrumentID": instrumentID, @@ -22,13 +26,17 @@ extension BKSimulationInstrumentReport: BKUserPresentablePayload { /// UI presentation mapping for batch summary reports. extension BKSimulationBatchReport: BKUserPresentablePayload { + /// Short title used when presenting this value to people. public var uiTitle: String { "Batch Simulation Report" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { "total \(totalInstruments), succeeded \(succeeded), failed \(failed)" } + /// Detailed description used when presenting this value to people. public var uiDescription: String { "\(uiSummary), elapsed \(elapsedMS.formatted(.number.precision(.fractionLength(2)))) ms" } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { [ "totalInstruments": String(totalInstruments), @@ -43,11 +51,15 @@ extension BKSimulationBatchReport: BKUserPresentablePayload { /// UI presentation mapping for batch detailed reports. extension BKSimulationBatchDetailedReport: BKUserPresentablePayload { + /// Short title used when presenting this value to people. public var uiTitle: String { "Detailed Batch Simulation Report" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { summary.uiSummary } + /// Detailed description used when presenting this value to people. public var uiDescription: String { "\(summary.uiDescription), instrumentReports=\(instrumentReports.count)" } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { var details = summary.uiMetadata details["instrumentReports"] = String(instrumentReports.count) @@ -57,13 +69,17 @@ extension BKSimulationBatchDetailedReport: BKUserPresentablePayload { /// UI presentation mapping for offline demo summaries. extension BKQuickDemoSummary: BKUserPresentablePayload { + /// Short title used when presenting this value to people. public var uiTitle: String { "Quick Demo Summary" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { "\(symbol): \(barCount) bars, trades \(result.numTrades), return \((result.totalReturn * 100).formatted(.number.precision(.fractionLength(2))))%" } + /// Detailed description used when presenting this value to people. public var uiDescription: String { return "\(uiSummary), range \(dateRangeStart.ISO8601Format()) to \(dateRangeEnd.ISO8601Format())" } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { return [ "symbol": symbol, @@ -80,11 +96,15 @@ extension BKQuickDemoSummary: BKUserPresentablePayload { /// UI presentation mapping for v2 simulation output payloads. extension BKV2.SimulateConfigOutput: BKUserPresentablePayload { + /// Short title used when presenting this value to people. public var uiTitle: String { "V2 Simulation Output" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { "\(trades.count) trades generated" } + /// Detailed description used when presenting this value to people. public var uiDescription: String { "Policy \(config.policy.rawValue), trades \(trades.count), profit \(analysis.profit.formatted(.number.precision(.fractionLength(2))))" } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { [ "tradeCount": String(trades.count), @@ -97,8 +117,12 @@ extension BKV2.SimulateConfigOutput: BKUserPresentablePayload { /// UI presentation mapping for position status values. extension PositionStatus: BKUserPresentablePayload { + /// Short title used when presenting this value to people. public var uiTitle: String { "Position Status" } + /// One-line summary used when presenting this value to people. public var uiSummary: String { rawValue } + /// Detailed description used when presenting this value to people. public var uiDescription: String { "Simulation ended with status '\(rawValue)'." } + /// Structured metadata associated with this value. public var uiMetadata: [String: String] { ["status": rawValue] } } diff --git a/BacktestingKit/Tools/BKExportTool.swift b/BacktestingKit/Tools/BKExportTool.swift index 78f36db..678206f 100644 --- a/BacktestingKit/Tools/BKExportTool.swift +++ b/BacktestingKit/Tools/BKExportTool.swift @@ -5,6 +5,7 @@ public enum BKExportError: LocalizedError, Equatable { case encodingFailed(String) case emptyData(String) + /// Localized description of the error. public var errorDescription: String? { switch self { case .encodingFailed(let message): diff --git a/BacktestingKit/Tools/BKToolHelperModels.swift b/BacktestingKit/Tools/BKToolHelperModels.swift index 1c72c3b..11163d6 100644 --- a/BacktestingKit/Tools/BKToolHelperModels.swift +++ b/BacktestingKit/Tools/BKToolHelperModels.swift @@ -2,12 +2,19 @@ import Foundation /// Aggregated output from a helper-driven CSV preflight workflow. public struct BKToolPreflightReport: Codable, Equatable, Sendable { + /// Ticker symbol associated with this value. public var symbol: String? + /// Number of rows represented by this value. public var rowCount: Int? + /// Start date represented by this value. public var startDate: Date? + /// End date represented by this value. public var endDate: Date? + /// Validation output associated with this value. public var validation: BKValidationReport + /// Diagnostics associated with this value. public var diagnostics: [BKDiagnosticEvent] + /// Whether this value is ready for the next workflow step. public var isReady: Bool /// Creates a new instance. @@ -32,8 +39,11 @@ public struct BKToolPreflightReport: Codable, Equatable, Sendable { /// Export-ready text payloads emitted by helper workflows. public struct BKToolExportBundle: Codable, Equatable, Sendable { + /// Preflight json associated with this value. public var preflightJSON: String + /// Diagnostics json associated with this value. public var diagnosticsJSON: String? + /// Trades CSV associated with this value. public var tradesCSV: String? /// Creates a new instance. @@ -50,10 +60,15 @@ public struct BKToolExportBundle: Codable, Equatable, Sendable { /// Aggregated diagnostics snapshot suitable for smoke-test and export workflows. public struct BKDiagnosticsSnapshotReport: Codable, Equatable, Sendable { + /// Event count represented by this value. public var eventCount: Int + /// First timestamp associated with this value. public var firstTimestamp: Date? + /// Last timestamp associated with this value. public var lastTimestamp: Date? + /// Stage counts associated with this value. public var stageCounts: [String: Int] + /// Last failure event associated with this value. public var lastFailureEvent: BKDiagnosticEvent? /// Creates a new instance. @@ -74,9 +89,13 @@ public struct BKDiagnosticsSnapshotReport: Codable, Equatable, Sendable { /// Export-ready payloads for completed runs and smoke-test artifacts. public struct BKRunExportBundle: Codable, Equatable, Sendable { + /// Summary json associated with this value. public var summaryJSON: String + /// Diagnostics summary json associated with this value. public var diagnosticsSummaryJSON: String? + /// Trades CSV associated with this value. public var tradesCSV: String? + /// Scenario json associated with this value. public var scenarioJSON: String? /// Creates a new instance. @@ -93,16 +112,50 @@ public struct BKRunExportBundle: Codable, Equatable, Sendable { } } +/// Export-ready payloads for additive portfolio runs. +public struct BKPortfolioExportBundle: Codable, Equatable, Sendable { + /// Aggregate portfolio JSON associated with this value. + public var portfolioJSON: String + /// Sleeve allocation CSV associated with this value. + public var weightsCSV: String? + /// Failure payload JSON associated with this value. + public var failuresJSON: String? + /// Rebalance event payload JSON associated with this value. + public var rebalanceJSON: String? + + /// Creates a new instance. + public init( + portfolioJSON: String, + weightsCSV: String? = nil, + failuresJSON: String? = nil, + rebalanceJSON: String? = nil + ) { + self.portfolioJSON = portfolioJSON + self.weightsCSV = weightsCSV + self.failuresJSON = failuresJSON + self.rebalanceJSON = rebalanceJSON + } +} + /// Metric-by-metric delta between two run summaries. public struct BKRunMetricDiff: Codable, Equatable, Sendable { + /// Baseline associated with this value. public var baseline: BKRunHeadlineMetrics + /// Candidate associated with this value. public var candidate: BKRunHeadlineMetrics + /// Trade count delta associated with this value. public var tradeCountDelta: Int + /// Win rate delta associated with this value. public var winRateDelta: Double + /// Total return delta represented by this value. public var totalReturnDelta: Double + /// Annualized return delta associated with this value. public var annualizedReturnDelta: Double + /// Maximum drawdown delta associated with this value. public var maxDrawdownDelta: Double + /// Sharpe ratio delta associated with this value. public var sharpeRatioDelta: Double + /// Profit factor delta associated with this value. public var profitFactorDelta: Double /// Creates a new instance. @@ -131,12 +184,19 @@ public struct BKRunMetricDiff: Codable, Equatable, Sendable { /// Structured summary diff for onboarding and regression review workflows. public struct BKRunSummaryDiff: Codable, Equatable, Sendable { + /// Baseline associated with this value. public var baseline: BKRunSummary + /// Candidate associated with this value. public var candidate: BKRunSummary + /// Symbol changed associated with this value. public var symbolChanged: Bool + /// Bar count delta associated with this value. public var barCountDelta: Int + /// Start date changed associated with this value. public var startDateChanged: Bool + /// End date changed associated with this value. public var endDateChanged: Bool + /// Headline metrics associated with this value. public var metrics: BKRunMetricDiff /// Creates a new instance. @@ -161,11 +221,17 @@ public struct BKRunSummaryDiff: Codable, Equatable, Sendable { /// Comparison report that flags whether summary differences exceed a caller-supplied tolerance. public struct BKRunComparisonReport: Codable, Equatable, Sendable { + /// Diff associated with this value. public var diff: BKRunSummaryDiff + /// Tolerance associated with this value. public var tolerance: Double + /// Compared field count represented by this value. public var comparedFieldCount: Int + /// Changed field count represented by this value. public var changedFieldCount: Int + /// Materially different fields associated with this value. public var materiallyDifferentFields: [String] + /// Whether is equivalent. public var isEquivalent: Bool /// Creates a new instance. @@ -188,6 +254,7 @@ public struct BKRunComparisonReport: Codable, Equatable, Sendable { /// Error emitted when two summaries are not equivalent within the supplied tolerance. public struct BKComparisonAssertionError: LocalizedError, Equatable, Codable, Sendable { + /// Detailed report associated with this value. public var report: BKRunComparisonReport /// Creates a new instance. @@ -195,6 +262,7 @@ public struct BKComparisonAssertionError: LocalizedError, Equatable, Codable, Se self.report = report } + /// Localized description of the error. public var errorDescription: String? { let fields = report.materiallyDifferentFields.joined(separator: ", ") return "Runs are not equivalent within tolerance \(report.tolerance). Material differences: \(fields)" @@ -203,8 +271,11 @@ public struct BKComparisonAssertionError: LocalizedError, Equatable, Codable, Se /// Validation-style readiness output for synthetic scenario workflows. public struct BKScenarioReadinessReport: Codable, Equatable, Sendable { + /// Configuration associated with this value. public var config: BKScenarioConfig + /// Validation output associated with this value. public var validation: BKValidationReport + /// Whether this value is ready for the next workflow step. public var isReady: Bool /// Creates a new instance. @@ -221,8 +292,11 @@ public struct BKScenarioReadinessReport: Codable, Equatable, Sendable { /// One case inside a scenario smoke suite. public struct BKScenarioSmokeCaseReport: Codable, Equatable, Sendable { + /// Configuration associated with this value. public var config: BKScenarioConfig + /// Readiness associated with this value. public var readiness: BKScenarioReadinessReport + /// High-level summary associated with this value. public var summary: BKRunSummary? /// Creates a new instance. @@ -239,9 +313,13 @@ public struct BKScenarioSmokeCaseReport: Codable, Equatable, Sendable { /// Aggregated output for deterministic scenario smoke suites. public struct BKScenarioSmokeSuiteReport: Codable, Equatable, Sendable { + /// Cases associated with this value. public var cases: [BKScenarioSmokeCaseReport] + /// Passed case count represented by this value. public var passedCaseCount: Int + /// Failed case count represented by this value. public var failedCaseCount: Int + /// Whether the operation completed successfully. public var isSuccessful: Bool /// Creates a new instance. diff --git a/BacktestingKit/Tools/BKToolWorkflowHelpers.swift b/BacktestingKit/Tools/BKToolWorkflowHelpers.swift index f5dee44..bc9c9ea 100644 --- a/BacktestingKit/Tools/BKToolWorkflowHelpers.swift +++ b/BacktestingKit/Tools/BKToolWorkflowHelpers.swift @@ -100,7 +100,7 @@ public enum BKComparisonTool { ) } - /// Throws when two summaries are not equivalent within the supplied tolerance. + /// Throws when two run summaries are not equivalent within the supplied tolerance. @discardableResult public static func assertEquivalent( baseline: BKRunSummary, @@ -175,6 +175,51 @@ public extension BKExportTool { } } + /// Encodes a completed additive portfolio run into portable JSON/CSV artifacts. + static func exportPortfolioRunBundle( + _ report: BKPortfolioRunReport, + prettyPrinted: Bool = true + ) -> Result { + switch toJSON(report, prettyPrinted: prettyPrinted) { + case .failure(let error): + return .failure(error) + case .success(let portfolioJSON): + let weightsCSV = portfolioWeightsToCSV(report.sleeveReports) + let failuresJSON: String? + if report.failures.isEmpty { + failuresJSON = nil + } else { + switch toJSON(report.failures, prettyPrinted: prettyPrinted) { + case .success(let output): + failuresJSON = output + case .failure(let error): + return .failure(error) + } + } + + let rebalanceJSON: String? + if report.rebalanceEvents.isEmpty { + rebalanceJSON = nil + } else { + switch toJSON(report.rebalanceEvents, prettyPrinted: prettyPrinted) { + case .success(let output): + rebalanceJSON = output + case .failure(let error): + return .failure(error) + } + } + + return .success( + BKPortfolioExportBundle( + portfolioJSON: portfolioJSON, + weightsCSV: weightsCSV, + failuresJSON: failuresJSON, + rebalanceJSON: rebalanceJSON + ) + ) + } + } + /// Exports a compact run summary as human-readable Markdown. static func exportMarkdownSummary( _ summary: BKRunSummary, @@ -214,6 +259,78 @@ public extension BKExportTool { return .success(markdown) } + + /// Exports an additive portfolio run as human-readable Markdown. + static func exportPortfolioMarkdownSummary( + _ report: BKPortfolioRunReport, + title: String? = nil + ) -> Result { + guard let summary = report.summary else { + return .failure(.emptyData("portfolio-summary")) + } + + let heading = title ?? "\(report.portfolioID) Portfolio Summary" + let percentStyle = FloatingPointFormatStyle.number.precision(.fractionLength(2)) + let ratioStyle = FloatingPointFormatStyle.number.precision(.fractionLength(4)) + let sleeveLines = report.sleeveReports.map { sleeve in + let totalReturn = ((sleeve.summary?.metrics.totalReturn ?? 0) * 100.0).formatted(percentStyle) + return "- `\(sleeve.symbol)` • status: \(sleeve.status.rawValue) • weight: \((sleeve.resolvedWeight * 100.0).formatted(percentStyle))% • return: \(totalReturn)%" + }.joined(separator: "\n") + + let markdown = """ + # \(heading) + + - Portfolio: `\(report.portfolioID)` + - Sleeves succeeded: \(report.succeededSleeveCount) + - Sleeves failed: \(report.failedSleeveCount) + - Trades: \(summary.metrics.tradeCount) + - Win rate: \((summary.metrics.winRate * 100.0).formatted(percentStyle))% + - Total return: \((summary.metrics.totalReturn * 100.0).formatted(percentStyle))% + - Annualized return: \((summary.metrics.annualizedReturn * 100.0).formatted(percentStyle))% + - Max drawdown: \((summary.metrics.maxDrawdown * 100.0).formatted(percentStyle))% + - Sharpe ratio: \(summary.metrics.sharpeRatio.formatted(ratioStyle)) + - Profit factor: \(summary.metrics.profitFactor.formatted(ratioStyle)) + - Rebalance events: \(report.rebalanceEvents.count) + + ## Sleeves + + \(sleeveLines) + """ + + return .success(markdown) + } +} + +private func portfolioWeightsToCSV(_ sleeveReports: [BKPortfolioSleeveRunReport]) -> String { + var lines = [ + "symbol,preset,status,requestedWeight,resolvedWeight,tradeCount,totalReturn,annualizedReturn,maxDrawdown,sharpeRatio" + ] + lines.reserveCapacity(sleeveReports.count + 1) + + for report in sleeveReports { + let fields: [String] = [ + report.symbol, + report.preset.rawValue, + report.status.rawValue, + report.requestedWeight.map { String($0) } ?? "", + String(report.resolvedWeight), + String(report.summary?.metrics.tradeCount ?? 0), + String(report.summary?.metrics.totalReturn ?? 0), + String(report.summary?.metrics.annualizedReturn ?? 0), + String(report.summary?.metrics.maxDrawdown ?? 0), + String(report.summary?.metrics.sharpeRatio ?? 0), + ] + lines.append(fields.map(bkEscapePortfolioCSVValue).joined(separator: ",")) + } + + return lines.joined(separator: "\n") +} + +private func bkEscapePortfolioCSVValue(_ value: String) -> String { + if value.contains(",") || value.contains("\"") || value.contains("\n") { + return "\"\(value.replacing("\"", with: "\"\""))\"" + } + return value } public extension BKScenarioTool { diff --git a/CHANGELOG.md b/CHANGELOG.md index b55f855..c7f15f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,24 @@ https://keepachangelog.com/en/1.1.0/ ## [Unreleased] -### Planned -- Keep compatibility aliases non-deprecated for the v0.1.x release cycle. +### Added +- Additive portfolio orchestration APIs for app and engine integrators: + - `BKEngine.PortfolioRequest` + - `BKEngine.runPortfolio(...)` + - `BKAppFacade.buildPortfolioCSVImportScreenState(...)` + - `BKAppFacade.runConfirmedPortfolioCSVImport(...)` +- Portfolio result/export helpers for stable JSON, CSV, and Markdown reporting without making export bundles the primary runtime contract. + +### Changed +- Beginner-facing documentation now follows a single guided path: + - `README.md` is the front door + - `docs/ONBOARDING.md` is the canonical markdown tutorial + - `docs/GETTING_STARTED.md` is the quick reference + - `docs/CHOOSE_YOUR_SURFACE.md` is the routing guide + - `docs/INDEX.md` is the documentation map +- DocC tutorial ordering now matches the markdown onboarding flow, with earlier first-success guidance and clearer app-integration routing. +- Onboarding/tutorial examples were updated to use current `BKRunSummary` metric access (`summary.metrics.*`) instead of stale `summary.result.*` examples. +- Compatibility aliases remain non-deprecated for the `v0.1.x` release cycle while canonical docs and examples route new integrations toward the preferred names. ## [0.1.0] - 2026-02-19 diff --git a/README.md b/README.md index 118e129..d05713f 100644 --- a/README.md +++ b/README.md @@ -1,200 +1,106 @@ # BacktestingKit -BacktestingKit is a Swift framework for running strategy backtests and simulation workflows with model parity to an existing JavaScript engine (v2 and v3 paths). +BacktestingKit is a Swift framework for deterministic backtests and simulation workflows with v2 and v3 parity-oriented execution paths. -Current release track: **v0.1.0**. +Current release track: **v0.1.x**. -## Highlights +## Who This Is For -- v2 and v3 simulation engines with parity-oriented data models. -- Strategy/rule evaluation plus indicator generation. -- Built-in analysis, optimization, walk-forward, and Monte Carlo helpers. -- Strict CSV ingestion support: - - ISO8601 date parsing. - - Chronological input enforcement. - - Explicit parse errors for UI handling. - - Custom OHLCV column mapping. -- Batch simulation driver with progress and structured error reporting. -- Data source is provider-driven (`BKRawCsvProvider`), not hard-coded to a single vendor. +- App integrators who want to start with `BKAppFacade` and move to `BKEngine` only when they need lower-level control. +- Engine integrators who want direct v2/v3 request-model execution through `BKEngine`. +- Candle-first workflows that already have normalized `Candlestick` data and want `BacktestingKitManager`. +- Diagnostics/export/parity workflows that need the tool helpers. ## Project Structure - `BacktestingKit/Core` – foundational shared types. - `BacktestingKit/Models` – v2/v3/external model definitions. -- `BacktestingKit/Indicators` – v2/v3 indicator setup. -- `BacktestingKit/Rules` – v2/v3 rule evaluation. -- `BacktestingKit/Simulation` – v2/v3 simulation entrypoints and drivers. - `BacktestingKit/Data` – CSV parsing, providers, cache, AlphaVantage support. -- `BacktestingKit/Analysis` – post-analysis and optimization helpers. -- `BacktestingKit/Engine` – presets, strategy factory, high-level manager APIs. -- `BacktestingKit/Support` – coders/model conversion helpers. +- `BacktestingKit/Simulation` – v2/v3 simulation entrypoints and drivers. +- `BacktestingKit/Engine` – presets, high-level engine APIs, manager workflows. +- `BacktestingKit/Tools` – validation, diagnostics, export, comparison, scenario, and parity helpers. + +Architecture notes live in `BacktestingKit/ARCHITECTURE.md`. -See also: `BacktestingKit/ARCHITECTURE.md` +## Start Here -## Quick Start +- New to the package: [docs/ONBOARDING.md](docs/ONBOARDING.md) +- Want the shortest install/build/run checklist: [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) +- Not sure which API surface to use: [docs/CHOOSE_YOUR_SURFACE.md](docs/CHOOSE_YOUR_SURFACE.md) +- Want the full docs map: [docs/INDEX.md](docs/INDEX.md) +- Want guided interactive tutorials in Xcode: open `BacktestingKit/BacktestingKit.docc` -Open and build: +## First Success + +Build the package: ```bash swift build ``` -Basic CSV parse: +Then run the smallest offline success path: ```swift -let parseResult = csvToBars(csv, reverse: false) -if case .success(let bars) = parseResult { - // use bars -} -``` +import BacktestingKit -Choose your path: - -- Beginner onboarding: `docs/ONBOARDING.md` -- Beginner app facade: `BKAppFacade` -- App-side CSV import/validation: - - Import-review path: `BKAppFacade.buildCSVImportScreenState(...)` - - Confirmed execution handoff: `BKAppFacade.runConfirmedCSVImport(...)` - - Developer diagnostics path: `BKAppFacade.diagnoseCSVImport(...)` - - Manual settings path: `BKAppFacade.inspectCSV(...)`, `previewCSV(...)`, `validateCSVImport(...)`, `normalizeCSVImport(...)`, `runCSVImport(...)` - - Auto-inference path: `BKAppFacade.detectCSVImportSettings(...)`, `previewCSVAuto(...)`, `validateCSVImportAuto(...)`, `normalizeCSVImportAuto(...)`, `runCSVImportAuto(...)` -- Surface guide: `docs/CHOOSE_YOUR_SURFACE.md` -- Offline first run: `BKEngine.runDemo(...)` -- Inline CSV helper flow: `BKEngine.runDemoCSV(...)`, `runV2CSV(...)`, `runV3CSV(...)` -- Canonical app integration: `BKEngine.runV2(...)`, `BKEngine.runV3(...)` -- Candle-first manager workflows: `BacktestingKitManager` -- Validation/export/comparison/parity tools: `BKValidationTool`, `BKExportTool`, `BKComparisonTool`, `BKParityTool` - -App-facing facade example: +let result = BKEngine.runDemo(dataset: .aapl) -```swift -let report = BKAppFacade.runPresetCSVAndExportMarkdown( - symbol: "AAPL", - csv: csv, - preset: .smaCrossover -) - -let importScreen = BKAppFacade.buildCSVImportScreenState( - symbol: "AAPL", - csv: csv, - maxRows: 5 -) - -let confirmedRun = BKAppFacade.runConfirmedCSVImport( - from: importScreen, - csv: csv, - preset: .smaCrossover -) - -let importDiagnostics = BKAppFacade.diagnoseCSVImport( - symbol: "AAPL", - csv: csv -) - -let importPreview = BKAppFacade.previewCSV( - symbol: "AAPL", - csv: csv, - maxRows: 5 -) - -let importAuto = BKAppFacade.runCSVImportAuto( - symbol: "AAPL", - csv: csv, - preset: .smaCrossover -) +switch result { +case .success(let summary): + print(summary.symbol) + print(summary.barCount) + print(summary.metrics.totalReturn) +case .failure(let error): + print(error.localizedDescription) +} ``` -Canonical engine entrypoint: - -```swift -// Use BKEngine as the single source of truth for engine entrypoints. -let v3Result = await BKEngine.runV3(v3Request) -let v2Result = await BKEngine.runV2(v2Request) -``` +Success looks like: -Provider injection example (no vendor lock-in): +- the package builds locally +- bundled CSV resources load without provider wiring +- you get a `BKRunSummary` back and can inspect `summary.metrics.totalReturn` -```swift -let provider = BKCustomCsvProvider { ticker, p1, p2 in - // Fetch CSV from your own backend/vendor and return raw CSV text. - await MyDataGateway.shared.fetchCsv(symbol: ticker, p1: p1, p2: p2) -} -``` +If you want the full beginner path after this, go to [docs/ONBOARDING.md](docs/ONBOARDING.md). -CSV parse with custom headers: +## Choose Your Surface -```swift -let mapping = BKCSVColumnMapping( - date: "Date", - open: "OpenPrice", - high: "HighPrice", - low: "LowPrice", - close: "ClosePrice", - volume: "TradeVolume" -) - -let barsResult = csvToBars(csv, reverse: false, columnMapping: mapping) -``` +- `BKAppFacade` + Start here if your app begins with pasted, uploaded, or reviewed CSV and you want one app-facing namespace. +- `BKEngine` + Use this when you need direct v2/v3 request-model control and provider-driven execution. +- `BacktestingKitManager` + Use this when you already have candles and want indicator bundles, strategy recipes, and summary/report helpers. +- Tool helpers + Use `BKValidationTool`, `BKExportTool`, `BKComparisonTool`, `BKScenarioTool`, and `BKParityTool` for validation, export, comparison, smoke scenarios, and parity checks. -Fastest CSV-backed workflow: +The fuller decision guide is in [docs/CHOOSE_YOUR_SURFACE.md](docs/CHOOSE_YOUR_SURFACE.md). -```swift -let csv = """ -timestamp,open,high,low,close,volume -2024-01-01,100,101,99,100.5,1000000 -2024-01-02,100.5,102,100,101.5,1100000 -""" +## Documentation -let result = BKEngine.runDemoCSV(symbol: "DEMO", csv: csv) -``` +### Beginner Path -Bundled preset smoke workflow: +- [docs/ONBOARDING.md](docs/ONBOARDING.md) – the canonical markdown tutorial from build to app integration +- [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) – the compact quick reference after installation +- [docs/CHOOSE_YOUR_SURFACE.md](docs/CHOOSE_YOUR_SURFACE.md) – the routing guide for `BKAppFacade`, `BKEngine`, manager workflows, and tools -```swift -let summary = BKQuickDemo.runBundledPresetDemo( - dataset: .aapl, - preset: .smaCrossover -) - -let matrix = BKQuickDemo.runBundledSmokeMatrix( - datasets: [.aapl, .msft, .nvda], - preset: .emaMeanReversion -) -``` +### Guided Interactive Tutorials -Manager helper workflow: +- `BacktestingKit/BacktestingKit.docc` – DocC tutorial bundle for install, first success, CSV import, app integration, first backtest, and follow-on workflows -```swift -let manager = BacktestingKitManager() -let trend = manager.applyTrendIndicatorBundle( - candles: candles, - smaPeriods: [5, 20], - emaPeriods: [12, 26], - keyNamespace: "screen" -) - -let summary = manager.runSMACrossoverSummary( - symbol: "AAPL", - candles: candles, - fast: 5, - slow: 20 -) -``` +### Reference and Deep Dives -Tool workflow example: +- [docs/INDEX.md](docs/INDEX.md) – complete documentation map +- [docs/PACKAGE_USAGE_GUIDE.md](docs/PACKAGE_USAGE_GUIDE.md) – workflow-oriented package usage map +- [docs/ENGINE_GUIDE.md](docs/ENGINE_GUIDE.md) – canonical `BKEngine`, drivers, and execution flows +- [docs/DATA_INGESTION.md](docs/DATA_INGESTION.md) – CSV parsing, mappings, providers, and normalization +- [docs/HELPER_WORKFLOWS.md](docs/HELPER_WORKFLOWS.md) – additive helper and façade workflows +- [docs/TOOLS.md](docs/TOOLS.md) – validation, diagnostics, export, comparison, scenario, and parity helpers +- [docs/INDICATORS_STRATEGIES_METRICS.md](docs/INDICATORS_STRATEGIES_METRICS.md) – manager-owned indicator, strategy, and metrics workflows -```swift -let preflight = BKValidationTool.preflightCSV(csv, symbol: "AAPL") -let collector = BKDiagnosticsCollector() -await collector.emit(kind: .simulationStarted, stage: "simulation", message: "Running scenario") -let diagnostics = await collector.summarizedSnapshot() -let exported = BKScenarioTool.runExportBundle( - config: BKScenarioConfig(symbol: "SCENARIO"), - diagnostics: diagnostics -) -``` +## Parity -## Trial / Parity Run +Run the parity check with: ```bash bash tools/parity/run_parity.sh @@ -211,94 +117,6 @@ Expected success output: - `PARITY_OK` - `Parity check passed.` -## Quick Trial Demo - -Run a bundled end-to-end trial using: - -- Symbol: `AAPL` -- Candles: `10y / 1d` -- Strategy: `SMA crossover 5/20` - -```bash -swift run BacktestingKitTrialDemo -``` - -One-liner API (works in apps, tests, and Swift Playground after importing the package): - -```swift -let demoResult = BKEngine.runDemo(dataset: .aapl) -``` - -The demo emits concise step-by-step logs (load sample CSV, parse, run backtest, summarize metrics). - -Bundled demo datasets (10 total across NASDAQ + NYSE): - -- `BacktestingKit/Resources/AAPL_10Y_1D.csv` -- `BacktestingKit/Resources/MSFT_10Y_1D.csv` -- `BacktestingKit/Resources/GOOGL_10Y_1D.csv` -- `BacktestingKit/Resources/NVDA_10Y_1D.csv` -- `BacktestingKit/Resources/TSLA_10Y_1D.csv` -- `BacktestingKit/Resources/AMZN_10Y_1D.csv` -- `BacktestingKit/Resources/JPM_10Y_1D.csv` -- `BacktestingKit/Resources/XOM_10Y_1D.csv` -- `BacktestingKit/Resources/WMT_10Y_1D.csv` -- `BacktestingKit/Resources/KO_10Y_1D.csv` - -## One-Line End-to-End Engine Calls (V2 / V3) - -Run full engine execution in one async call and get either `Result.success` or `Result.failure`. -`BKEngine` is the canonical surface; lower-level drivers are still available. - -V3: - -```swift -let result = await BKEngine.runV3( - .init( - instrument: instrument, - p1: 5, - p2: 20, - dateFormat: "yyyy-MM-dd", - executionOptions: .init(parserMode: .streamingStrict, csvColumnMapping: nil), - dataStore: dataStore, - csvProvider: csvProvider, - log: { print($0) } - ) -) -``` - -V2: - -```swift -let result = await BKEngine.runV2( - .init( - instrumentID: "AAPL", - config: v2Config, - p1: 5, - p2: 20, - dateFormat: "yyyy-MM-dd", - csvColumnMapping: nil, - csvProvider: csvProvider, - log: { print($0) } - ) -) -``` - -## Detailed Docs - -- `docs/ONBOARDING.md` -- `docs/PACKAGE_USAGE_GUIDE.md` -- `docs/GETTING_STARTED.md` -- `docs/INDEX.md` -- `docs/HELPER_WORKFLOWS.md` -- `docs/ENGINE_GUIDE.md` -- `docs/DATA_INGESTION.md` -- `docs/INDICATORS_STRATEGIES_METRICS.md` -- `docs/TOOLS.md` -- `docs/PARITY_TESTING.md` -- `docs/AGENTIC_USAGE.md` -- `docs/OPEN_SOURCE_MAINTAINERS.md` -- `docs/RELEASE_CHECKLIST.md` - ## Open Source Metadata - License: `LICENSE` (MIT) diff --git a/ROADMAP.md b/ROADMAP.md index 30139a0..b81f680 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,6 @@ ## Kanban ### Backlog -- [ ] Portfolio multi-instrument orchestration API - [ ] Result export API (JSON/CSV) - [ ] Structured logging context model - [ ] Mobile memory budget guardrails @@ -23,6 +22,7 @@ - [ ] None ### Done +- [x] Portfolio multi-instrument orchestration API - [x] Canonical engine entrypoint (`BKEngine`) - [x] One-liner API (v2/v3) - [x] Typed `Result` error model (`BKEngineFailure`) @@ -49,7 +49,7 @@ - [x] UI presentation contracts for payloads/errors (`BKUserPresentablePayload`, `BKUserPresentableError`, `BKResultPresentation`) ### Release -- [ ] API naming audit -- [ ] DocC final pass -- [ ] CI matrix finalization -- [ ] `v0.1.x` release notes +- [x] API naming audit +- [x] DocC final pass +- [x] CI matrix finalization +- [x] `v0.1.x` release notes diff --git a/Tests/BacktestingKitTests/BacktestingKitAppFacadeTests.swift b/Tests/BacktestingKitTests/BacktestingKitAppFacadeTests.swift index d0abb31..2b25797 100644 --- a/Tests/BacktestingKitTests/BacktestingKitAppFacadeTests.swift +++ b/Tests/BacktestingKitTests/BacktestingKitAppFacadeTests.swift @@ -452,6 +452,168 @@ final class BacktestingKitAppFacadeTests: XCTestCase { XCTAssertEqual(report.run.summary?.symbol, "AAPL") } + func testBuildPortfolioCSVImportScreenStateReturnsReadyWhenAllSleevesAreReady() { + let state = BKAppFacade.buildPortfolioCSVImportScreenState( + portfolioID: "BASKET", + sleeves: [ + BKAppPortfolioImportItem(symbol: "AAPL", csv: inlineCsv, preset: .smaCrossover, targetWeight: 0.6), + BKAppPortfolioImportItem(symbol: "MSFT", csv: inlineCsv, preset: .emaCrossover, targetWeight: 0.4), + ], + allocation: .sleeveWeights, + maxRows: 2 + ) + + XCTAssertEqual(state.portfolioID, "BASKET") + XCTAssertEqual(state.status, .ready) + XCTAssertTrue(state.isReadyToContinue) + XCTAssertEqual(state.sleeves.count, 2) + XCTAssertEqual(state.sleeves.map(\.screenState.status), [.ready, .ready]) + XCTAssertEqual(state.allocation.mode, .sleeveWeights) + } + + func testBuildPortfolioCSVImportScreenStateReturnsInvalidWhenAnySleeveIsInvalid() { + let state = BKAppFacade.buildPortfolioCSVImportScreenState( + sleeves: [ + BKAppPortfolioImportItem(symbol: "AAPL", csv: inlineCsv, preset: .smaCrossover), + BKAppPortfolioImportItem(symbol: "BROKEN", csv: "", preset: .smaCrossover), + ] + ) + + XCTAssertEqual(state.portfolioID, "PORTFOLIO") + XCTAssertEqual(state.status, .invalid) + XCTAssertFalse(state.isReadyToContinue) + XCTAssertEqual(state.sleeves.count, 2) + XCTAssertTrue(state.issues.contains(where: { $0.symbol == "BROKEN" })) + } + + func testBuildPortfolioCSVImportScreenStateRejectsDuplicateSleeves() { + let state = BKAppFacade.buildPortfolioCSVImportScreenState( + portfolioID: "DUPES", + sleeves: [ + BKAppPortfolioImportItem(symbol: "AAPL", csv: inlineCsv, preset: .smaCrossover), + BKAppPortfolioImportItem(symbol: "AAPL", csv: inlineCsv, preset: .emaCrossover), + ], + allocation: .sleeveWeights, + maxRows: 2 + ) + + XCTAssertEqual(state.status, .invalid) + XCTAssertFalse(state.isReadyToContinue) + XCTAssertTrue(state.issues.contains(where: { + $0.symbol == "DUPES" + && $0.title == "Portfolio" + && $0.items.contains(where: { $0.code == "portfolio_duplicate_symbols" }) + })) + } + + func testBuildPortfolioCSVImportScreenStateRejectsExplicitWeightCountMismatch() { + let state = BKAppFacade.buildPortfolioCSVImportScreenState( + portfolioID: "MISMATCH", + sleeves: [ + BKAppPortfolioImportItem(symbol: "AAPL", csv: inlineCsv, preset: .smaCrossover), + BKAppPortfolioImportItem(symbol: "MSFT", csv: inlineCsv, preset: .emaCrossover), + ], + allocation: .explicit([1.0]), + maxRows: 2 + ) + + XCTAssertEqual(state.status, .invalid) + XCTAssertFalse(state.isReadyToContinue) + XCTAssertTrue(state.issues.contains(where: { + $0.symbol == "MISMATCH" + && $0.title == "Portfolio" + && $0.items.contains(where: { $0.code == "portfolio_explicit_weight_count_mismatch" }) + })) + } + + func testBuildPortfolioCSVImportScreenStateRejectsExplicitWeightTotalThatIsNotPositive() { + let state = BKAppFacade.buildPortfolioCSVImportScreenState( + portfolioID: "ZERO_TOTAL", + sleeves: [ + BKAppPortfolioImportItem(symbol: "AAPL", csv: inlineCsv, preset: .smaCrossover), + BKAppPortfolioImportItem(symbol: "MSFT", csv: inlineCsv, preset: .emaCrossover), + ], + allocation: .explicit([0, -1]), + maxRows: 2 + ) + + XCTAssertEqual(state.status, .invalid) + XCTAssertFalse(state.isReadyToContinue) + XCTAssertTrue(state.issues.contains(where: { + $0.symbol == "ZERO_TOTAL" + && $0.title == "Portfolio" + && $0.items.contains(where: { + $0.code == "portfolio_explicit_weight_total_invalid" + && $0.message.localizedStandardContains("positive total") + }) + })) + } + + func testRunConfirmedPortfolioCSVImportUsesInferredSleeveSettings() { + let screenState = BKAppFacade.buildPortfolioCSVImportScreenState( + portfolioID: "AUTO", + sleeves: [ + BKAppPortfolioImportItem(symbol: "AAPL", csv: descendingCsv, preset: .smaCrossover, targetWeight: 0.7), + BKAppPortfolioImportItem(symbol: "MSFT", csv: inlineCsv, preset: .smaCrossover, targetWeight: 0.3), + ], + allocation: .sleeveWeights, + maxRows: 2 + ) + + let report = BKAppFacade.runConfirmedPortfolioCSVImport(from: screenState) + + XCTAssertTrue(report.run.isSuccessful) + XCTAssertFalse(report.run.isPartialSuccess) + XCTAssertEqual(report.confirmedSettingsBySymbol.count, 0) + XCTAssertEqual(report.run.succeededSleeveCount, 2) + XCTAssertEqual(report.run.failedSleeveCount, 0) + XCTAssertEqual(report.run.sleeveReports.count, 2) + XCTAssertEqual(report.run.sleeveReports[0].resolvedWeight, 0.7, accuracy: 1e-9) + XCTAssertEqual(report.run.sleeveReports[1].resolvedWeight, 0.3, accuracy: 1e-9) + XCTAssertEqual( + report.run.sleeveReports.first?.summary?.startDate, + ISO8601DateFormatter().date(from: "2024-01-01T00:00:00Z") + ) + } + + func testRunConfirmedPortfolioCSVImportAllowsPerSleeveOverrides() { + let screenState = BKAppFacade.buildPortfolioCSVImportScreenState( + portfolioID: "OVERRIDES", + sleeves: [ + BKAppPortfolioImportItem(symbol: "AAPL", csv: ambiguousDateCsv, preset: .smaCrossover), + BKAppPortfolioImportItem(symbol: "MSFT", csv: inlineCsv, preset: .smaCrossover), + ], + allocation: .explicit([0.5, 0.5]), + maxRows: 2 + ) + let mapping = BKCSVColumnMapping( + date: "date", + open: "open", + high: "high", + low: "low", + close: "close", + volume: "volume" + ) + + let report = BKAppFacade.runConfirmedPortfolioCSVImport( + from: screenState, + confirmedSettingsBySymbol: [ + "AAPL": BKAppCSVConfirmedImportSettings( + columnMapping: mapping, + dateFormat: "yyyy-MM-dd", + reverse: false + ) + ] + ) + + XCTAssertTrue(report.run.isSuccessful) + XCTAssertEqual(report.confirmedSettingsBySymbol["AAPL"]?.columnMapping?.date, "date") + XCTAssertEqual(report.run.succeededSleeveCount, 2) + XCTAssertEqual(report.run.sleeveReports.count, 2) + XCTAssertEqual(report.run.sleeveReports[0].resolvedWeight, 0.5, accuracy: 1e-9) + XCTAssertEqual(report.run.sleeveReports[1].resolvedWeight, 0.5, accuracy: 1e-9) + } + func testAppFacadeRunCSVImportAndExportMarkdownSuccess() { let report = BKAppFacade.runCSVImportAndExportMarkdown( symbol: "AAPL", diff --git a/Tests/BacktestingKitTests/BacktestingKitPortfolioTests.swift b/Tests/BacktestingKitTests/BacktestingKitPortfolioTests.swift new file mode 100644 index 0000000..873a7cc --- /dev/null +++ b/Tests/BacktestingKitTests/BacktestingKitPortfolioTests.swift @@ -0,0 +1,571 @@ +import XCTest +@testable import BacktestingKit + +final class BacktestingKitPortfolioTests: XCTestCase { + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .gregorian) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd" + return formatter + }() + + func testRunPortfolioAggregatesExplicitSleeveWeights() { + let growthSleeve = makeSleeve( + symbol: "GROWTH", + config: BKScenarioConfig( + symbol: "GROWTH", + barCount: 96, + driftPerBar: 0.0010, + volatility: 0.009, + seed: 11, + strategy: .smaCrossover + ) + ) + let valueSleeve = makeSleeve( + symbol: "VALUE", + config: BKScenarioConfig( + symbol: "VALUE", + barCount: 96, + driftPerBar: 0.0004, + volatility: 0.014, + seed: 22, + strategy: .smaCrossover + ) + ) + let individualRuns = [growthSleeve, valueSleeve].map { sleeve in + BKEngine.preflightAndRunCSV( + symbol: sleeve.symbol, + csv: sleeve.csv, + preset: sleeve.preset, + dateFormat: sleeve.dateFormat, + reverse: sleeve.reverse, + columnMapping: sleeve.columnMapping + ) + } + + let portfolio = BKEngine.runPortfolio( + .init( + portfolioID: "MODEL", + sleeves: [growthSleeve, valueSleeve], + allocation: .explicit([0.7, 0.3]) + ) + ) + + XCTAssertTrue(portfolio.isSuccessful) + XCTAssertFalse(portfolio.isPartialSuccess) + XCTAssertEqual(portfolio.succeededSleeveCount, 2) + XCTAssertEqual(portfolio.failedSleeveCount, 0) + assertWeights(portfolio.sleeveReports.map(\.resolvedWeight), equal: [0.7, 0.3]) + + guard let growthSummary = individualRuns[0].summary, + let valueSummary = individualRuns[1].summary, + let summary = portfolio.summary else { + XCTFail("Expected successful sleeve and portfolio summaries") + return + } + + let expected = expectedMetrics( + summaries: [growthSummary, valueSummary], + weights: [0.7, 0.3] + ) + + XCTAssertEqual(summary.symbol, "MODEL") + XCTAssertEqual(summary.barCount, growthSummary.barCount + valueSummary.barCount) + XCTAssertEqual(summary.metrics.tradeCount, expected.tradeCount) + XCTAssertEqual(summary.metrics.winRate, expected.winRate, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.totalReturn, expected.totalReturn, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.annualizedReturn, expected.annualizedReturn, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.maxDrawdown, expected.maxDrawdown, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.sharpeRatio, expected.sharpeRatio, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.profitFactor, expected.profitFactor, accuracy: 1e-12) + } + + func testRunPortfolioRiskParityOverweightsLowerVolSleeve() { + let lowVol = makeSleeve( + symbol: "LOWVOL", + config: BKScenarioConfig( + symbol: "LOWVOL", + barCount: 80, + driftPerBar: 0.0005, + volatility: 0.004, + seed: 1, + strategy: .smaCrossover + ) + ) + let highVol = makeSleeve( + symbol: "HIGHVOL", + config: BKScenarioConfig( + symbol: "HIGHVOL", + barCount: 80, + driftPerBar: 0.0005, + volatility: 0.03, + seed: 2, + strategy: .smaCrossover + ) + ) + + let report = BKEngine.runPortfolio( + .init( + portfolioID: "RISK_PARITY", + sleeves: [lowVol, highVol], + allocation: .riskParity + ) + ) + + XCTAssertTrue(report.isSuccessful) + XCTAssertEqual(report.sleeveReports.count, 2) + let weights = report.sleeveReports.map(\.resolvedWeight) + XCTAssertEqual(weights.reduce(0, +), 1.0, accuracy: 1e-12) + XCTAssertGreaterThan(weights[0], weights[1]) + XCTAssertLessThan( + report.sleeveReports[0].annualizedVolatility ?? .infinity, + report.sleeveReports[1].annualizedVolatility ?? -.infinity + ) + } + + func testRunPortfolioRiskOnRiskOffUsesMomentumPreset() { + let riskOn = makeSleeve( + symbol: "RISKON", + config: BKScenarioConfig( + symbol: "RISKON", + barCount: 96, + driftPerBar: 0.0012, + volatility: 0.01, + seed: 7, + strategy: .smaCrossover + ) + ) + let riskOff = makeSleeve( + symbol: "RISKOFF", + config: BKScenarioConfig( + symbol: "RISKOFF", + barCount: 96, + driftPerBar: -0.0004, + volatility: 0.006, + seed: 8, + strategy: .smaCrossover + ) + ) + + let report = BKEngine.runPortfolio( + .init( + portfolioID: "RISK_SWITCH", + sleeves: [riskOn, riskOff], + allocation: .riskOnRiskOff(riskOnIndex: 0, riskOffIndex: 1) + ) + ) + + XCTAssertTrue(report.isSuccessful) + let riskOnMomentum = report.sleeveReports[0].momentumScore ?? 0 + let riskOffMomentum = report.sleeveReports[1].momentumScore ?? 0 + let expectedWeights = BKPortfolioPresets.riskOnRiskOffWeights( + riskOnMomentum: riskOnMomentum, + riskOffMomentum: riskOffMomentum + ) + assertWeights(report.sleeveReports.map(\.resolvedWeight), equal: expectedWeights) + XCTAssertEqual(report.sleeveReports.reduce(0) { $0 + $1.resolvedWeight }, 1.0, accuracy: 1e-12) + } + + func testRunPortfolioBuildsPeriodicAndManualRebalanceEvents() { + let sleeve = makeSleeve( + symbol: "SLEEVE", + config: BKScenarioConfig( + symbol: "SLEEVE", + barCount: 20, + driftPerBar: 0.0008, + volatility: 0.01, + seed: 5, + strategy: .smaCrossover + ) + ) + + let periodic = BKEngine.runPortfolio( + .init( + portfolioID: "PERIODIC", + sleeves: [sleeve], + allocation: .explicit([1.0]), + rebalancePolicy: .periodic(.weekly) + ) + ) + + XCTAssertTrue(periodic.isSuccessful) + XCTAssertEqual(periodic.rebalanceEvents.count, 2) + XCTAssertEqual(periodic.rebalanceEvents.map(\.source), ["periodic:weekly", "periodic:weekly"]) + + let manualDates = [ + Date(timeIntervalSince1970: 86_400 * 5), + Date(timeIntervalSince1970: 86_400 * 40), + Date(timeIntervalSince1970: 86_400 * 2), + ] + let manual = BKEngine.runPortfolio( + .init( + portfolioID: "MANUAL", + sleeves: [sleeve], + allocation: .explicit([1.0]), + rebalancePolicy: .manual(manualDates) + ) + ) + + XCTAssertTrue(manual.isSuccessful) + XCTAssertEqual(manual.rebalanceEvents.count, 2) + XCTAssertEqual(manual.rebalanceEvents.map(\.source), ["manual", "manual"]) + XCTAssertEqual( + manual.rebalanceEvents.map(\.date), + [ + Date(timeIntervalSince1970: 86_400 * 2), + Date(timeIntervalSince1970: 86_400 * 5), + ] + ) + } + + func testRunPortfolioRecordsPartialFailuresAndSupportsFailFastOverride() { + let validOne = makeSleeve( + symbol: "VALID_A", + config: BKScenarioConfig( + symbol: "VALID_A", + barCount: 64, + driftPerBar: 0.0009, + volatility: 0.01, + seed: 41, + strategy: .smaCrossover + ) + ) + let invalid = BKPortfolioSleeveRequest( + symbol: "BROKEN", + csv: "", + preset: .smaCrossover + ) + let validTwo = makeSleeve( + symbol: "VALID_B", + config: BKScenarioConfig( + symbol: "VALID_B", + barCount: 64, + driftPerBar: 0.0003, + volatility: 0.012, + seed: 42, + strategy: .smaCrossover + ) + ) + + let bestEffort = BKEngine.runPortfolio( + .init( + portfolioID: "BEST_EFFORT", + sleeves: [validOne, invalid, validTwo], + allocation: .explicit([0.4, 0.2, 0.4]), + continueOnFailure: true + ) + ) + + XCTAssertTrue(bestEffort.isSuccessful) + XCTAssertTrue(bestEffort.isPartialSuccess) + XCTAssertEqual(bestEffort.succeededSleeveCount, 2) + XCTAssertEqual(bestEffort.failedSleeveCount, 1) + XCTAssertEqual(bestEffort.sleeveReports.count, 3) + XCTAssertNotNil(bestEffort.summary) + + let failFast = BKEngine.runPortfolio( + .init( + portfolioID: "FAIL_FAST", + sleeves: [validOne, invalid, validTwo], + allocation: .explicit([0.4, 0.2, 0.4]), + continueOnFailure: false + ) + ) + + XCTAssertTrue(failFast.isSuccessful) + XCTAssertTrue(failFast.isPartialSuccess) + XCTAssertEqual(failFast.succeededSleeveCount, 1) + XCTAssertEqual(failFast.failedSleeveCount, 1) + XCTAssertEqual(failFast.sleeveReports.count, 2) + } + + func testRunPortfolioMarksAllocationValidationFailureAsUnsuccessful() { + let first = makeSleeve( + symbol: "FIRST", + config: BKScenarioConfig( + symbol: "FIRST", + barCount: 64, + driftPerBar: 0.0007, + volatility: 0.01, + seed: 101, + strategy: .smaCrossover + ) + ) + let second = makeSleeve( + symbol: "SECOND", + config: BKScenarioConfig( + symbol: "SECOND", + barCount: 64, + driftPerBar: 0.0006, + volatility: 0.012, + seed: 202, + strategy: .smaCrossover + ) + ) + + let report = BKEngine.runPortfolio( + .init( + portfolioID: "INVALID_ALLOC", + sleeves: [first, second], + allocation: .explicit([1.0]) + ) + ) + + XCTAssertFalse(report.isSuccessful) + XCTAssertTrue(report.isPartialSuccess) + XCTAssertEqual(report.succeededSleeveCount, 2) + XCTAssertEqual(report.failedSleeveCount, 0) + XCTAssertNil(report.summary) + XCTAssertTrue(report.failures.contains(where: { $0.stage == "portfolio-allocation" })) + } + + func testRunPortfolioRejectsExplicitWeightsWhenSuccessfulSleevesResolveToNonPositiveTotal() { + let first = makeSleeve( + symbol: "FIRST", + config: BKScenarioConfig( + symbol: "FIRST", + barCount: 64, + driftPerBar: 0.0007, + volatility: 0.01, + seed: 101, + strategy: .smaCrossover + ) + ) + let second = makeSleeve( + symbol: "SECOND", + config: BKScenarioConfig( + symbol: "SECOND", + barCount: 64, + driftPerBar: 0.0006, + volatility: 0.012, + seed: 202, + strategy: .smaCrossover + ) + ) + + let report = BKEngine.runPortfolio( + .init( + portfolioID: "ZERO_TOTAL", + sleeves: [first, second], + allocation: .explicit([0, 0]) + ) + ) + + XCTAssertFalse(report.isSuccessful) + XCTAssertTrue(report.isPartialSuccess) + XCTAssertEqual(report.succeededSleeveCount, 2) + XCTAssertEqual(report.failedSleeveCount, 0) + XCTAssertNil(report.summary) + XCTAssertEqual(report.sleeveReports.map(\.resolvedWeight), [0, 0]) + XCTAssertTrue(report.failures.contains(where: { + $0.stage == "portfolio-allocation" + && $0.message.localizedStandardContains("positive total") + })) + } + + func testRunPortfolioFailFastResolvesSuccessfulSleeveWeightsBeforeFinalization() { + let valid = makeSleeve( + symbol: "VALID", + config: BKScenarioConfig( + symbol: "VALID", + barCount: 64, + driftPerBar: 0.0009, + volatility: 0.01, + seed: 41, + strategy: .smaCrossover + ) + ) + let invalid = BKPortfolioSleeveRequest( + symbol: "BROKEN", + csv: "", + preset: .smaCrossover + ) + let skipped = makeSleeve( + symbol: "SKIPPED", + config: BKScenarioConfig( + symbol: "SKIPPED", + barCount: 64, + driftPerBar: 0.0003, + volatility: 0.012, + seed: 42, + strategy: .smaCrossover + ) + ) + + let standalone = BKEngine.preflightAndRunCSV( + symbol: valid.symbol, + csv: valid.csv, + preset: valid.preset, + dateFormat: valid.dateFormat, + reverse: valid.reverse, + columnMapping: valid.columnMapping + ) + guard let validSummary = standalone.summary else { + XCTFail("Expected standalone valid sleeve summary") + return + } + + let report = BKEngine.runPortfolio( + .init( + portfolioID: "FAIL_FAST_WEIGHTS", + sleeves: [valid, invalid, skipped], + allocation: .explicit([0.4, 0.2, 0.4]), + continueOnFailure: false + ) + ) + + XCTAssertTrue(report.isSuccessful) + XCTAssertTrue(report.isPartialSuccess) + XCTAssertEqual(report.succeededSleeveCount, 1) + XCTAssertEqual(report.failedSleeveCount, 1) + XCTAssertEqual(report.sleeveReports.count, 2) + assertWeights(report.sleeveReports.map(\.resolvedWeight), equal: [1, 0]) + + guard let summary = report.summary else { + XCTFail("Expected fail-fast portfolio summary") + return + } + + XCTAssertEqual(summary.metrics.totalReturn, validSummary.metrics.totalReturn, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.annualizedReturn, validSummary.metrics.annualizedReturn, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.maxDrawdown, validSummary.metrics.maxDrawdown, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.sharpeRatio, validSummary.metrics.sharpeRatio, accuracy: 1e-12) + XCTAssertEqual(summary.metrics.profitFactor, validSummary.metrics.profitFactor, accuracy: 1e-12) + } + + func testExportToolExportsPortfolioRunBundleAndMarkdownSummary() { + let first = makeSleeve( + symbol: "EXPORT_A", + config: BKScenarioConfig( + symbol: "EXPORT_A", + barCount: 64, + driftPerBar: 0.0007, + volatility: 0.009, + seed: 12, + strategy: .smaCrossover + ) + ) + let second = makeSleeve( + symbol: "EXPORT_B", + config: BKScenarioConfig( + symbol: "EXPORT_B", + barCount: 64, + driftPerBar: 0.0002, + volatility: 0.015, + seed: 34, + strategy: .smaCrossover + ) + ) + let report = BKEngine.runPortfolio( + .init( + portfolioID: "EXPORTS", + sleeves: [first, second], + allocation: .explicit([0.6, 0.4]), + rebalancePolicy: .periodic(.monthly) + ) + ) + + guard report.isSuccessful else { + XCTFail("Expected successful portfolio report for export") + return + } + + switch BKExportTool.exportPortfolioRunBundle(report) { + case .success(let bundle): + XCTAssertTrue(bundle.portfolioJSON.localizedStandardContains("\"portfolioID\"")) + XCTAssertTrue(bundle.portfolioJSON.localizedStandardContains("\"EXPORTS\"")) + XCTAssertTrue(bundle.weightsCSV?.localizedStandardContains("symbol,preset,status") == true) + XCTAssertTrue(bundle.weightsCSV?.localizedStandardContains("EXPORT_A") == true) + XCTAssertNil(bundle.failuresJSON) + XCTAssertTrue(bundle.rebalanceJSON?.localizedStandardContains("\"source\"") == true) + case .failure(let error): + XCTFail("Expected portfolio export success, got: \(error)") + } + + switch BKExportTool.exportPortfolioMarkdownSummary(report, title: "Exports Summary") { + case .success(let markdown): + XCTAssertTrue(markdown.localizedStandardContains("# Exports Summary")) + XCTAssertTrue(markdown.localizedStandardContains("`EXPORT_A`")) + XCTAssertTrue(markdown.localizedStandardContains("## Sleeves")) + case .failure(let error): + XCTFail("Expected portfolio markdown export success, got: \(error)") + } + } + + private func makeSleeve( + symbol: String, + config: BKScenarioConfig, + preset: BKPresetCatalog = .smaCrossover, + targetWeight: Double? = nil + ) -> BKPortfolioSleeveRequest { + BKPortfolioSleeveRequest( + symbol: symbol, + csv: makeCSV(from: BKScenarioTool.generateCandles(config: config)), + preset: preset, + targetWeight: targetWeight + ) + } + + private func makeCSV(from candles: [Candlestick]) -> String { + let rows = candles.map { candle in + [ + dateFormatter.string(from: candle.date), + String(candle.open), + String(candle.high), + String(candle.low), + String(candle.close), + String(candle.volume), + ].joined(separator: ",") + } + return (["timestamp,open,high,low,close,volume"] + rows).joined(separator: "\n") + } + + private func expectedMetrics( + summaries: [BKRunSummary], + weights: [Double] + ) -> BKRunHeadlineMetrics { + let totalWeight = weights.reduce(0, +) + let normalizedWeights = weights.map { totalWeight > 0 ? $0 / totalWeight : 0 } + let totalTrades = summaries.reduce(0) { $0 + $1.metrics.tradeCount } + let weightedWinRate: Double + if totalTrades > 0 { + weightedWinRate = zip(summaries, normalizedWeights).reduce(0) { partial, pair in + partial + (Double(pair.0.metrics.tradeCount) * pair.0.metrics.winRate) + } / Double(totalTrades) + } else { + weightedWinRate = zip(summaries, normalizedWeights).reduce(0) { partial, pair in + partial + (pair.0.metrics.winRate * pair.1) + } + } + + func weighted(_ keyPath: KeyPath) -> Double { + zip(summaries, normalizedWeights).reduce(0) { partial, pair in + partial + (pair.0.metrics[keyPath: keyPath] * pair.1) + } + } + + return BKRunHeadlineMetrics( + tradeCount: totalTrades, + winRate: weightedWinRate, + totalReturn: weighted(\.totalReturn), + annualizedReturn: weighted(\.annualizedReturn), + maxDrawdown: weighted(\.maxDrawdown), + sharpeRatio: weighted(\.sharpeRatio), + profitFactor: weighted(\.profitFactor) + ) + } + + private func assertWeights( + _ actual: [Double], + equal expected: [Double], + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertEqual(actual.count, expected.count, file: file, line: line) + for (lhs, rhs) in zip(actual, expected) { + XCTAssertEqual(lhs, rhs, accuracy: 1e-9, file: file, line: line) + } + } +} diff --git a/docs/AGENTIC_USAGE.md b/docs/AGENTIC_USAGE.md index d4e33ad..6ff5253 100644 --- a/docs/AGENTIC_USAGE.md +++ b/docs/AGENTIC_USAGE.md @@ -16,9 +16,9 @@ Agents must keep these non-negotiable rules: Use this sequence for every non-trivial change: 1. Read docs: - - `docs/MODELS_AND_PARITY.md` - - `docs/API_REFERENCE.md` - - `docs/TOOLS.md` + - `MODELS_AND_PARITY.md` + - `API_REFERENCE.md` + - `TOOLS.md` 2. Make change in smallest isolated file set. 3. Run validation: - `swift test` @@ -134,6 +134,6 @@ For PR automation agents: ## Related Docs -- `docs/TOOLS.md` -- `docs/PARITY_TESTING.md` -- `docs/RELEASE_CHECKLIST.md` +- `TOOLS.md` +- `PARITY_TESTING.md` +- `RELEASE_CHECKLIST.md` diff --git a/docs/CHOOSE_YOUR_SURFACE.md b/docs/CHOOSE_YOUR_SURFACE.md index 625a923..64d2096 100644 --- a/docs/CHOOSE_YOUR_SURFACE.md +++ b/docs/CHOOSE_YOUR_SURFACE.md @@ -2,6 +2,8 @@ Use this guide when you are not sure whether to start with `BKAppFacade`, `BKEngine`, `BacktestingKitManager`, or the tool helpers. +If you are brand new to the package, do `ONBOARDING.md` first and come back here once you have one successful run. + ## Start with `BKAppFacade` Use `BKAppFacade` when you are building an app and want the shortest path from user input to a usable backtesting workflow. @@ -22,6 +24,11 @@ Start here first when your app needs: - `runCSVImportAuto(...)` - `runPresetCSVAndExportMarkdown(...)` +Success looks like: + +- your app can render review state, grouped issues, and readiness without reconstructing parsing logic +- the app can move from reviewed CSV to execution in one handoff + ## Drop to `BKEngine` Use `BKEngine` when you need direct request-model control over v2 or v3 execution. @@ -39,6 +46,11 @@ Start here when your app already knows: - the request configuration - whether it wants the v2 or v3 path +Success looks like: + +- your integration owns request construction directly +- you call `await BKEngine.runV3(...)` or `await BKEngine.runV2(...)` and handle the result at the engine boundary + ## Use `BacktestingKitManager` Use `BacktestingKitManager` when you are already working with candles and want candle-first composition. @@ -48,10 +60,13 @@ Choose it for: - indicator bundles - strategy recipes - summary/report building over `Candlestick` arrays -- manager-owned screening and metrics helpers Do not start here for CSV import UI work. Use `BKAppFacade` or `BKEngine` first. +Success looks like: + +- you stay entirely in candle-first workflows without needing import-review or provider wiring + ## Use the Tool Helpers Use the tool helpers when you need validation, diagnostics, export, comparison, scenario generation, or parity workflows. @@ -74,6 +89,10 @@ Choose them for: - deterministic smoke scenarios - parity checks +Success looks like: + +- you get validation, export, comparison, or diagnostics behavior without treating those concerns as part of your main app surface + ## Recommended Order for New Users 1. Start with `BKAppFacade` for app-facing CSV import and preset helpers. diff --git a/docs/ENGINE_GUIDE.md b/docs/ENGINE_GUIDE.md index ea6b62a..5b014fb 100644 --- a/docs/ENGINE_GUIDE.md +++ b/docs/ENGINE_GUIDE.md @@ -137,4 +137,4 @@ case .failure(let error): - Use `BKEngine` for app integration. - Use `BKSimulationDriver`/`BKV2SimulationDriver` when you need low-level orchestration. - Use `BKQuickDemo` for smoke tests, CI sanity checks, and playground validation. -- Use `docs/PACKAGE_USAGE_GUIDE.md` when you want the package organized by workflow rather than by implementation layer. +- Use `PACKAGE_USAGE_GUIDE.md` when you want the package organized by workflow rather than by implementation layer. diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 8770c19..ed89c27 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -1,6 +1,8 @@ # Getting Started -For the linear beginner path, start at `docs/ONBOARDING.md`. For the full documentation map, start at `docs/INDEX.md`. For a complete package usage map, read `docs/PACKAGE_USAGE_GUIDE.md`. +Use this page when you already know you want the shortest path from clone to first successful run. + +If you are new to BacktestingKit, start at `ONBOARDING.md` instead. If you want the full docs map, start at `INDEX.md`. ## Requirements @@ -8,127 +10,72 @@ For the linear beginner path, start at `docs/ONBOARDING.md`. For the full docume - macOS environment with command-line tools installed. - Optional: Node.js + local JS engine checkout for parity checks in `tools/parity`. -## Build +## Build and Verify + +```bash +swift build +swift test +``` + +If you prefer Xcode-based verification: ```bash xcodebuild -scheme BacktestingKit -project BacktestingKit.xcodeproj -configuration Debug build ``` -## Core Entry Points +## Fastest First Success -`BKAppFacade` is the shortest app-facing entrypoint for beginners and integrations. `BKEngine` remains the canonical public entrypoint when you want direct engine request-model control. - -- `BKAppFacade.runPresetCSVAndExportMarkdown(...)` - Run a preset-backed inline CSV workflow and get a human-readable Markdown summary in one call. -- `BKAppFacade.buildCSVImportScreenState(...)` - Build one app-facing import-review payload with inspection, inference, preview, validation, normalization, grouped issues, and readiness. -- `BKAppFacade.runConfirmedCSVImport(...)` - Hand off reviewed CSV import state into execution without reconstructing settings in app code. -- `BKAppFacade.inspectCSV(...)`, `previewCSV(...)`, `validateCSVImport(...)`, `normalizeCSVImport(...)`, `runCSVImport(...)` - App-side CSV import and validation flow for onboarding, import screens, and simple preset execution. -- `BKAppFacade.detectCSVImportSettings(...)`, `previewCSVAuto(...)`, `validateCSVImportAuto(...)`, `normalizeCSVImportAuto(...)`, `runCSVImportAuto(...)` - App-side CSV import flow when the app does not know the settings yet and wants safe inference plus explicit reporting. -- `BKAppFacade.runScenarioAndExportBundle(...)` - Run a deterministic scenario and export a portable bundle for app diagnostics or sample data flows. - -`BKEngine` is the canonical public entrypoint for engine usage. - -- `BKEngine.runV3(...)` for v3 one-shot execution. -- `BKEngine.runV2(...)` for v2 one-shot execution. -- `BKEngine.runDemo(...)` for bundled quick validation. - -Lower-level entrypoints remain available for advanced usage: - -- V3 driver: `BKSimulationDriver` -- V2 driver: `BKV2SimulationDriver` -- V3 simulate: `v3simulateConfig` -- V2 simulate: `v2simulateConfig` - -## Fastest Helper Paths - -For app integration and smoke workflows, prefer the additive helper layer before dropping to raw driver setup. - -- `BKAppFacade.runPresetCSVAndExportMarkdown(...)` - Start from one app-facing helper namespace for execution plus export. -- `BKAppFacade.runCSVImport(...)` - Validate, normalize, and execute user-supplied CSV through a preset in one app-facing path. -- `BKAppFacade.runCSVImportAuto(...)` - Infer safe CSV settings, normalize descending input when needed, and execute without pushing inference logic into app code. -- `BKAppFacade.runConfirmedCSVImport(...)` - Execute from review-state or explicit user-confirmed settings after the import screen step. -- `BKAppFacade.previewCSV(...)` - Build a bounded app-side preview before deciding whether to persist or run imported CSV. -- `BKAppFacade.previewCSVAuto(...)` - Build the same preview when the app needs settings inference first. -- `BKAppFacade.runScenarioAndExportBundle(...)` - Generate a deterministic scenario and package the artifacts for app-facing workflows. -- `BKEngine.runDemoCSV(...)` - Run inline CSV through the built-in SMA crossover demo path. -- `BKEngine.runV2CSV(...)` / `BKEngine.runV3CSV(...)` - Execute v2/v3 requests from inline CSV without building a provider type. -- `BKQuickDemo.runBundledPresetDemo(...)` - Run one bundled dataset through a preset-backed helper flow. -- `BKQuickDemo.runBundledSmokeMatrix(...)` - Execute the same preset across a deterministic demo dataset matrix. -- `BacktestingKitManager.runSMACrossoverSummary(...)` - Produce a compact `BKRunSummary` from a manager-owned strategy workflow. -- `BKValidationTool.preflightCSV(...)` - Validate CSV and capture row count plus date range for onboarding UIs. -- `BKScenarioTool.runExportBundle(...)` - Generate a deterministic scenario, summarize it, and export bundle artifacts. - -## Minimal Helper Workflow +Run the bundled demo path: ```swift -let csv = """ -timestamp,open,high,low,close,volume -2024-01-01,100,101,99,100.5,1000000 -2024-01-02,100.5,102,100,101.5,1100000 -""" - -let preflight = BKValidationTool.preflightCSV(csv, symbol: "AAPL") -guard preflight.isReady else { return } - -let demo = BKEngine.runDemoCSV(symbol: "AAPL", csv: csv) -let importScreen = BKAppFacade.buildCSVImportScreenState( - symbol: "AAPL", - csv: csv, - maxRows: 2 -) -let importPreview = BKAppFacade.previewCSV(symbol: "AAPL", csv: csv, maxRows: 2) -let importAuto = BKAppFacade.previewCSVAuto(symbol: "AAPL", csv: csv, maxRows: 2) - -switch demo { -case .success(let summary): - print(summary.symbol) -case .failure(let error): - print(error.localizedDescription) -} -``` - -If you are new to the package, do the bundled demo onboarding step first in `docs/ONBOARDING.md`, then come back here for the rest of the setup and execution details. +import BacktestingKit -## Minimal V3 Flow +let result = BKEngine.runDemo(dataset: .aapl) +``` -1. Get raw CSV data (`BKRawCsvProvider` or custom provider). -2. Parse to bars (`csvToBars` or `csvToBarsStreaming`). -3. Build rules/config models (`BKV3_*`). -4. Run simulation via `BKSimulationDriver` or direct `v3simulateConfig`. -5. Run post-analysis (`v3postAnalysis`) if needed. +Success looks like: -## Batch Simulation +- the package builds and tests cleanly +- the bundled CSV resources load without external setup +- you can inspect `summary.metrics.totalReturn` from the returned `BKRunSummary` -Use `simulateInstrumentsWithDetailedReport(...)` to get: +You can also run the trial demo from the command line: -- Per-instrument reports. -- Structured failures (`BKEngineFailure`) with error stage/code metadata. -- Progress callback support via `BKSimulationProgress`. +```bash +swift run BacktestingKitTrialDemo +``` -## Notes +## Core Entry Points -- Keep v2/v3 models unchanged if you need strict JavaScript parity. -- For custom sources, normalize CSV before parsing and preserve chronological order. -- Full parity requires a local JS engine path (`../js-engine`, `../algotrade-js-trial`, or `JS_ENGINE_ROOT`). -- The helper workflow catalog is documented in `docs/HELPER_WORKFLOWS.md`. -- The linear onboarding path is documented in `docs/ONBOARDING.md`. -- The full package workflow map is documented in `docs/PACKAGE_USAGE_GUIDE.md`. +Use these as the main top-level surfaces: + +- `BKAppFacade` + Start here for app-facing CSV import/review screens, preset-backed flows, and beginner-friendly integration. +- `BKEngine` + Use this for canonical direct v2/v3 request-model execution and provider-driven data access. +- `BacktestingKitManager` + Use this when you already have candles and want manager-owned indicator, strategy, and report helpers. +- Tool helpers + Use `BKValidationTool`, `BKExportTool`, `BKComparisonTool`, `BKScenarioTool`, and `BKParityTool` for validation, export, comparison, scenarios, and parity. + +## Shortest Routes by Task + +- User CSV -> app UI review state: + `BKAppFacade.buildCSVImportScreenState(...)` +- User CSV -> reviewed execution: + `BKAppFacade.runConfirmedCSVImport(...)` +- Inline CSV -> helper-backed smoke test: + `BKEngine.runDemoCSV(...)` +- Explicit v3 request: + `await BKEngine.runV3(...)` +- Explicit v2 request: + `await BKEngine.runV2(...)` + +## What To Read Next + +- New to the package: `ONBOARDING.md` +- Choosing between surfaces: `CHOOSE_YOUR_SURFACE.md` +- Helper and façade workflows: `HELPER_WORKFLOWS.md` +- Canonical engine flows: `ENGINE_GUIDE.md` +- CSV/provider details: `DATA_INGESTION.md` +- Full documentation map: `INDEX.md` diff --git a/docs/INDEX.md b/docs/INDEX.md index d0e881e..f9d4201 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,46 +1,50 @@ # BacktestingKit Documentation Index -This directory contains implementation-level documentation for the Swift engine. +This directory contains the markdown companion docs for BacktestingKit. Use this page as the map, not as another onboarding guide. -## Start Here +## Pick Your Start -1. `ONBOARDING.md` — first-run beginner path from build to app integration. -2. `PACKAGE_USAGE_GUIDE.md` — complete package usage map by workflow. -3. `CHOOSE_YOUR_SURFACE.md` — decide when to use `BKAppFacade`, `BKEngine`, manager workflows, or tool helpers. -4. `GETTING_STARTED.md` — build/test/run setup and first execution paths, including `BKAppFacade`. -5. `ENGINE_GUIDE.md` — canonical APIs (`BKEngine`, drivers, one-liners). -6. `DATA_INGESTION.md` — CSV parsing rules, custom mappings, provider abstraction. +- New to the package: `ONBOARDING.md` +- Already installed and want the shortest route to first success: `GETTING_STARTED.md` +- Not sure whether to use `BKAppFacade`, `BKEngine`, manager workflows, or tools: `CHOOSE_YOUR_SURFACE.md` +- Want the workflow-oriented package map: `PACKAGE_USAGE_GUIDE.md` +- Want guided interactive tutorials in Xcode: open `../BacktestingKit/BacktestingKit.docc` -## Tutorials and Workflow Guides +## Recommended Beginner Order + +1. `ONBOARDING.md` — the canonical markdown tutorial from build to app integration. +2. `CHOOSE_YOUR_SURFACE.md` — the decision guide once you know the package basically works. +3. `HELPER_WORKFLOWS.md` — helper-first and façade-first app flows. +4. `ENGINE_GUIDE.md` — canonical `BKEngine` request-model execution. +5. `DATA_INGESTION.md` — CSV parsing, mappings, providers, and normalization. +6. `INDICATORS_STRATEGIES_METRICS.md` or `TOOLS.md` — depending on whether you need manager workflows or tooling workflows next. + +## Workflow Guides - `ONBOARDING.md` — the linear beginner path for learning the package. +- `GETTING_STARTED.md` — quickest route from clone to first successful run. - `CHOOSE_YOUR_SURFACE.md` — quick routing guide across the main package surfaces. - `PACKAGE_USAGE_GUIDE.md` — one document that explains when to use each major package surface. -- `GETTING_STARTED.md` — quickest route from clone to first successful run. - `HELPER_WORKFLOWS.md` — additive convenience layer for app and smoke-test workflows, including `BKAppFacade`. - `ENGINE_GUIDE.md` — v2/v3 engine entrypoints, requests, drivers, and batch orchestration. - `TOOLS.md` — validation, diagnostics, export, comparison, benchmark, scenario, and parity helpers. - `INDICATORS_STRATEGIES_METRICS.md` — manager-owned indicators, strategy recipes, analytics, and preset philosophy. -## Deep Dives +## Reference and Deep Dives +- `API_REFERENCE.md` — symbol-oriented API inventory with grouping by module. - `ARCHITECTURE_DEEP_DIVE.md` — module boundaries, layering, and design constraints. - `MODELS_AND_PARITY.md` — v2/v3 parity contract and model compatibility rules. -- `INDICATORS_STRATEGIES_METRICS.md` — built-in indicators, strategy presets, and metrics. - `AGENTIC_USAGE.md` — instructions for using BacktestingKit with agentic coding/automation tools. - `ERROR_HANDLING_AND_DIAGNOSTICS.md` — error taxonomy and UI-facing failure handling. - `PERFORMANCE_AND_MEMORY.md` — memory/performance guidance and optimization levers. - `EXTENDING_BACKTESTINGKIT.md` — extension points for providers, parsers, drivers, and analytics. -- `TOOLS.md` — additive validation/diagnostics/benchmark/parity/scenario/export utilities. -## Operations +## Operations and Release Docs - `../ROADMAP.md` — master checklist for all planned/completed engine work. - `PARITY_TESTING.md` — JS parity runner setup and expectations. - `RELEASE_CHECKLIST.md` — release gate checklist. -- `RELEASE_NOTES_v0.1.0.md` — prepared GitHub release notes for `v0.1.0`. +- `RELEASE_NOTES_v0.1.x.md` — prepared release notes for the current `v0.1.x` release track. +- `RELEASE_NOTES_v0.1.0.md` — historical release notes for the first public release. - `OPEN_SOURCE_MAINTAINERS.md` — maintenance policy and contribution standards. - -## API Inventory - -- `API_REFERENCE.md` — symbol-oriented API inventory with grouping by module. diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index c244370..5b5d973 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -1,19 +1,20 @@ # BacktestingKit Onboarding -This guide is the fastest beginner path through the package. It is written for someone who is new to BacktestingKit and wants one clear route from “it builds” to “I can integrate this into my app”. +This is the canonical markdown tutorial for BacktestingKit. It is written for someone who is new to the package and wants one clear route from “it builds” to “I can integrate this into my app”. -If you want the full docs map instead, start at [INDEX.md](INDEX.md). If you want the workflow-oriented package map, read [PACKAGE_USAGE_GUIDE.md](PACKAGE_USAGE_GUIDE.md). +If you want the full docs map instead, start at [INDEX.md](INDEX.md). If you want guided interactive tutorials in Xcode, open `../BacktestingKit/BacktestingKit.docc`. -## What you will do +## What You Will Finish With -In this onboarding flow you will: +By the end of this tutorial you will have: 1. Build the package locally. 2. Run a bundled demo backtest with no API keys and no external setup. 3. Run the package from inline CSV. -4. Learn the app-facing CSV import/validation flow. -5. Move into the canonical app-facing `BKEngine` integration shape. -6. Branch into the deeper guide that matches your next task. +4. Understand which top-level surface should own your next integration step. +5. Integrate through `BKAppFacade`. +6. Drop to the canonical `BKEngine` path when direct request control matters. +7. Know which deep-dive guide to read next. ## Step 1: Build the package @@ -23,7 +24,19 @@ From the repo root: swift build ``` -If you prefer Xcode-based verification, see [GETTING_STARTED.md](GETTING_STARTED.md). +Success looks like: + +- the package builds cleanly from the repo root +- you are ready to run the bundled demo without any external provider setup + +What you learned: + +- the package can build locally in the simplest supported flow + +Where to go next: + +- continue to Step 2 for the first offline success path +- if you want Xcode-specific build commands, see [GETTING_STARTED.md](GETTING_STARTED.md) ## Step 2: Prove the package works offline @@ -40,18 +53,27 @@ switch result { case .success(let summary): print(summary.symbol) print(summary.barCount) - print(summary.result.totalReturn) + print(summary.metrics.totalReturn) case .failure(let error): print(error.localizedDescription) } ``` -When this works, you know: +Success looks like: -- the package builds - the bundled resources load correctly - the parser runs -- the default strategy flow can complete end-to-end +- you get a `BKRunSummary` back and can inspect `summary.metrics.totalReturn` + +What you learned: + +- `BKEngine.runDemo(...)` is the smallest full end-to-end smoke path +- the package can complete a deterministic backtest offline + +Where to go next: + +- continue to Step 3 if you want to switch from bundled data to your own CSV +- read [GETTING_STARTED.md](GETTING_STARTED.md) if you only wanted the shortest install/build/run checklist ## Step 3: Run your own inline CSV @@ -74,14 +96,48 @@ guard preflight.isReady else { let result = BKEngine.runDemoCSV(symbol: "AAPL", csv: csv) ``` -Use this stage to learn two habits early: +Success looks like: + +- your CSV passes preflight and runs through the helper-backed demo flow +- you can move from raw CSV text to a `BKRunSummary` without building a provider type + +What you learned: + +- preflight user-supplied CSV before execution +- helper workflows are the shortest path while you are still onboarding + +Where to go next: + +- continue to Step 4 to decide which top-level surface should own your next integration step +- read [DATA_INGESTION.md](DATA_INGESTION.md) if your next task is CSV mapping, date formats, or provider normalization + +## Step 4: Understand the surface choice + +Before wiring anything deeper, decide which surface owns your next job: + +- `BKAppFacade` if your input is user CSV and your output is app UI state +- `BKEngine` if your app already knows the provider shape and wants direct request control +- `BacktestingKitManager` if you already have candles +- tool helpers if the task is validation, export, comparison, scenario generation, or parity + +Success looks like: + +- you know where to start before writing integration code +- you avoid dropping into `BKEngine` earlier than necessary + +What you learned: + +- the package has distinct top-level surfaces with different jobs +- beginner integrations should usually start helper-first and app-integrator-first -- preflight data before execution when onboarding user-supplied CSV -- stay on helper workflows until you need lower-level control +Where to go next: -## Step 4: Learn the app-facing CSV import path +- continue to Step 5 if your app starts from reviewed user CSV +- read [CHOOSE_YOUR_SURFACE.md](CHOOSE_YOUR_SURFACE.md) if you want the fuller routing guide across all package surfaces -If your app lets users paste, upload, or edit CSV before running a backtest, stay on `BKAppFacade` first. The CSV import helpers are designed for app UIs that need inspection, preview, validation, normalization, and then execution. +## Step 5: Integrate through `BKAppFacade` + +For first-time app integrations, start with `BKAppFacade`. It is designed for apps that need inspection, preview, validation, normalization, and then execution from pasted, uploaded, or edited CSV. ```swift let screenState = BKAppFacade.buildCSVImportScreenState( @@ -102,7 +158,7 @@ let confirmedRun = BKAppFacade.runConfirmedCSVImport( ) ``` -If your app does not know the CSV settings yet, use the explicit auto-inference path instead of guessing in UI code. The auto helpers infer only safe defaults, report what they inferred, and still let your app surface or override those settings. +If your app does not know the CSV settings yet, use the explicit auto-inference path instead of guessing in UI code: ```swift let inference = BKAppFacade.detectCSVImportSettings( @@ -123,44 +179,24 @@ let autoRun = BKAppFacade.runCSVImportAuto( ) ``` -Use this layer when you need: - -- one stable review-state payload for onboarding/import screens -- grouped issues and readiness for app-side error display -- one clean handoff from reviewed settings into confirmed execution -- normalized bars/candles before deciding whether to persist or run -- one-step import plus preset execution for simple app flows -- a safe auto-apply path when the app does not already know the column mapping, date format, or chronological direction - -## Step 5: Move into canonical app integration +Success looks like: -When you are ready to wire BacktestingKit into a real app, start with `BKAppFacade` for the shortest app-facing path, then drop to `BKEngine` when you need direct request-model control. +- your app can render one review-state payload with issues, preview rows, inferred settings, and readiness +- your app can choose between reviewed and auto-inferred handoff paths without re-implementing import logic -### Facade-first app integration +What you learned: -```swift -let report = BKAppFacade.runPresetCSVAndExportMarkdown( - symbol: "AAPL", - csv: csv, - preset: .smaCrossover -) - -if report.isSuccessful { - print(report.markdown ?? "") -} -``` +- `BKAppFacade` is the default first production-style integration layer for app-facing import and preset flows +- the package already exposes explicit reviewed and auto-inferred handoff paths -Use `BKAppFacade` when you want: +Where to go next: -- one namespace for preset, scenario, export, and comparison helpers -- a beginner-friendly app integration entrypoint -- delegated workflows that still rely on the canonical engine/tool surfaces underneath +- continue to Step 6 when you need direct engine request ownership +- read [HELPER_WORKFLOWS.md](HELPER_WORKFLOWS.md) if you want the full façade/helper workflow catalog -### Canonical engine shape +## Step 6: Drop to `BKEngine` when you need direct request control -When you need request-level control, switch to `BKEngine`. That remains the canonical public execution surface for direct v2/v3 execution. - -### v3 shape +When you are ready to own provider wiring and request construction directly, switch to `BKEngine`. That remains the canonical public execution surface for direct v2/v3 execution. ```swift struct DemoProvider: BKRawCsvProvider { @@ -186,24 +222,22 @@ let request = BKEngine.V3Request( let result = await BKEngine.runV3(request) ``` -### v2 shape +Success looks like: -```swift -let request = BKEngine.V2Request( - instrumentID: "AAPL", - config: config, - p1: 5, - p2: 20, - csvProvider: provider, - log: { print($0) } -) +- your app constructs the request model itself +- `await BKEngine.runV3(...)` or `await BKEngine.runV2(...)` becomes the stable lower-level execution boundary -let result = await BKEngine.runV2(request) -``` +What you learned: + +- `BKEngine` is the canonical direct engine surface +- you only need to drop here once helper/facade workflows stop being expressive enough + +Where to go next: -At this point you have graduated from onboarding and can treat the package as an app integration dependency rather than a demo environment. +- read [ENGINE_GUIDE.md](ENGINE_GUIDE.md) for full v2/v3 request shapes, one-liners, drivers, and batch orchestration +- read [DATA_INGESTION.md](DATA_INGESTION.md) if your next task is custom provider wiring or CSV normalization rules -## Step 6: Choose your next guide +## Step 7: Choose your next guide After this onboarding path, use the guide that matches your next task: @@ -221,8 +255,9 @@ After this onboarding path, use the guide that matches your next task: If you want a structured sequence after onboarding: 1. [GETTING_STARTED.md](GETTING_STARTED.md) -2. [HELPER_WORKFLOWS.md](HELPER_WORKFLOWS.md) -3. [ENGINE_GUIDE.md](ENGINE_GUIDE.md) -4. [DATA_INGESTION.md](DATA_INGESTION.md) -5. [INDICATORS_STRATEGIES_METRICS.md](INDICATORS_STRATEGIES_METRICS.md) -6. [TOOLS.md](TOOLS.md) +2. [CHOOSE_YOUR_SURFACE.md](CHOOSE_YOUR_SURFACE.md) +3. [HELPER_WORKFLOWS.md](HELPER_WORKFLOWS.md) +4. [ENGINE_GUIDE.md](ENGINE_GUIDE.md) +5. [DATA_INGESTION.md](DATA_INGESTION.md) +6. [INDICATORS_STRATEGIES_METRICS.md](INDICATORS_STRATEGIES_METRICS.md) +7. [TOOLS.md](TOOLS.md) diff --git a/docs/PACKAGE_USAGE_GUIDE.md b/docs/PACKAGE_USAGE_GUIDE.md index 23989ff..5b60ce5 100644 --- a/docs/PACKAGE_USAGE_GUIDE.md +++ b/docs/PACKAGE_USAGE_GUIDE.md @@ -82,7 +82,7 @@ switch result { case .success(let summary): print(summary.symbol) print(summary.barCount) - print(summary.result.totalReturn) + print(summary.metrics.totalReturn) case .failure(let error): print(error.localizedDescription) } diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index 80ae864..dcf13e0 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -19,6 +19,7 @@ - [x] `LICENSE`, `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `SECURITY.md` are updated. - [x] No generated artifacts are tracked (`tools/parity/*_output.json`, `tools/parity/swift_runner_bin`, `.build/`). - [x] `Package.swift` products/targets/resources are correct. +- [x] Current patch-track release notes are prepared in `docs/RELEASE_NOTES_v0.1.x.md`. ## Publish diff --git a/docs/RELEASE_NOTES_v0.1.x.md b/docs/RELEASE_NOTES_v0.1.x.md new file mode 100644 index 0000000..60e6626 --- /dev/null +++ b/docs/RELEASE_NOTES_v0.1.x.md @@ -0,0 +1,41 @@ +# Release Notes - v0.1.x + +Release date: TBD + +## Highlights + +- Additive portfolio orchestration for app and engine integrators with: + - `BKEngine.PortfolioRequest` + - `BKEngine.runPortfolio(...)` + - `BKAppFacade.buildPortfolioCSVImportScreenState(...)` + - `BKAppFacade.runConfirmedPortfolioCSVImport(...)` +- Portfolio export/report helpers for JSON, CSV, and Markdown outputs. +- Documentation/tutorial completion pass that gives the package one obvious markdown starting point, one clearer DocC starting point, and a more linear first-run story. + +## Documentation Highlights + +- `README.md` now acts as a lighter front door instead of trying to be the full tutorial. +- `docs/ONBOARDING.md` is the canonical markdown tutorial from build to app integration. +- `docs/GETTING_STARTED.md` is the shortest route from install to first success. +- `docs/CHOOSE_YOUR_SURFACE.md` now cleanly routes users between `BKAppFacade`, `BKEngine`, manager workflows, and tool helpers. +- DocC tutorial ordering now follows the same recommended sequence as the markdown beginner path. +- Tutorial examples now consistently use `summary.metrics.*` when inspecting `BKRunSummary`. + +## Validation Status + +- `swift build` ✅ +- `swift build -c release` ✅ +- `swift test` ✅ +- `swift run BacktestingKitTrialDemo` ✅ +- `bash tools/parity/run_parity.sh` ✅ when a local JS engine checkout is available + +## Upgrade Notes + +- Portfolio orchestration is additive and does not replace the existing single-instrument execution paths. +- The recommended beginner path is helper-first (`BKAppFacade`) and only drops to `BKEngine` when direct request-model control is needed. +- Compatibility aliases remain available through the `v0.1.x` release cycle to reduce upgrade friction while the canonical docs converge on the preferred names. + +## Release Prep Notes + +- This file is the prepared patch-track release summary and intentionally avoids locking the final `v0.1.x` tag too early. +- Tag creation, GitHub release publication, and contributor announcement remain explicit publish-time steps. diff --git a/docs/plans/2026-04-11-xcode-api-doc-comments-plan.md b/docs/plans/2026-04-11-xcode-api-doc-comments-plan.md new file mode 100644 index 0000000..0f4fe6e --- /dev/null +++ b/docs/plans/2026-04-11-xcode-api-doc-comments-plan.md @@ -0,0 +1,28 @@ +# 2026-04-11 Xcode API Doc Comments Plan + +## Goal + +Add Xcode-compatible documentation comments to the BacktestingKit public API surface so exposed symbols have usable Quick Help without changing runtime behavior. + +## Scope + +- Public and open declarations in `BacktestingKit/` +- Type, method, property, initializer, and typealias surfaces that are part of the shipped package API +- Existing public APIs only; no functional changes + +## Approach + +1. Inventory public declarations and identify symbols that do not already have adjacent doc comments. +2. Add short, behavior-focused `///` comments only where coverage is missing. +3. Prefer concise summaries plus parameter/returns notes for non-obvious functions. +4. Keep edits additive and localized to the files that expose public APIs. + +## Verification + +- Re-scan the package for public declarations without adjacent doc comments. +- Run a Swift package verification command to catch syntax or formatting regressions. + +## Risks + +- Heuristic scans can miss edge cases such as grouped declarations inside public extensions. +- Some legacy APIs have broad surfaces, so consistency matters more than exhaustive prose. diff --git a/tasks/lessons.md b/tasks/lessons.md new file mode 100644 index 0000000..17a9397 --- /dev/null +++ b/tasks/lessons.md @@ -0,0 +1,5 @@ +# Lessons + +## 2026-04-11 + +- No project-specific corrections captured in this session yet. diff --git a/tasks/todo.md b/tasks/todo.md new file mode 100644 index 0000000..5871ecb --- /dev/null +++ b/tasks/todo.md @@ -0,0 +1,97 @@ +# Active Todo + +## 2026-04-15 Follow-up Review Comment Fixes + +- [completed] Reproduce the fail-fast portfolio summary bug and the explicit zero-total review-state bug with failing regression tests. +- [completed] Update portfolio orchestration so fail-fast runs still resolve weights before finalization. +- [completed] Extend app-facing portfolio review validation to reject explicit allocations whose clamped total is not positive. +- [completed] Re-run targeted and full Swift test verification, then record the result. + +## 2026-04-15 Review Comment Fixes + +- [completed] Reproduce the two PR review comments with failing tests for explicit zero-total allocations and portfolio review-state validation. +- [completed] Update portfolio allocation resolution so invalid explicit zero-total weights fail instead of silently equal-weighting successful sleeves. +- [completed] Add portfolio-level validation to the app-facing review state for duplicate symbols and explicit weight-count mismatches. +- [completed] Re-run targeted and full test verification after the fix. + +## 2026-04-15 Release Prep + +- [completed] Audit release-facing metadata, roadmap/checklist state, and current verification evidence. +- [completed] Update `CHANGELOG.md` and prepare patch-track release notes without forcing a final tag name early. +- [completed] Sync `ROADMAP.md` and `docs/RELEASE_CHECKLIST.md` with the completed docs/tutorial and CI work. +- [completed] Re-run release-prep verification and record what remains intentionally manual. + +## 2026-04-13 Documentation and Tutorial Completion + +- [completed] Audit the beginner-facing markdown and DocC docs for overlap, stale examples, and unclear entry points. +- [completed] Reshape `README.md` into a lighter front door with one proof-of-success snippet, audience routing, and earlier tutorial links. +- [completed] Rewrite `docs/INDEX.md`, `docs/ONBOARDING.md`, `docs/GETTING_STARTED.md`, and `docs/CHOOSE_YOUR_SURFACE.md` so each page has one clear job. +- [completed] Align the DocC landing/tutorial order with the markdown onboarding path and add lightweight success checkpoints. +- [completed] Verify links and example accuracy with doc scans plus a Swift package build/test pass, then record review notes. + +## 2026-04-13 Portfolio Orchestration API + +- [completed] Review existing single-instrument, batch, import-review, and export seams for portfolio reuse. +- [completed] Add portfolio domain models plus a deep orchestration core for sleeve execution, weighting, rebalance interpretation, and aggregate reporting. +- [completed] Expose canonical `BKEngine` portfolio request/run APIs and app-facing `BKAppFacade` basket review/run helpers. +- [completed] Extend export/report helpers for portfolio JSON/CSV/Markdown outputs. +- [completed] Add focused tests for orchestration behavior, façade basket workflows, and portfolio exports. +- [completed] Run targeted verification, update roadmap/review notes, and summarize follow-up gaps. + +## 2026-04-11 Xcode API Documentation Pass + +- [completed] Review the current public API surface and existing documentation coverage. +- [completed] Identify public declarations that are missing Xcode doc comments. +- [completed] Add concise Xcode documentation comments to the full exposed API surface, including stored properties/constants. +- [completed] Verify documentation coverage with repo scans and a Swift package build/test pass. +- [completed] Record results and any follow-up gaps in the review notes below. + +## Review + +- Added regression coverage for two review-reported cases: explicit allocations that collapse to a non-positive total across successful sleeves, and app-facing basket review states that previously ignored duplicate-symbol and explicit-weight-count portfolio constraints. +- `BKPortfolioOrchestration` now keeps zero weights and emits a `portfolio-allocation` failure for invalid explicit zero-total allocations instead of silently converting them into equal-weight runs. +- `BKAppFacade.buildPortfolioCSVImportScreenState(...)` now adds portfolio-scoped validation issues and marks the basket invalid before execution when duplicate sleeve symbols or explicit weight-count mismatches are present. +- Verification: `swift test --filter 'BacktestingKitPortfolioTests/testRunPortfolioRejectsExplicitWeightsWhenSuccessfulSleevesResolveToNonPositiveTotal|BacktestingKitAppFacadeTests/testBuildPortfolioCSVImportScreenStateRejectsDuplicateSleeves|BacktestingKitAppFacadeTests/testBuildPortfolioCSVImportScreenStateRejectsExplicitWeightCountMismatch'` +- Verification: `swift test --filter 'BacktestingKitPortfolioTests|BacktestingKitAppFacadeTests'` +- Verification: `swift test` passed with 125 tests and 0 failures. + +- Added follow-up regression coverage for two more review-reported cases: fail-fast portfolio runs now resolve processed successful-sleeve weights before finalization, and portfolio import review now rejects explicit allocations whose clamped total is not positive. +- `BKPortfolioOrchestration` no longer returns early on fail-fast failures before weight resolution; it breaks out, resolves weights against the full request, and finalizes with correct resolved weights and aggregate metrics for the already-successful sleeves. +- `BKAppFacade.buildPortfolioCSVImportScreenState(...)` now emits `portfolio_explicit_weight_total_invalid` when explicit review weights clamp to a non-positive total, so `isReadyToContinue` no longer advertises a deterministically invalid run as ready. +- Verification: `swift test --filter 'BacktestingKitPortfolioTests/testRunPortfolioFailFastResolvesSuccessfulSleeveWeightsBeforeFinalization|BacktestingKitAppFacadeTests/testBuildPortfolioCSVImportScreenStateRejectsExplicitWeightTotalThatIsNotPositive'` +- Verification: `swift test --filter 'BacktestingKitPortfolioTests|BacktestingKitAppFacadeTests'` +- Verification: `swift test` passed with 127 tests and 0 failures. + +- Prepared the current patch-track release metadata with an unreleased changelog entry, a new `docs/RELEASE_NOTES_v0.1.x.md` draft, and doc index/readme references that no longer hardcode `v0.1.0` as the active release track. +- Synced the release lane in `ROADMAP.md` to reflect the work that is actually complete: API naming audit, DocC final pass, CI matrix finalization, and prepared `v0.1.x` release notes. +- Left the publish-time steps in `docs/RELEASE_CHECKLIST.md` intentionally unchecked because tag creation, GitHub release publication, and contributor announcement are still explicit manual release actions. +- Shared tracking: opened, updated, and closed GitHub issue `#3` for this release-prep pass after recording the final verification summary. +- Verification: `swift build` +- Verification: `swift build -c release` +- Verification: `swift test` passed with 122 tests and 0 failures. +- Verification: `swift run BacktestingKitTrialDemo` exited successfully. +- Verification: `bash tools/parity/run_parity.sh` correctly reported that no local JS engine checkout is present, so parity remains an environment-dependent release gate rather than a package failure in this worktree. +- Release candidate branch pushed: `codex/release-prep-v0.1.x` +- Draft PR opened: `#4` +- Final public tag/release intentionally paused pending one decision: release `v0.1.1` directly from this branch or merge to `main` first. The parity baseline announcement also still needs an explicit JS engine revision if we want that checklist item fully closed. + +- Reshaped the beginner-facing docs so `README.md` is now a lighter front door, `docs/ONBOARDING.md` is the canonical markdown tutorial, `docs/GETTING_STARTED.md` is the compact quick reference, `docs/CHOOSE_YOUR_SURFACE.md` is the routing guide, and `docs/INDEX.md` is the docs map rather than a competing onboarding page. +- Promoted the existing DocC tutorial track by surfacing `BacktestingKit/BacktestingKit.docc` earlier in the markdown docs, reordering the DocC onboarding chapter around first success -> onboarding -> CSV import -> app integration, and adding lightweight success-oriented framing to the touched tutorial pages. +- Fixed stale onboarding/tutorial examples that still referenced `summary.result.totalReturn` or `summary.result.maxDrawdown` on `BKRunSummary`; the touched markdown and DocC examples now use `summary.metrics.*`. +- Shared tracking: opened and closed GitHub issue `#2` for the documentation/tutorial pass after posting the completion summary and verification notes. +- Verification scan: `rg -n 'summary\\.result\\.' README.md docs BacktestingKit/BacktestingKit.docc` returned no matches. +- Verification: `swift build` +- Verification: `swift test` passed with 122 tests and 0 failures. + +- Implemented additive portfolio orchestration with new public portfolio request/result models, `BKEngine.runPortfolio(...)`, and a deep sleeve-execution core that resolves explicit, sleeve-driven, risk-parity, and risk-on/risk-off allocations. +- Added app-facing basket review/execution APIs with `BKAppFacade.buildPortfolioCSVImportScreenState(...)` and `BKAppFacade.runConfirmedPortfolioCSVImport(...)`, reusing the existing CSV inspection, inference, and normalization workflow for each sleeve. +- Added portfolio export/report helpers for JSON, CSV, and Markdown output so integrators can persist aggregate portfolio results, sleeve allocations, failures, and rebalance events without using export bundles as the primary runtime contract. +- Added portfolio-focused regression coverage for weighted aggregation, rebalance event generation, helper-driven weight resolution, partial failure vs. fail-fast behavior, app basket review readiness, confirmed run handoff, and portfolio export payloads. +- Verification: `swift build` +- Verification: `swift test --filter 'BacktestingKitPortfolioTests|BacktestingKitAppFacadeTests'` +- Verification: `swift test` passed with 122 tests and 0 failures. + +- Added Xcode Quick Help comments for previously undocumented public declarations and stored properties/constants across the exposed package surface. +- 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.