PublicFunctions/Get-RHCCostBasisSummary.ps1

function Get-RHCCostBasisSummary { 
 

    <#
        .SYNOPSIS
            Calculates the cost basis for all current crypto holdings.

        .DESCRIPTION
            Retrieves holdings and order history to calculate total cost basis,
            quantity, and average cost per unit for each asset.

        .PARAMETER ApiKey
            The API key for authenticating with the Robinhood Crypto API.
            If not specified, it will be retrieved from stored credentials.

        .PARAMETER PrivateKeySeed
            The private key seed used for signing the API request.
            If not specified, it will be retrieved from stored credentials.

        .PARAMETER AssetCodes
            Optional. One or more cryptocurrency asset codes (e.g., "BTC", "ETH") to filter the results.
            If not specified, returns cost basis for all holdings.

        .EXAMPLE
            Get-RHCCostBasisSummary

            Returns cost basis for all cryptocurrency holdings.

        .EXAMPLE
            Get-RHCCostBasisSummary -AssetCodes "BTC"

            Returns cost basis for only Bitcoin holdings.

        .EXAMPLE
            Get-RHCCostBasisSummary -AssetCodes "BTC","ETH"

            Returns cost basis for Bitcoin and Ethereum holdings.

        .NOTES
            When dealing with averages, the result may not be exact. Price change fluxuations over time could also cause inaccuracies.
            This is a best effort calculation based on the order history and current holdings, not transfers in/out.

            Also note that free or transferred crypto assets will show $0, as there is no order history to calculate from.
    #>


    [CmdletBinding()]

    Param (

        [Parameter(Mandatory = $false)]
        [string] $ApiKey = $(Get-RHCCredentials -ApiKey),

        [Parameter(Mandatory = $false)]
        [string] $PrivateKeySeed = $(Get-RHCCredentials -PrivateKeySeed),

        [Parameter(Mandatory = $false)]
        [string[]] $AssetCodes
    )

    Begin {

        Initialize-RHCRequirements | Out-Null

        $authParams = @{
            ApiKey         = $ApiKey
            PrivateKeySeed = $PrivateKeySeed
        }
    }

    Process {

        $holdingsParams = $authParams.Clone()

        if ($AssetCodes) {
            $holdingsParams.Add("AssetCodes", $AssetCodes)
        }

        $holdings = Get-RHCHoldings @holdingsParams

        if (-not $holdings -or -not $holdings.results) {
            Write-Warning "No holdings found."
            return $null
        }

        $results = @()

        foreach ($holding in $holdings.results) {

            $buyOrders = Get-RHCOrder @authParams -QueryParameters @{
                side   = "buy"
                state  = "filled"
                symbol = "$($holding.asset_code)-USD"
            }
            $buyLots = @()
            if ($buyOrders -and $buyOrders.results) {
                foreach ($order in ($buyOrders.results | Sort-Object created_at)) {
                    foreach ($exec in $order.executions) {
                        $buyLots += [PSCustomObject]@{
                            Quantity = [decimal]$exec.quantity
                            Cost     = [decimal]$exec.effective_price * [decimal]$exec.quantity
                        }
                    }
                }
            }


            $sellOrders = Get-RHCOrder @authParams -QueryParameters @{
                side   = "sell"
                state  = "filled"
                symbol = "$($holding.asset_code)-USD"
            }
            $sellExecs = @()
            if ($sellOrders -and $sellOrders.results) {
                foreach ($order in ($sellOrders.results | Sort-Object created_at)) {
                    foreach ($exec in $order.executions) {
                        $sellExecs += [PSCustomObject]@{
                            Quantity = [decimal]$exec.quantity
                        }
                    }
                }
            }

            # FIFO matters here
            foreach ($sell in $sellExecs) {
                $qtyToRemove = $sell.Quantity
                $newBuyLots = @()
                foreach ($lot in $buyLots) {

                    if ($qtyToRemove -le 0) {
                        $newBuyLots += $lot
                        continue
                    }

                    if ($lot.Quantity -le $qtyToRemove) {

                        $qtyToRemove -= $lot.Quantity
                    }
                    else {

                        $remainingQty = $lot.Quantity - $qtyToRemove
                        $remainingCost = $lot.Cost * ($remainingQty / $lot.Quantity)
                        $newBuyLots += [PSCustomObject]@{
                            Quantity = $remainingQty
                            Cost     = $remainingCost
                        }

                        $qtyToRemove = 0
                    }
                }

                $buyLots = $newBuyLots
            }

            $totalQuantity = ($buyLots | Measure-Object Quantity -Sum).Sum
            $totalCost = ($buyLots | Measure-Object Cost -Sum).Sum

            $avgCost = 0

            if ($totalQuantity -gt 0) {
                $avgCost = $totalCost / $totalQuantity
            }

            $results += [PSCustomObject]@{
                AssetCode          = $holding.asset_code
                CurrentQuantity    = [decimal]$holding.quantity
                TotalCost          = [math]::Round($totalCost, 2)
                AverageCostPerUnit = [math]::Round($avgCost, 8)
            }
        }

        return $results
    }
 
 };