Skip to content

Instantly share code, notes, and snippets.

@klumsy
Created September 30, 2018 00:32
Show Gist options
  • Save klumsy/10fbcc320e78f7d50d1820ee70b950ae to your computer and use it in GitHub Desktop.
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
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