Created
September 30, 2018 00:32
-
-
Save klumsy/10fbcc320e78f7d50d1820ee70b950ae to your computer and use it in GitHub Desktop.
screwing around with capital gain optomization (only works with input data CSVs in specific format
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Enum GainTerm { | |
Short | |
Long | |
} | |
Enum GainLoss { | |
Gain | |
Loss | |
} | |
Enum GainType { | |
ShortTermGain | |
ShortTermLoss | |
LongTermGain | |
LongTermLoss | |
} | |
Function MatchPriority([GainLoss]$gainloss, [GainTerm]$term) | |
{ | |
if (($gainloss -eq [GainLoss]::Loss) -and ($term -eq [GainTerm]::Short)) { return 1 } | |
if (($gainloss -eq [GainLoss]::Loss) -and ($term -eq [GainTerm]::Long)) { return 2 } | |
if (($gainloss -eq [GainLoss]::Gain) -and ($term -eq [GainTerm]::Long)) { return 3 } | |
if (($gainloss -eq [GainLoss]::Gain) -and ($term -eq [GainTerm]::Short)) { return 4 } | |
} | |
$combocount = 0; #lets just see how many combinations we have tried. | |
$longtermrate = 0.20; #look at turbotax to see reality | |
$shorttermrate = 0.35; | |
$sourcesales= Import-Csv ~/Documents/CapitalGains/Data/Sales.csv | |
$sourcesales | Add-Member "Version" 1 | |
$sourcesales | Add-Member "FullSaleShares" 0 | |
$sourcesales | % { $_.FullSaleShares = $_.ShareNum } | |
$sourcetranches = Import-Csv ~/Documents/CapitalGains/Data/tranches.csv #numbers are strings. | |
$sourcetranches | Add-Member "Version" 1 | |
$sourcetranches | Add-Member "Change" $Null #used only for ledger, terrible mixin of concerns | |
$sourcetranches | Add-Member "SaleId" $null #used only for ledger, terrible mixin of concerns | |
$sourcetranches | Add-Member "StartingShares" 0 | |
$sourcetranches | % { $_.StartingShares = $_.Shares} | |
function Match-Combinations($sales, $tranches) | |
{ | |
$possiblecombinations = | |
foreach( $sale in $sales) | |
{ | |
foreach ($tranch in $tranches) | |
{ | |
$possibility = @{ | |
#Sale Data | |
ContractNote = $sale.ContractNote; | |
SaleDate = [datetime]$sale.DateUS; | |
SaleSharePrice = [Decimal] $sale.SharePriceUSD | |
SharesSold = [Int] $sale.ShareNum | |
SaleCost = [decimal] $sale.CostsUSD | |
SaleGross = [decimal] $sale.SaleUSD | |
SaleNet = [decimal] $sale.SaleAfterCostUSD | |
#Tranch Data | |
TranchId = [int]$tranch.TranchId | |
VestDate = [Datetime]$tranch.TranchDate | |
TranchShares = [Int]$tranch.Shares | |
TranchBasis = [Decimal]$tranch.Basis | |
#Computed Data | |
} | |
$possibility.age = ($possibility.SaleDate - $possibility.vestdate).TotalDays | |
if($possibility.SaleDate -lt $possibility.vestdate){ continue} | |
$possibility.term = if ($possibility.age -gt 365) {[GainTerm]::Long} else {[GainTerm]::Short} | |
$possibility.IsWashSale = ($possibility.age -le 31) | |
$possibility.GainOrLoss = if ($possibility.SaleSharePrice -ge $possibility.tranchBasis) { | |
[GainLoss]::Gain} else { [GainLoss]::Loss} | |
$possibility.MatchPriority = MatchPriority -gainloss $possibility.GainOrLoss -term $possibility.term | |
$possibility.overflowtranch = $possibility.SharesSold -gt $possibility.TranchShares | |
$possibility.sharedmatched = [math]::min($possibility.SharesSold,$possibility.TranchShares) | |
$saleportion = $possibility.sharedmatched / $possibility.SharesSold #Approx | |
$possibility.ratio = [Decimal]::round($saleportion * 100,2) | |
$possibility.GainDollars = [Decimal]::Round( | |
(($possibility.SaleSharePrice - $possibility.TranchBasis)* $possibility.sharedmatched ) - $possibility.SaleCost , 2) | |
#lets always apply fees first, so that the fees are used in the first match, | |
# and subsequent matches have zero fees | |
$possibility.GainTax = if ($possibility.GainOrLoss -eq [GainTerm]::Long) {$possibility.GainDollars * $longtermrate } else {$possibility.GainDollars * $longtermrate} | |
$possibility.GainTaxPerShare = ($possibility.GainTax / $possibility.sharedmatched) | |
new-object psobject -Property $possibility # select in order | |
} | |
} | |
$possiblecombinations | |
} | |
function Match-BestCombo ($combinations) | |
{ | |
#Match-GainCategories $combinations | |
match-PerGainTaxPerShare $combinations | |
#Match-PerGainTax $combinations | |
#Match-PerGainTaxNoOverFlow $combinations | |
#Match-FirstInFirstOut $combinations | |
#Match-LastInFirstOut $combinations | |
} | |
function Match-GainCategories($combinations) | |
{ | |
#short term loss, long term loss, long term gain, short term gain, then gaintax | |
$combinations | Where { -not $_.IsWashSale} | Sort-Object MatchPriority,GainTaxPerShare | select -first 1 | |
} | |
#not sure i got logic right | |
function Match-FirstInFirstOut($combinations) | |
{ | |
$combinations | Where { -not $_.IsWashSale} | Sort-Object -Property @{Expression = "VestDate"; Descending = $false}, @{Expression = "SaleDate"; Descending = $false} | | |
select -first 1 | |
} | |
#not sure i got logic right | |
function Match-LastInFirstOut($combinations) | |
{ | |
$combinations | Where { -not $_.IsWashSale} | Sort-Object -Property @{Expression = "VestDate"; Descending = $true}, @{Expression = "SaleDate"; Descending = $true} | | |
select -first 1 | |
} | |
function Match-PerGainTaxPerShare($combinations) | |
{ | |
$combinations | Where { -not $_.IsWashSale} | #Where { -not $_.overflowtranch } | # exclude these for now | |
Sort-Object GainTaxPerShare | select -first 1 | |
# Sort-Object GainTaxPerShare -Descending | select -first 1 #worst case scenario | |
} | |
function Match-PerGainTax($combinations) | |
{ | |
$combinations | Where { -not $_.IsWashSale} | #Where { -not $_.overflowtranch } | # exclude these for now | |
Sort-Object GainTax | select -first 1 | |
} | |
function Match-PerGainTaxNoOverFlow($combinations) | |
{ | |
$combinations | Where { -not $_.IsWashSale} | Where { -not $_.overflowtranch } | # exclude these for now | |
Sort-Object GainTax | select -first 1 | |
} | |
$tranchledger = @(); | |
$processedsales = @(); | |
$sales = $sourcesales | select * | |
$tranches = $sourcetranches | select * | |
Function Apply-Sale ($saletoprocess ) | |
{ | |
if ($null -eq $saletoprocess) {Write-Error "no sale to process"} | |
$saletolog = $saletoprocess | select * | |
$tranch = $tranches | where { $_.tranchid -eq $saletoprocess.TranchId} | |
$originalsale = $sales | where { $_.ContractNote -eq $saletoprocess.ContractNote} | |
$saletolog | Add-Member SaleId "$($tranch.TranchId)-$($saletoprocess.ContractNote)-$($originalsale.Version)"; | |
$global:processedsales += $saletolog | |
if ([int]$saletolog.SharesSold -eq [int]$tranch.Shares) { | |
$tranch.Shares = 0 | |
$tranch.Change = $tranch.Shares * -1 | |
$tranch.SaleId = $saletolog.SaleId | |
$global:tranches = $tranches | where { $_.tranchid -ne $saletoprocess.TranchId} #remove it for now (later will have to decide whether to update it/reduce it) | |
$global:sales = $sales | where { $_.ContractNote -ne $saletoprocess.ContractNote} | |
Write-Host -ForegroundColor blue "sale and tranch same size, tranch deleted, sale consumed" | |
} else { | |
if ([int]$saletolog.SharesSold -gt [int]$tranch.Shares) { | |
$global:tranches = $tranches | where { $_.tranchid -ne $saletoprocess.TranchId} #remove it for now (later will have to decide whether to update it/reduce it) | |
Write-Host -ForegroundColor green "sale and tranch same size, tranch deleted, left over sale reduced" | |
#TODO: reduce sale with new name | |
$saletolog.SharesSold = $tranch.Shares | |
#TODO some bug below!!! | |
Write-Error "next line MIGHT STILL have a bug" | |
$originalsale.ShareNum = $originalsale.ShareNum - $saletolog.SharesSold | |
#todo: also take off the cost part since we don't deduct that over and over. | |
$originalsale.version += 1 | |
$originalsale.CostsUSD = 0 #cost applied on this sale, so can't deduct it on future | |
$tranch.Shares = 0 | |
$tranch.Change = $tranch.Shares * -1 | |
$tranch.SaleId = $saletolog.SaleId | |
#$global:sales = $sales | where { $_.ContractNote -ne $saletoprocess.ContractNote} | |
} else { | |
$tranch.Change = $saletolog.SharesSold * -1 | |
$tranch.Shares = [int]$tranch.Shares - [int]$saletolog.SharesSold | |
$tranch.Version = [int]$tranch.Version + 1 | |
$tranch.SaleId = $saletolog.SaleId | |
Write-Host -ForegroundColor yellow "sale less than tranch, sale consumed, tranch reduced $([int]$saletolog.SharesSold)" | |
$global:sales = $sales | where { $_.ContractNote -ne $saletoprocess.ContractNote} | |
} | |
} | |
$global:tranchledger += $tranch | select * | |
} | |
## $combos | Where { -not $_.IsWashSale} | Where { -not $_.overflowtranch } | # exclude these for now | |
## Sort-Object GainTaxPerShare | Out-Numbers | |
while ($sales.count -gt 0) { | |
#write-host -ForegroundColor Yellow "Incoming stats: salescount: $($sales.count) - tranchescount: $($tranches.Count) " | |
$combos = Match-Combinations -sales $sales -tranches $tranches | |
$combocount += $combos.Count | |
$bestcombo = Match-BestCombo -combinations $combos | |
#write-host -ForegroundColor Green "match stats: combocount $($combos.count) - comboisnull $($null -eq $bestcombo) " | |
#$bestcombo | |
Apply-Sale -saletoprocess $bestcombo | |
} | |
$sales.count | |
$tranches.Count | |
#$bestcombos | |
"----" | |
#$tranchledger | |
"####" | |
$processedsales | Out-Numbers -FileNamePrefix "processed sales" | |
$tranches | out-numbers -FileNamePrefix "tranches" | |
$tranchledger | Sort-Object tranchid, version | out-numbers -FileNamePrefix "tranchledger" | |
Write-Host -ForegroundColor Yellow "Combinations analyzed: $combocount" | |
#for actual match, have the same be a combo of sale,tranch and number like -1 and -2 if split | |
#(done) TODO: cost would need to be prorated when not full match (maybe ratio it ) - lets not prorate fees but just apply to thefirst one | |
#(done) TODO: canT prorate parts of a share, have to round down | |
#deduct fees different , its messing with the math | |
#todo, change to gaintype...so its a column | |
#putting sale ID in ledger | |
#add a sale ledger too |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment