Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Reed Es committed Aug 29, 2022
0 parents commit 52a7123
Show file tree
Hide file tree
Showing 47 changed files with 4,511 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.build/
.swiftpm/
Package.resolved
1 change: 1 addition & 0 deletions .swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5.3.1
373 changes: 373 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// swift-tools-version:5.5
//
// Copyright 2021, 2022 OpenAlloc LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//

import PackageDescription

let package = Package(
name: "FlowAllocLow",
platforms: [.macOS(.v12), .iOS(.v15)],
products: [
.library(name: "FlowAllocLow", targets: ["FlowAllocLow"]),
],
dependencies: [
.package(url: "https://github.com/openalloc/FlowXCT.git", from: "1.1.0"),
.package(url: "https://github.com/openalloc/FlowBase.git", from: "1.1.0"),
],
targets: [
.target(
name: "FlowAllocLow",
dependencies: [
"FlowBase",
],
path: "Sources"
),
.testTarget(
name: "FlowAllocLowTests",
dependencies: [
"FlowAllocLow",
"FlowBase",
"FlowXCT",
],
path: "Tests"
),
]
)
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# FlowAllocLow

Swift library containing low-level support for the [FlowAllocator](https://openalloc.github.io/FlowAllocator/index.html) app

_FlowAllocLow_ is part of the [OpenAlloc](https://github.com/openalloc) family of open source Swift software tools.

## Unit Tests

Most critical functionality in this library is backed by unit tests. Coverage can likely be better than it is.

Note that any given unit test may have defects or bad assumptions, and can be worthy of review and re-write.

## See Also

* [FlowAllocator](https://openalloc.github.io/FlowAllocator/index.html) - portfolio rebalancing tool for macOS
* [FlowWorth](https://openalloc.github.io/FlowWorth/index.html) - portfolio valuation and tracking tool for macOS
* [FlowUI](https://github.com/openalloc/FlowUI) - shared UI support for the _FlowAllocator_ and _FlowWorth_ apps
* [FlowAllocHigh](https://github.com/openalloc/FlowAllocHigh) - high-level support for the _FlowAllocator_ app
* [FlowAllocLow](https://github.com/openalloc/FlowAllocLow) - low-level support for the _FlowAllocator_ app
* [FlowWorthLib](https://github.com/openalloc/FlowWorthLib) - support for the _FlowWorth_ app
* [FlowBase](https://github.com/openalloc/FlowBase) - shared support for the _FlowAllocator_ and _FlowWorth_ apps
* [FlowStats](https://github.com/openalloc/FlowStats) - shared stats support for the _FlowAllocator_ and _FlowWorth_ apps
* [FlowViz](https://github.com/openalloc/FlowViz) - shared visualization components for the _FlowAllocator_ and _FlowWorth_ apps
* [FlowXCT](https://github.com/openalloc/FlowXCT) - shared testing components for the _FlowAllocator_ and _FlowWorth_ apps

## License

Copyright 2021, 2022 OpenAlloc LLC

The code for this library is licensed under the [Mozilla Public License 2](https://www.mozilla.org/en-US/MPL/2.0/), except where noted in individual modules.

## Contributing

Contributions are welcome. You are encouraged to submit pull requests to fix bugs, improve documentation, or offer new features.

The pull request need not be a production-ready feature or fix. It can be a draft of proposed changes, or simply a test to show that expected behavior is buggy. Discussion on the pull request can proceed from there.

Contributions should ultimately have adequate test coverage. See tests for current entities to see what coverage is expected.
283 changes: 283 additions & 0 deletions Sources/Allocation/AllocateUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
//
// AllocateUtils.swift
//
// Copyright 2021, 2022 OpenAlloc LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//

import Foundation
import os

import AllocData

import FlowBase


let epsilon = 0.0001 // accuracy of Double comparisons

//let alog = Logger(subsystem: "app.flowallocator", category: "Allocate")

public func getAccountAllocationMap(allocs: [AssetValue],
accountKeys: [AccountKey],
allocFlowMode: Double,
assetAccountLimitMap: AssetAccountLimitMap,
accountUserVertLimitMap: AccountUserVertLimitMap,
accountUserAssetLimitMap: AccountUserAssetLimitMap,
accountCapacitiesMap: AccountCapacitiesMap,
isStrict: Bool = false) throws -> AccountAssetValueMap
{
var remainingAssetClassCapacities = allocs.map(\.value)

let map: AccountAssetValueMap = try accountKeys.enumerated().reduce(into: [:]) { map, entry in
let (accountIndex, accountKey) = entry

// need to draw this down to 0 for account, or abort allocation (e.g., 33%)
guard let accountCapacity = accountCapacitiesMap[accountKey],
accountCapacity.isGreater(than: 0.0, accuracy: epsilon)
else {
map[accountKey] = [:] // so that cells will be rendered despite no allocation/funds
return
}

guard let userVertLimitMap = accountUserVertLimitMap[accountKey] else { throw AllocLowError1.missingVertLimit }
guard let userAssetLimitMap = accountUserAssetLimitMap[accountKey] else { throw AllocLowError1.missingAssetLimit }

map[accountKey] = try getAllocationMap(accountKeys: accountKeys,
accountIndex: accountIndex,
allocs: allocs,
allocFlowMode: allocFlowMode,
accountCapacity: accountCapacity,
assetAccountLimitMap: assetAccountLimitMap,
userVertLimitMap: userVertLimitMap,
userAssetLimitMap: userAssetLimitMap,
remainingAssetClassCapacities: &remainingAssetClassCapacities,
isStrict: isStrict)
}

return map
}

func getAllocationMap(accountKeys: [AccountKey],
accountIndex: Int,
allocs: [AssetValue],
allocFlowMode: Double,
accountCapacity: Double,
assetAccountLimitMap: AssetAccountLimitMap,
userVertLimitMap: UserVertLimitMap,
userAssetLimitMap: UserAssetLimitMap,
remainingAssetClassCapacities: inout [Double],
isStrict: Bool = false) throws -> AssetValueMap
{
// Horizontal: starts as 100% of account's share of strategy, and decreases (vertical)
var remainingToAllocateInAccount = accountCapacity

return try allocs.enumerated().reduce(into: [:]) { map, entry in
let (allocIndex, alloc) = entry

guard remainingToAllocateInAccount.isGreater(than: 0.0, accuracy: epsilon) else { return }

guard let userVertLimit = userVertLimitMap[alloc.assetKey] else { throw AllocLowError1.missingVertLimit }
guard let userAssetLimit = userAssetLimitMap[alloc.assetKey] else { throw AllocLowError1.missingAssetLimit }
let accountLimitMap = assetAccountLimitMap[alloc.assetKey] ?? [:]

let netAllocation = try getAllocation(accountKeys: accountKeys,
alloc: alloc,
allocIndex: allocIndex,
allocFlowMode: allocFlowMode,
accountCapacity: accountCapacity,
accountLimitMap: accountLimitMap,
accountIndex: accountIndex,
isStrict: isStrict,
userVertLimit: userVertLimit,
userAssetLimit: userAssetLimit,
remainingToAllocateInAccount: remainingToAllocateInAccount,
remainingAssetClassCapacities: &remainingAssetClassCapacities)

remainingToAllocateInAccount -= netAllocation

//print("netAllocation=\(netAllocation) remainingToAllocateInAccount=\(remainingToAllocateInAccount)")

// if less than zero, but within tolerance, coerce to zero, to avoid Core Data
// validation complaining about a negative value.
let netAllocation_ = netAllocation.coerceIfEqual(to: 0.0, accuracy: epsilon)

map[alloc.assetKey] = netAllocation_ / accountCapacity
}
}

// returns the size of the allocation, as a fraction of the entire strategy
func getAllocation(accountKeys: [AccountKey],
alloc: AssetValue,
allocIndex: Int,
allocFlowMode: Double,
accountCapacity: Double,
accountLimitMap: AccountLimitMap,
accountIndex: Int,
isStrict: Bool,
userVertLimit: Double,
userAssetLimit: Double,
remainingToAllocateInAccount: Double,
remainingAssetClassCapacities: inout [Double]) throws -> Double
{
// is the folio's asset class explicitly supported by this account?
// guard let assetID = strategySlice.assetID else { throw StrategySliceError.missingAssetClass }

// remaining capacity to allocate in current assetID, across subsequent accounts (horizontal)
let remainingAssetClassCapacity = remainingAssetClassCapacities[allocIndex]

// os_log("[%@] %@ strategySliceIndex=%d remainingAssetClassCapacity=%0.4f", #function, strategySlice.assetID, strategySliceIndex, remainingAssetClassCapacity)

guard remainingAssetClassCapacity.isGreater(than: 0, accuracy: epsilon) else { return 0 }

//print("remainingAssetClassCapacities=\(remainingAssetClassCapacities) index=\(strategySliceIndex)")

// remaining capacity to allocate in subsequent asset classes, across all accounts
let forwardAssetClassCapacity = remainingAssetClassCapacities.forwardSum(start: allocIndex + 1)

// os_log("[%@] GGG forwardAssetCapacity=%0.4f", #function, forwardAssetClassCapacity)

// user will tolerate up to 100% of the account to be allocated to an asset class
// e.g., 100% of $64K Roth in SPY

// calculate the user-suggested limit on allocations for this asset class for all subsequent accounts
// e.g., the user wishes to limit bonds to 0% in the taxable (rightmost) account
let forwardAssetClassLimit: Double = accountLimitMap.forwardSum(order: accountKeys, start: accountIndex + 1)

//print("forwardAssetClassLimit=\(forwardAssetClassLimit) assetID=\(strategySlice.assetID)")

// os_log("[%@] HHH forwardAssetClassLimit=%0.4f", #function, forwardAssetClassLimit)

let skewedAllocFlowMode = getSkewedAllocFlowMode(rawAllocFlowMode: allocFlowMode)

let flowTarget = getFlowTarget(targetPct: alloc.value,
accountCapacity: accountCapacity,
allocFlowMode: skewedAllocFlowMode)

let surplusRequired = getSurplusRequired(remainingAssetClassCapacity: remainingAssetClassCapacity,
forwardAssetClassLimit: forwardAssetClassLimit,
flowTarget: flowTarget)

// suggest a limit for the current cap based on user preference and degree to which we're mirroring
let userMaxLimit = getUserMaxLimit(userLimit: userAssetLimit,
flowTarget: flowTarget,
accountCapacity: accountCapacity,
surplusRequired: surplusRequired)

// os_log("[%@] cap flowTarget=%0.4f surplusRequired=%0.4f userMaxLimit=%0.4f", #function, flowTarget, surplusRequired, userMaxLimit)

// limit amount allocated to asset class in account, if specified in allocation slice
let netAllocation = getStrategyPct(remainingAccountCapacity: remainingToAllocateInAccount,
remainingAssetClassCapacity: remainingAssetClassCapacity,
forwardAssetClassCapacity: forwardAssetClassCapacity,
userMaxLimit: userMaxLimit,
userVertLimit: userVertLimit)

//print("remainingAssetClassCapacity=\(remainingAssetClassCapacity) forwardAssetClassCapacity=\(forwardAssetClassCapacity) forwardAssetClassLimit=\(forwardAssetClassLimit) skewedAllocFlowMode=\(skewedAllocFlowMode) flowTarget=\(flowTarget) surplusRequired=\(surplusRequired) userMaxLimit=\(userMaxLimit) netAllocation=\(netAllocation)")

if isStrict, netAllocation > userAssetLimit {
throw AllocLowError1.userLimitExceededUnderStrict
}

// os_log("[%@] MMM netAllocation=%0.4f", #function, netAllocation)

guard netAllocation.isGreater(than: 0.0, accuracy: epsilon) else { return 0 }

// for the benefit of future accounts, deduct our current allocation
remainingAssetClassCapacities[allocIndex] -= netAllocation

// if substantially less than zero, raise the alarm
if netAllocation.isLess(than: 0.0, accuracy: epsilon) {
throw AllocLowError1.unexpectedResult("netSlice less than zero")
}

return netAllocation
}

// convex skew for greater sensitivity when adjusting towards flow
func getSkewedAllocFlowMode(rawAllocFlowMode: Double) -> Double {
1 - ((1 - rawAllocFlowMode) * (1 - rawAllocFlowMode))
}

func getFlowTarget(targetPct: Double,
accountCapacity: Double,
allocFlowMode: Double) -> Double
{
let mirrorTarget = targetPct * accountCapacity

return mirrorTarget + ((targetPct - mirrorTarget) * allocFlowMode)
}

func getSurplusRequired(remainingAssetClassCapacity: Double,
forwardAssetClassLimit: Double,
flowTarget: Double) -> Double
{
max(0, remainingAssetClassCapacity - forwardAssetClassLimit - flowTarget)
}

// Suggest a limit based on user preference and degree to which we're mirroring.
//
// If mirroring (allocFlowMode<1) for assetID, maximize UP TO current limitPct
// to accommodate user's limitPct on forward allocations in assetID.
//
// With 100% flow (allocFlowMode==1) we're always maximizing to limitPct, so no
// special treatment.
//
func getUserMaxLimit(userLimit: Double,
flowTarget: Double,
accountCapacity: Double,
surplusRequired: Double) -> Double
{
let baseLimit = min(userLimit, flowTarget)

return min(accountCapacity, baseLimit + surplusRequired)
}

//
// In current slice: Example
// - can allocate as most A% 80%
// - must allocate as least B% 10%
// - user wants to allocate at most C% 50%
//
// min( A, max( B, C ) ) 50%
//
// tested in MStrategyTargetGetPercentTests
//
//
func getStrategyPct(remainingAccountCapacity: Double,
remainingAssetClassCapacity: Double,
forwardAssetClassCapacity: Double,
userMaxLimit: Double,
userVertLimit: Double) -> Double
{
// can allocate at most
let a = min(remainingAccountCapacity, remainingAssetClassCapacity)

// must allocate at least
let b = max(0, remainingAccountCapacity - forwardAssetClassCapacity)

// user wants to allocate at most
let c = max(userMaxLimit, userVertLimit)

return min(a, max(b, c))
}

func getCapacitiesMap(_ accountKeys: [AccountKey],
_ accountPresentValueMap: AccountPresentValueMap) -> AccountCapacitiesMap
{
let accountsTotal = accountKeys.reduce(0) { $0 + (accountPresentValueMap[$1] ?? 0) }
if accountsTotal <= 0 { return AccountCapacitiesMap() }
return accountKeys.reduce(into: [:]) { map, accountKey in
let accountTotal = accountPresentValueMap[accountKey] ?? 0
map[accountKey] = accountTotal / accountsTotal
}
}

func getLimitPctMap(_ caps: [MCap]) -> LimitPctMap {
caps.reduce(into: [:]) { map, cap in
guard cap.assetKey.isValid else { return }
map[cap.assetKey] = cap.limitPct
}
}
Loading

0 comments on commit 52a7123

Please sign in to comment.