
Cost Basis Engine
- 190 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
cost-basis-engine is a Claude Code skill that computes crypto cost basis via specific identification, FIFO, LIFO, HIFO, and proportional average cost with partial-sell handling.
About
cost-basis-engine is a Claude Code skill that computes crypto cost basis using FIFO, LIFO, HIFO, Specific Identification, and average-cost methods, then compares the resulting tax liability across methods. It handles partial sells and on-chain events like airdrops, staking rewards, and LP entry/exit. A developer uses it when building tax-lot accounting for a crypto trading system. It runs on the Python standard library with no external dependencies.
- Computes cost basis via FIFO, LIFO, HIFO, Specific ID, and Average Cost
- Handles partial sells and on-chain events (airdrops, staking, LP entry/exit, migrations)
- Standard-library only, no external dependencies
Cost Basis Engine by the numbers
- 190 all-time installs (skills.sh)
- Ranked #486 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
cost-basis-engine capabilities & compatibility
Free; standard-library Python only, no API keys or dependencies.
- Capabilities
- cost basis calculation · tax lot accounting · realized gains · fifo lifo hifo · tax liability comparison
- Use cases
- trading · data analysis
- Runs
- Runs locally
- Pricing
- Free
What cost-basis-engine says it does
Multi-method cost basis computation including specific identification, FIFO, LIFO, HIFO, and proportional average cost with partial sell handling
No external dependencies required (standard library only)
This skill provides computational tools for informational purposes only. It does not constitute tax, legal, or financial advice.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill cost-basis-engineAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 190 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Compute crypto cost basis across FIFO/LIFO/HIFO/SpecificID/average methods and compare tax liability.
Who is it for?
Comparing realized-gain and tax liability across accounting methods for on-chain crypto trades.
Skip if: Serving as tax advice; the skill states it is informational only and to consult a tax professional.
When should I use this skill?
You need to compute cost basis and realized gains for crypto trades across multiple accounting methods.
What you get
Per-method realized gains and a tax-liability comparison across FIFO, LIFO, HIFO, Specific ID, and average cost.
By the numbers
- Five cost-basis methods (FIFO, LIFO, HIFO, Specific ID, Average Cost)
- No external dependencies (standard library only)
- Requires Python 3.10+
Files
Cost Basis Engine
Compute cost basis for crypto trades using multiple accounting methods and compare the resulting tax liability across methods. This skill handles the full complexity of on-chain activity: partial sells, token migrations, airdrops, staking rewards, LP entry/exit, and multi-hop swaps.
Disclaimer: This skill provides computational tools for informational purposes only. It does not constitute tax, legal, or financial advice. Consult a qualified tax professional for your specific situation. Tax law varies by jurisdiction and changes frequently.
Prerequisites
- Python 3.10+
- No external dependencies required (standard library only)
- Trade history as a list of dicts or CSV with columns:
date,action,token,quantity,price_usd,fee_usd
Methods Overview
| Method | Logic | Best For |
|---|---|---|
| FIFO | First lots purchased are sold first | Simplicity, many jurisdictions' default |
| LIFO | Last lots purchased are sold first | Deferring gains when prices rise over time |
| HIFO | Highest-cost lots are sold first | Minimizing current tax liability |
| Specific ID | Trader selects which lots to sell | Maximum control, requires record-keeping |
| Average Cost | Weighted average of all held lots | Simplicity, required in some jurisdictions |
---
1. FIFO (First-In, First-Out)
Sell the oldest lots first. This is the default method in the US if no other method is elected.
def fifo_sell(lots: list[dict], sell_qty: float, sell_price: float) -> list[dict]:
"""Sell using FIFO. lots sorted oldest-first."""
remaining = sell_qty
realized = []
while remaining > 0 and lots:
lot = lots[0]
used = min(lot["qty"], remaining)
gain = (sell_price - lot["cost_per_unit"]) * used
realized.append({"qty": used, "basis": lot["cost_per_unit"], "gain": gain})
lot["qty"] -= used
remaining -= used
if lot["qty"] <= 0:
lots.pop(0)
return realizedPartial sell example
You hold three lots of TOKEN:
- Lot A: 100 units @ $1.00 (oldest)
- Lot B: 50 units @ $2.00
- Lot C: 75 units @ $1.50
You sell 120 units at $3.00:
- 100 from Lot A: gain = (3.00 - 1.00) * 100 = $200
- 20 from Lot B: gain = (3.00 - 2.00) * 20 = $20
- Total realized gain: $220
- Lot B remainder: 30 units @ $2.00
---
2. LIFO (Last-In, First-Out)
Sell the newest lots first. Reverses the order compared to FIFO.
def lifo_sell(lots: list[dict], sell_qty: float, sell_price: float) -> list[dict]:
"""Sell using LIFO. Pops from end (newest first)."""
remaining = sell_qty
realized = []
while remaining > 0 and lots:
lot = lots[-1]
used = min(lot["qty"], remaining)
gain = (sell_price - lot["cost_per_unit"]) * used
realized.append({"qty": used, "basis": lot["cost_per_unit"], "gain": gain})
lot["qty"] -= used
remaining -= used
if lot["qty"] <= 0:
lots.pop()
return realizedUsing the same lots and selling 120 at $3.00 with LIFO:
- 75 from Lot C: gain = (3.00 - 1.50) * 75 = $112.50
- 45 from Lot B: gain = (3.00 - 2.00) * 45 = $45
- Total realized gain: $157.50
---
3. HIFO (Highest-In, First-Out)
Sell the highest-cost lots first to minimize realized gains.
def hifo_sell(lots: list[dict], sell_qty: float, sell_price: float) -> list[dict]:
"""Sell using HIFO. Sort by cost descending, consume highest first."""
lots.sort(key=lambda x: x["cost_per_unit"], reverse=True)
remaining = sell_qty
realized = []
for lot in lots:
if remaining <= 0:
break
used = min(lot["qty"], remaining)
gain = (sell_price - lot["cost_per_unit"]) * used
realized.append({"qty": used, "basis": lot["cost_per_unit"], "gain": gain})
lot["qty"] -= used
remaining -= used
lots[:] = [l for l in lots if l["qty"] > 0]
return realizedSame lots, selling 120 at $3.00 with HIFO:
- 50 from Lot B ($2.00, highest): gain = (3.00 - 2.00) * 50 = $50
- 70 from Lot C ($1.50, next highest): gain = (3.00 - 1.50) * 70 = $105
- Total realized gain: $155
- Remaining: Lot A 100 @ $1.00, Lot C 5 @ $1.50
---
4. Specific Identification
The trader explicitly selects which lots to sell. Provides maximum control but requires meticulous record-keeping. Each lot must be uniquely identifiable (e.g., by purchase date and time, or a lot ID).
def specific_id_sell(lots: dict[str, dict], lot_ids: list[tuple[str, float]],
sell_price: float) -> list[dict]:
"""Sell specific lots by ID. lot_ids = [(lot_id, qty_to_sell), ...]"""
realized = []
for lot_id, sell_qty in lot_ids:
lot = lots[lot_id]
used = min(lot["qty"], sell_qty)
gain = (sell_price - lot["cost_per_unit"]) * used
realized.append({"lot_id": lot_id, "qty": used, "basis": lot["cost_per_unit"], "gain": gain})
lot["qty"] -= used
if lot["qty"] <= 0:
del lots[lot_id]
return realized---
5. Proportional / Average Cost Method
Compute a single weighted-average cost per unit across all held lots. Every sell uses that average cost. The average updates after each buy.
def average_cost_basis(lots: list[dict]) -> float:
"""Compute weighted average cost per unit across all lots."""
total_cost = sum(l["qty"] * l["cost_per_unit"] for l in lots)
total_qty = sum(l["qty"] for l in lots)
if total_qty == 0:
return 0.0
return total_cost / total_qty
def average_cost_sell(lots: list[dict], sell_qty: float, sell_price: float) -> dict:
"""Sell using average cost. Reduces all lots proportionally."""
avg = average_cost_basis(lots)
total_qty = sum(l["qty"] for l in lots)
sell_qty = min(sell_qty, total_qty)
gain = (sell_price - avg) * sell_qty
# Reduce each lot proportionally
ratio = sell_qty / total_qty
for lot in lots:
lot["qty"] *= (1 - ratio)
lots[:] = [l for l in lots if l["qty"] > 1e-12]
return {"qty": sell_qty, "avg_basis": avg, "gain": gain}Partial sell with average cost
Lots: 100 @ $1.00, 50 @ $2.00, 75 @ $1.50. Total: 225 units, total cost $312.50.
Average cost = $312.50 / 225 = $1.3889/unit
Sell 120 at $3.00: gain = (3.00 - 1.3889) 120 = $193.33*
After the sell, 105 units remain at the same $1.3889 average.
---
6. Special Events
Airdrops
Airdrops are treated as income at fair market value (FMV) on the date received. The FMV becomes the cost basis for future sales.
airdrop_lot = {
"date": "2025-03-15",
"qty": 1000,
"cost_per_unit": 0.05, # FMV at time of receipt
"income_recognized": 50.0, # 1000 * 0.05 reported as income
"source": "airdrop"
}Staking Rewards
Staking rewards are income at FMV when received (similar to airdrops). Each reward event creates a new lot.
staking_lot = {
"date": "2025-04-01",
"qty": 5.2,
"cost_per_unit": 150.0, # SOL price at receipt
"income_recognized": 780.0,
"source": "staking_reward"
}Token Splits and Migrations
A token split or migration (old token to new token 1:1 or N:M) is generally not a taxable event. The total cost basis transfers to the new tokens.
def apply_split(lots: list[dict], split_ratio: float) -> None:
"""Apply a token split. split_ratio > 1 means more tokens."""
for lot in lots:
lot["qty"] *= split_ratio
lot["cost_per_unit"] /= split_ratioFor a 1:10 split of 100 tokens @ $5.00: result is 1000 tokens @ $0.50. Total basis unchanged at $500.
---
7. LP Entry/Exit as Token Swaps
Entering an LP position is treated as selling the deposited tokens and receiving LP tokens. Exiting is the reverse.
LP Entry (deposit 10 SOL + 1500 USDC into SOL/USDC pool): 1. Dispose of 10 SOL at current FMV → capital gain/loss event 2. Dispose of 1500 USDC at current FMV → usually negligible gain/loss 3. Receive LP tokens with cost basis = FMV of deposited assets
LP Exit (redeem LP tokens for 12 SOL + 1400 USDC): 1. Dispose of LP tokens at FMV of received assets → capital gain/loss 2. Receive 12 SOL with cost basis = FMV at redemption 3. Receive 1400 USDC with cost basis = FMV at redemption
def lp_entry(sol_qty: float, sol_price: float, usdc_qty: float,
lp_tokens_received: float) -> dict:
"""Model LP entry as disposal of component tokens."""
total_value = sol_qty * sol_price + usdc_qty * 1.0
lp_cost_basis = total_value / lp_tokens_received
return {
"disposals": [
{"token": "SOL", "qty": sol_qty, "price": sol_price},
{"token": "USDC", "qty": usdc_qty, "price": 1.0},
],
"lp_lot": {"qty": lp_tokens_received, "cost_per_unit": lp_cost_basis}
}---
8. Multi-Hop Swaps
A multi-hop swap (e.g., SOL -> USDC -> TOKEN) creates multiple taxable events, one for each intermediate step. Jupiter often routes through intermediate tokens.
def multi_hop_events(hops: list[dict]) -> list[dict]:
"""
Each hop is: {"sell_token", "sell_qty", "sell_price",
"buy_token", "buy_qty", "buy_price"}
Each hop is a separate taxable event.
"""
events = []
for i, hop in enumerate(hops):
events.append({
"event": i + 1,
"dispose": hop["sell_token"],
"dispose_qty": hop["sell_qty"],
"dispose_value": hop["sell_qty"] * hop["sell_price"],
"acquire": hop["buy_token"],
"acquire_qty": hop["buy_qty"],
"acquire_basis": hop["buy_qty"] * hop["buy_price"],
})
return eventsExample: Swap 1 SOL ($150) -> 150 USDC -> 10,000 TOKEN ($0.015 each)
- Event 1: Dispose 1 SOL (basis vs. $150 proceeds) → gain/loss on SOL
- Event 2: Dispose 150 USDC (basis vs. $150 proceeds) → usually ~$0 gain
- Result: 10,000 TOKEN with cost basis = $0.015/unit
---
9. Comparison View
The core value of this skill: run the same trade history through all five methods and compare total realized gain and estimated tax liability.
methods = ["FIFO", "LIFO", "HIFO", "Specific ID", "Average Cost"]
# After processing all trades through each method:
comparison = {
"FIFO": {"total_gain": 220.00, "tax_at_30pct": 66.00},
"LIFO": {"total_gain": 157.50, "tax_at_30pct": 47.25},
"HIFO": {"total_gain": 155.00, "tax_at_30pct": 46.50},
"Specific ID": {"total_gain": 160.00, "tax_at_30pct": 48.00},
"Average Cost":{"total_gain": 193.33, "tax_at_30pct": 58.00},
}
# HIFO minimizes liability in this exampleSee scripts/cost_basis_calculator.py for a full runnable comparison with realistic trade data including partial sells.
---
Quick Start
from scripts.cost_basis_calculator import CostBasisEngine
engine = CostBasisEngine()
# Add purchases
engine.add_buy("2025-01-10", "TOKEN", 100, 1.00)
engine.add_buy("2025-02-15", "TOKEN", 50, 2.00)
engine.add_buy("2025-03-01", "TOKEN", 75, 1.50)
# Sell and compare methods
results = engine.sell_compare("2025-04-01", "TOKEN", 120, 3.00)
engine.print_comparison(results)---
Use Cases
1. Tax season preparation: Run your full year of trades through all methods before choosing one to report. 2. Accumulation strategy: Track partial sells during DCA accumulation, see how each method affects remaining basis. 3. LP position tracking: Model LP entry/exit as swaps and capture the associated gain/loss events. 4. Airdrop and staking income: Properly record income events and set cost basis for future disposals. 5. Multi-hop swap decomposition: Break down Jupiter routes into individual taxable events.
---
Files
| File | Description |
|---|---|
references/planned_features.md | Method formulas, partial sell worked examples, special event handling, multi-hop treatment |
scripts/cost_basis_calculator.py | Full engine with all 5 methods, comparison table, demo mode with realistic trades |
---
Remember: The "best" method depends on your jurisdiction, your specific trade history, and your tax situation. This engine helps you compare — a tax professional helps you decide.
Cost Basis Engine — Method Reference
1. Method Formulas
1.1 FIFO (First-In, First-Out)
Lots are ordered by acquisition date ascending. On each sell, consume from the oldest lot first.
lots = sorted(lots, key=acquisition_date, ascending=True)
for each sell event (qty_sell, price_sell):
remaining = qty_sell
while remaining > 0:
lot = lots[0] # oldest
used = min(lot.qty, remaining)
realized_gain += (price_sell - lot.cost_per_unit) * used
lot.qty -= used
remaining -= used
if lot.qty == 0: remove lot1.2 LIFO (Last-In, First-Out)
Lots ordered by acquisition date descending. On each sell, consume from the newest lot first.
lots = sorted(lots, key=acquisition_date, descending=True)
# Same consumption loop as FIFO but starting from newest1.3 HIFO (Highest-In, First-Out)
Lots ordered by cost per unit descending. On each sell, consume the highest-cost lot first. This minimizes realized gain (or maximizes realized loss).
lots = sorted(lots, key=cost_per_unit, descending=True)
# Same consumption loop, starting from highest cost1.4 Specific Identification
Trader explicitly designates which lots to sell. Each lot requires a unique identifier (typically acquisition date + time or a sequential ID). The trader must make the designation at the time of sale and maintain records.
for each (lot_id, qty_to_sell) in designation:
lot = lookup(lot_id)
gain = (sell_price - lot.cost_per_unit) * qty_to_sell
lot.qty -= qty_to_sell1.5 Average Cost (Proportional)
Maintain a running weighted average cost. All units are fungible at the average cost.
avg_cost = sum(lot.qty * lot.cost_per_unit for lot in lots) / sum(lot.qty for lot in lots)
On sell of qty_sell at price_sell:
gain = (price_sell - avg_cost) * qty_sell
total_qty -= qty_sell
# avg_cost remains unchanged until next buy
On buy of qty_buy at price_buy:
new_total_cost = avg_cost * old_qty + price_buy * qty_buy
new_total_qty = old_qty + qty_buy
avg_cost = new_total_cost / new_total_qty---
2. Partial Sell Worked Examples
Starting position for all examples:
| Lot | Date | Qty | Cost/Unit | Total Cost |
|---|---|---|---|---|
| A | 2025-01-10 | 100 | $1.00 | $100.00 |
| B | 2025-02-15 | 50 | $2.00 | $100.00 |
| C | 2025-03-01 | 75 | $1.50 | $112.50 |
Total: 225 units, $312.50 total cost, $1.3889 average cost.
Sell event: 120 units at $3.00 on 2025-04-01. Proceeds = $360.00.
2.1 FIFO Partial Sell
1. Consume Lot A entirely: 100 units. Gain = (3.00 - 1.00) 100 = $200.00 2. Consume 20 from Lot B: Gain = (3.00 - 2.00) 20 = $20.00
| Result | Value |
|---|---|
| Total gain | $220.00 |
| Lots remaining | B: 30 @ $2.00, C: 75 @ $1.50 |
| Remaining basis | $172.50 |
2.2 LIFO Partial Sell
1. Consume Lot C entirely: 75 units. Gain = (3.00 - 1.50) 75 = $112.50 2. Consume 45 from Lot B: Gain = (3.00 - 2.00) 45 = $45.00
| Result | Value |
|---|---|
| Total gain | $157.50 |
| Lots remaining | A: 100 @ $1.00, B: 5 @ $2.00 |
| Remaining basis | $110.00 |
2.3 HIFO Partial Sell
Sorted by cost: B ($2.00) > C ($1.50) > A ($1.00).
1. Consume Lot B entirely: 50 units. Gain = (3.00 - 2.00) 50 = $50.00 2. Consume 70 from Lot C: Gain = (3.00 - 1.50) 70 = $105.00
| Result | Value |
|---|---|
| Total gain | $155.00 |
| Lots remaining | A: 100 @ $1.00, C: 5 @ $1.50 |
| Remaining basis | $107.50 |
2.4 Average Cost Partial Sell
Average cost = $312.50 / 225 = $1.3889.
Gain = (3.00 - 1.3889) 120 = $193.33*
| Result | Value |
|---|---|
| Total gain | $193.33 |
| Remaining qty | 105 units |
| Remaining avg cost | $1.3889 (unchanged) |
| Remaining basis | $145.83 |
2.5 Method Comparison Summary
| Method | Realized Gain | Tax @ 30% | Remaining Basis |
|---|---|---|---|
| FIFO | $220.00 | $66.00 | $172.50 |
| LIFO | $157.50 | $47.25 | $110.00 |
| HIFO | $155.00 | $46.50 | $107.50 |
| Average | $193.33 | $58.00 | $145.83 |
HIFO minimizes current tax liability. Note that remaining basis is also lowest under HIFO, meaning future sells will have higher gains — HIFO defers tax, it does not eliminate it.
---
3. Special Event Handling
3.1 Airdrops
- Tax treatment: Income at FMV on date of receipt.
- Cost basis: FMV at receipt becomes the cost basis for the new lot.
- Formula:
income = qty_received * fmv_at_receipt - If FMV is zero or indeterminate at receipt, cost basis is $0 and the full proceeds on any future sale are gain.
3.2 Staking Rewards
- Tax treatment: Income at FMV when the reward is received (i.e., when the tokens become available for withdrawal or transfer).
- Cost basis: Same as airdrops — FMV at receipt.
- Frequency: Each staking reward event creates a separate lot. For validators receiving rewards every epoch, this can mean hundreds of micro-lots per year.
- Practical simplification: Batch rewards by day or week, using the average price over the period.
3.3 Token Splits and Migrations
- Tax treatment: Generally not a taxable event (analogous to stock splits).
- Cost basis: Total basis is preserved; per-unit basis adjusts inversely to the split ratio.
- Formula for N:M split:
new_qty = old_qty * (M / N)
new_cost_per_unit = old_cost_per_unit * (N / M)
total_basis = unchanged- Migration (1:1 swap to new token contract): Same treatment. Old token lots transfer directly to new token with identical basis and acquisition dates.
3.4 Hard Forks
- If a fork produces a new token with value, the IRS (US) position is that the new token has a cost basis of $0 and FMV at receipt is income. Treatment varies by jurisdiction.
---
4. LP Entry/Exit Treatment
4.1 LP Entry (Deposit)
Depositing tokens into an LP is treated as a disposal of the component tokens:
1. Dispose Token A: qty_a at FMV → realize gain/loss vs. existing basis 2. Dispose Token B: qty_b at FMV → realize gain/loss vs. existing basis 3. Receive LP tokens: cost basis = total FMV of deposited assets
lp_basis = (qty_a * price_a) + (qty_b * price_b)
lp_cost_per_token = lp_basis / lp_tokens_received4.2 LP Exit (Withdrawal)
Redeeming LP tokens is treated as disposing the LP tokens and acquiring the component tokens:
1. Dispose LP tokens: at FMV of received assets → realize gain/loss vs. LP basis 2. Receive Token A: cost basis = FMV at redemption 3. Receive Token B: cost basis = FMV at redemption
redemption_value = (qty_a_out * price_a) + (qty_b_out * price_b)
lp_gain = redemption_value - (lp_tokens_redeemed * lp_cost_per_token)4.3 Impermanent Loss Note
Impermanent loss is embedded in the LP gain/loss calculation. It is not a separate tax event — it shows up as a lower redemption value compared to holding the original tokens.
---
5. Multi-Hop Swap Treatment
5.1 Why It Matters
DEX aggregators like Jupiter route swaps through intermediate tokens for best pricing. A swap SOL -> TOKEN might actually execute as SOL -> USDC -> TOKEN. Each intermediate swap is a separate taxable event.
5.2 Event Decomposition
Example: Swap 1 SOL ($150) -> USDC -> TOKEN
| Event | Dispose | Qty | Proceeds | Acquire | Qty | Basis |
|---|---|---|---|---|---|---|
| 1 | SOL | 1 | $150.00 | USDC | 150 | $150.00 |
| 2 | USDC | 150 | $150.00 | TOKEN | 10,000 | $150.00 |
- Event 1: Gain/loss on SOL depends on your SOL cost basis
- Event 2: USDC gain/loss is typically negligible ($0 if basis = $1.00)
- Final: 10,000 TOKEN with basis $0.015/unit
5.3 Practical Concern
Multi-hop routes can create unexpected tax events even when the trader intended a single swap. A 3-hop route creates 3 taxable events. When fetching swap transaction details from Jupiter or on-chain, decompose the full route to capture all intermediate disposals.
5.4 Stablecoin Intermediaries
When USDC or USDT is the intermediate token, the gain/loss on the stablecoin leg is usually near zero. However, if stablecoins were acquired at a price other than $1.00 (e.g., during a depeg), there may be a non-trivial gain or loss on that leg.
---
6. Holding Period Considerations
- Short-term: Held <= 1 year. Taxed at ordinary income rates in many jurisdictions.
- Long-term: Held > 1 year. Often taxed at reduced capital gains rates.
- FIFO tends to produce more long-term gains (oldest lots first).
- LIFO and HIFO tend to produce more short-term gains (newer or recently-priced lots first).
- The holding period analysis adds another dimension to method comparison beyond just realized gain amounts.
---
7. Record-Keeping Requirements
For specific identification, the trader must: 1. Identify the specific lot at the time of sale (not retroactively) 2. Maintain records showing which lots were designated 3. Receive confirmation from the exchange/platform (where applicable)
For all methods, maintain:
- Date and time of every acquisition and disposal
- Quantity and price for each transaction
- Fee amounts (fees adjust basis or proceeds)
- Source of acquisition (purchase, airdrop, staking, LP redemption)
#!/usr/bin/env python3
"""Cost basis calculator supporting FIFO, LIFO, HIFO, Specific ID, and Average Cost.
Computes cost basis under all five methods for the same trade history,
then displays a comparison table showing which method minimizes tax liability.
Usage:
python scripts/cost_basis_calculator.py
python scripts/cost_basis_calculator.py --demo
Dependencies:
None (standard library only)
Environment Variables:
None required.
"""
import argparse
import copy
import sys
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
# ── Data Models ─────────────────────────────────────────────────────
@dataclass
class Lot:
"""A single tax lot representing an acquisition of tokens."""
lot_id: str
date: str
token: str
qty: float
cost_per_unit: float
source: str = "purchase" # purchase, airdrop, staking, lp_redemption
@property
def total_cost(self) -> float:
return self.qty * self.cost_per_unit
def __repr__(self) -> str:
return (f"Lot({self.lot_id}: {self.qty:.4f} {self.token} "
f"@ ${self.cost_per_unit:.4f}, {self.date})")
@dataclass
class RealizedGain:
"""A single realized gain/loss event from a sale."""
sell_date: str
token: str
qty: float
proceeds_per_unit: float
cost_per_unit: float
lot_id: str
source: str = "purchase"
@property
def proceeds(self) -> float:
return self.qty * self.proceeds_per_unit
@property
def basis(self) -> float:
return self.qty * self.cost_per_unit
@property
def gain(self) -> float:
return self.proceeds - self.basis
@dataclass
class MethodResult:
"""Result of processing all trades under a single method."""
method: str
realized_gains: list[RealizedGain] = field(default_factory=list)
remaining_lots: list[Lot] = field(default_factory=list)
@property
def total_gain(self) -> float:
return sum(g.gain for g in self.realized_gains)
@property
def total_proceeds(self) -> float:
return sum(g.proceeds for g in self.realized_gains)
@property
def total_basis_used(self) -> float:
return sum(g.basis for g in self.realized_gains)
@property
def remaining_basis(self) -> float:
return sum(lot.total_cost for lot in self.remaining_lots)
# ── Trade Record ────────────────────────────────────────────────────
@dataclass
class Trade:
"""A single trade action."""
date: str
action: str # buy, sell, airdrop, staking_reward, split
token: str
qty: float
price_usd: float
fee_usd: float = 0.0
lot_ids: Optional[list[tuple[str, float]]] = None # For specific ID sells
split_ratio: float = 1.0 # For splits
# ── Core Engine ─────────────────────────────────────────────────────
class CostBasisEngine:
"""Multi-method cost basis computation engine."""
def __init__(self) -> None:
self._trades: list[Trade] = []
self._lot_counter: int = 0
def _next_lot_id(self) -> str:
self._lot_counter += 1
return f"L{self._lot_counter:04d}"
def add_buy(self, date: str, token: str, qty: float, price_usd: float,
fee_usd: float = 0.0) -> None:
"""Record a token purchase."""
self._trades.append(Trade(date=date, action="buy", token=token,
qty=qty, price_usd=price_usd, fee_usd=fee_usd))
def add_sell(self, date: str, token: str, qty: float, price_usd: float,
fee_usd: float = 0.0,
lot_ids: Optional[list[tuple[str, float]]] = None) -> None:
"""Record a token sale. lot_ids used only for specific identification."""
self._trades.append(Trade(date=date, action="sell", token=token,
qty=qty, price_usd=price_usd, fee_usd=fee_usd,
lot_ids=lot_ids))
def add_airdrop(self, date: str, token: str, qty: float,
fmv_usd: float) -> None:
"""Record an airdrop (income at FMV)."""
self._trades.append(Trade(date=date, action="airdrop", token=token,
qty=qty, price_usd=fmv_usd))
def add_staking_reward(self, date: str, token: str, qty: float,
fmv_usd: float) -> None:
"""Record a staking reward (income at FMV)."""
self._trades.append(Trade(date=date, action="staking_reward",
token=token, qty=qty, price_usd=fmv_usd))
def add_split(self, date: str, token: str, split_ratio: float) -> None:
"""Record a token split (e.g., 10.0 for 1:10 split)."""
self._trades.append(Trade(date=date, action="split", token=token,
qty=0, price_usd=0, split_ratio=split_ratio))
# ── Method Implementations ──────────────────────────────────────
def _process_fifo(self, trades: list[Trade]) -> MethodResult:
"""Process trades using FIFO method."""
lots: list[Lot] = []
result = MethodResult(method="FIFO")
for trade in trades:
if trade.action in ("buy", "airdrop", "staking_reward"):
cost = trade.price_usd + (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
lots.append(Lot(lot_id=self._next_lot_id(), date=trade.date,
token=trade.token, qty=trade.qty,
cost_per_unit=cost, source=trade.action))
elif trade.action == "sell":
token_lots = [l for l in lots if l.token == trade.token]
token_lots.sort(key=lambda l: l.date) # oldest first
remaining = trade.qty
sell_price = trade.price_usd - (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
for lot in token_lots:
if remaining <= 0:
break
used = min(lot.qty, remaining)
result.realized_gains.append(RealizedGain(
sell_date=trade.date, token=trade.token, qty=used,
proceeds_per_unit=sell_price, cost_per_unit=lot.cost_per_unit,
lot_id=lot.lot_id, source=lot.source))
lot.qty -= used
remaining -= used
lots[:] = [l for l in lots if l.qty > 1e-12]
elif trade.action == "split":
for lot in lots:
if lot.token == trade.token:
lot.qty *= trade.split_ratio
lot.cost_per_unit /= trade.split_ratio
result.remaining_lots = [l for l in lots if l.qty > 1e-12]
return result
def _process_lifo(self, trades: list[Trade]) -> MethodResult:
"""Process trades using LIFO method."""
lots: list[Lot] = []
result = MethodResult(method="LIFO")
for trade in trades:
if trade.action in ("buy", "airdrop", "staking_reward"):
cost = trade.price_usd + (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
lots.append(Lot(lot_id=self._next_lot_id(), date=trade.date,
token=trade.token, qty=trade.qty,
cost_per_unit=cost, source=trade.action))
elif trade.action == "sell":
token_lots = [l for l in lots if l.token == trade.token]
token_lots.sort(key=lambda l: l.date, reverse=True) # newest first
remaining = trade.qty
sell_price = trade.price_usd - (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
for lot in token_lots:
if remaining <= 0:
break
used = min(lot.qty, remaining)
result.realized_gains.append(RealizedGain(
sell_date=trade.date, token=trade.token, qty=used,
proceeds_per_unit=sell_price, cost_per_unit=lot.cost_per_unit,
lot_id=lot.lot_id, source=lot.source))
lot.qty -= used
remaining -= used
lots[:] = [l for l in lots if l.qty > 1e-12]
elif trade.action == "split":
for lot in lots:
if lot.token == trade.token:
lot.qty *= trade.split_ratio
lot.cost_per_unit /= trade.split_ratio
result.remaining_lots = [l for l in lots if l.qty > 1e-12]
return result
def _process_hifo(self, trades: list[Trade]) -> MethodResult:
"""Process trades using HIFO (Highest-In, First-Out) method."""
lots: list[Lot] = []
result = MethodResult(method="HIFO")
for trade in trades:
if trade.action in ("buy", "airdrop", "staking_reward"):
cost = trade.price_usd + (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
lots.append(Lot(lot_id=self._next_lot_id(), date=trade.date,
token=trade.token, qty=trade.qty,
cost_per_unit=cost, source=trade.action))
elif trade.action == "sell":
token_lots = [l for l in lots if l.token == trade.token]
token_lots.sort(key=lambda l: l.cost_per_unit, reverse=True) # highest cost first
remaining = trade.qty
sell_price = trade.price_usd - (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
for lot in token_lots:
if remaining <= 0:
break
used = min(lot.qty, remaining)
result.realized_gains.append(RealizedGain(
sell_date=trade.date, token=trade.token, qty=used,
proceeds_per_unit=sell_price, cost_per_unit=lot.cost_per_unit,
lot_id=lot.lot_id, source=lot.source))
lot.qty -= used
remaining -= used
lots[:] = [l for l in lots if l.qty > 1e-12]
elif trade.action == "split":
for lot in lots:
if lot.token == trade.token:
lot.qty *= trade.split_ratio
lot.cost_per_unit /= trade.split_ratio
result.remaining_lots = [l for l in lots if l.qty > 1e-12]
return result
def _process_average(self, trades: list[Trade]) -> MethodResult:
"""Process trades using Average Cost method."""
avg_costs: dict[str, float] = {}
quantities: dict[str, float] = {}
result = MethodResult(method="Average Cost")
for trade in trades:
token = trade.token
if trade.action in ("buy", "airdrop", "staking_reward"):
cost = trade.price_usd + (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
old_qty = quantities.get(token, 0.0)
old_avg = avg_costs.get(token, 0.0)
new_qty = old_qty + trade.qty
if new_qty > 0:
avg_costs[token] = (old_avg * old_qty + cost * trade.qty) / new_qty
quantities[token] = new_qty
elif trade.action == "sell":
avg = avg_costs.get(token, 0.0)
sell_qty = min(trade.qty, quantities.get(token, 0.0))
sell_price = trade.price_usd - (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
if sell_qty > 0:
result.realized_gains.append(RealizedGain(
sell_date=trade.date, token=token, qty=sell_qty,
proceeds_per_unit=sell_price, cost_per_unit=avg,
lot_id="AVG", source="average"))
quantities[token] = quantities.get(token, 0.0) - sell_qty
elif trade.action == "split":
if token in quantities and quantities[token] > 0:
quantities[token] *= trade.split_ratio
avg_costs[token] /= trade.split_ratio
for token, qty in quantities.items():
if qty > 1e-12:
result.remaining_lots.append(Lot(
lot_id="AVG", date="aggregate", token=token,
qty=qty, cost_per_unit=avg_costs.get(token, 0.0),
source="average"))
return result
def _process_specific_id(self, trades: list[Trade]) -> MethodResult:
"""Process trades using Specific Identification.
Falls back to HIFO ordering when lot_ids are not specified on a sell.
"""
lots: list[Lot] = []
lot_map: dict[str, Lot] = {}
result = MethodResult(method="Specific ID")
for trade in trades:
if trade.action in ("buy", "airdrop", "staking_reward"):
cost = trade.price_usd + (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
lot = Lot(lot_id=self._next_lot_id(), date=trade.date,
token=trade.token, qty=trade.qty,
cost_per_unit=cost, source=trade.action)
lots.append(lot)
lot_map[lot.lot_id] = lot
elif trade.action == "sell":
sell_price = trade.price_usd - (trade.fee_usd / trade.qty if trade.qty > 0 else 0)
if trade.lot_ids:
for lot_id, qty_to_sell in trade.lot_ids:
if lot_id in lot_map:
lot = lot_map[lot_id]
used = min(lot.qty, qty_to_sell)
result.realized_gains.append(RealizedGain(
sell_date=trade.date, token=trade.token,
qty=used, proceeds_per_unit=sell_price,
cost_per_unit=lot.cost_per_unit,
lot_id=lot.lot_id, source=lot.source))
lot.qty -= used
else:
# Fallback: HIFO ordering when no specific lots designated
token_lots = [l for l in lots if l.token == trade.token and l.qty > 1e-12]
token_lots.sort(key=lambda l: l.cost_per_unit, reverse=True)
remaining = trade.qty
for lot in token_lots:
if remaining <= 0:
break
used = min(lot.qty, remaining)
result.realized_gains.append(RealizedGain(
sell_date=trade.date, token=trade.token,
qty=used, proceeds_per_unit=sell_price,
cost_per_unit=lot.cost_per_unit,
lot_id=lot.lot_id, source=lot.source))
lot.qty -= used
remaining -= used
lots[:] = [l for l in lots if l.qty > 1e-12]
lot_map = {l.lot_id: l for l in lots}
elif trade.action == "split":
for lot in lots:
if lot.token == trade.token:
lot.qty *= trade.split_ratio
lot.cost_per_unit /= trade.split_ratio
result.remaining_lots = [l for l in lots if l.qty > 1e-12]
return result
# ── Public API ──────────────────────────────────────────────────
def compute_all_methods(self) -> dict[str, MethodResult]:
"""Run all trades through every method and return results."""
self._lot_counter = 0
results: dict[str, MethodResult] = {}
for name, processor in [
("FIFO", self._process_fifo),
("LIFO", self._process_lifo),
("HIFO", self._process_hifo),
("Average Cost", self._process_average),
("Specific ID", self._process_specific_id),
]:
self._lot_counter = 0
results[name] = processor(list(self._trades))
return results
def sell_compare(self, date: str, token: str, qty: float,
price_usd: float) -> dict[str, MethodResult]:
"""Add a sell and compute comparison across all methods."""
self.add_sell(date, token, qty, price_usd)
return self.compute_all_methods()
def print_comparison(self, results: dict[str, MethodResult],
tax_rate: float = 0.30) -> None:
"""Print a formatted comparison table."""
print("\n" + "=" * 78)
print("COST BASIS METHOD COMPARISON")
print("=" * 78)
print(f"{'Method':<16} {'Proceeds':>12} {'Basis Used':>12} "
f"{'Gain/Loss':>12} {'Tax @{:.0%}'.format(tax_rate):>12} "
f"{'Rem. Basis':>12}")
print("-" * 78)
best_method = ""
best_tax = float("inf")
for name, res in results.items():
tax = res.total_gain * tax_rate if res.total_gain > 0 else 0.0
print(f"{name:<16} {res.total_proceeds:>12,.2f} "
f"{res.total_basis_used:>12,.2f} "
f"{res.total_gain:>12,.2f} {tax:>12,.2f} "
f"{res.remaining_basis:>12,.2f}")
if tax < best_tax:
best_tax = tax
best_method = name
print("-" * 78)
print(f">>> Lowest current liability: {best_method} "
f"(${best_tax:,.2f} estimated tax)")
print("=" * 78)
def print_detailed(self, result: MethodResult) -> None:
"""Print detailed lot-by-lot gains for a single method."""
print(f"\n--- {result.method} Detail ---")
for g in result.realized_gains:
direction = "GAIN" if g.gain >= 0 else "LOSS"
print(f" {g.sell_date} | Sell {g.qty:>10.4f} {g.token} | "
f"Proceeds ${g.proceeds:>10.2f} | Basis ${g.basis:>10.2f} | "
f"{direction} ${abs(g.gain):>10.2f} | Lot {g.lot_id}")
if result.remaining_lots:
print(f" Remaining lots:")
for lot in result.remaining_lots:
print(f" {lot.lot_id}: {lot.qty:.4f} {lot.token} "
f"@ ${lot.cost_per_unit:.4f} "
f"(basis ${lot.total_cost:.2f})")
# ── Demo Data ───────────────────────────────────────────────────────
def build_demo_trades() -> CostBasisEngine:
"""Build a realistic demo trade history with accumulation and partial sells.
Scenario: A trader accumulates SOL and a memecoin (BONK) over several
months, takes partial profits, receives staking rewards and an airdrop,
and experiences a token split.
Returns:
Configured CostBasisEngine with all trades loaded.
"""
engine = CostBasisEngine()
# ── SOL accumulation (DCA pattern) ──────────────────────────────
engine.add_buy("2025-01-05", "SOL", 10.0, 95.00) # Buy 10 SOL @ $95
engine.add_buy("2025-01-20", "SOL", 8.0, 105.00) # Buy 8 SOL @ $105
engine.add_buy("2025-02-10", "SOL", 12.0, 88.00) # Buy 12 SOL @ $88 (dip)
engine.add_buy("2025-03-01", "SOL", 5.0, 130.00) # Buy 5 SOL @ $130
# Staking rewards
engine.add_staking_reward("2025-02-01", "SOL", 0.15, 100.00)
engine.add_staking_reward("2025-03-01", "SOL", 0.18, 130.00)
# Partial sell: take profit on 15 SOL at $140
engine.add_sell("2025-03-15", "SOL", 15.0, 140.00, fee_usd=0.50)
# ── BONK accumulation ───────────────────────────────────────────
engine.add_buy("2025-01-10", "BONK", 5_000_000, 0.000025)
engine.add_buy("2025-02-01", "BONK", 3_000_000, 0.000040)
engine.add_buy("2025-02-20", "BONK", 2_000_000, 0.000018)
# Airdrop of BONK
engine.add_airdrop("2025-02-15", "BONK", 500_000, 0.000035)
# Partial sell: 4 million BONK at $0.000055 (price pump)
engine.add_sell("2025-03-10", "BONK", 4_000_000, 0.000055, fee_usd=0.10)
# ── WIF with token split ────────────────────────────────────────
engine.add_buy("2025-01-15", "WIF", 200, 2.50)
engine.add_buy("2025-02-05", "WIF", 100, 3.20)
# 1:5 token split
engine.add_split("2025-02-28", "WIF", 5.0)
# Sell 500 WIF (post-split) at $0.80
engine.add_sell("2025-03-20", "WIF", 500, 0.80)
return engine
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run the cost basis calculator."""
parser = argparse.ArgumentParser(
description="Cost basis calculator with multi-method comparison")
parser.add_argument("--demo", action="store_true",
help="Run with demo trade data")
parser.add_argument("--tax-rate", type=float, default=0.30,
help="Tax rate for liability estimation (default: 0.30)")
parser.add_argument("--detail", action="store_true",
help="Show detailed lot-by-lot breakdown for each method")
args = parser.parse_args()
if not args.demo:
print("Cost Basis Calculator")
print("Run with --demo to see a full example with realistic trades.")
print("In production, use the CostBasisEngine class directly.")
print("\nExample:")
print(" python scripts/cost_basis_calculator.py --demo")
print(" python scripts/cost_basis_calculator.py --demo --detail")
print(" python scripts/cost_basis_calculator.py --demo --tax-rate 0.37")
return
print("=" * 78)
print("COST BASIS ENGINE — DEMO")
print("=" * 78)
print()
print("Scenario: Trader DCA-accumulates SOL and BONK, receives staking")
print("rewards and an airdrop, experiences a WIF token split, and takes")
print("partial profits on each position.")
print()
print("NOTE: This is for informational purposes only. Consult a tax")
print("professional for advice on your specific situation.")
print()
engine = build_demo_trades()
results = engine.compute_all_methods()
# Print trade summary
print("Trade Summary:")
print("-" * 50)
tokens_traded = set()
buy_count = 0
sell_count = 0
for t in engine._trades:
tokens_traded.add(t.token)
if t.action == "buy":
buy_count += 1
elif t.action == "sell":
sell_count += 1
print(f" Tokens: {', '.join(sorted(tokens_traded))}")
print(f" Buy events: {buy_count}")
print(f" Sell events: {sell_count}")
print(f" Special events: staking rewards, airdrop, token split")
# Print comparison
engine.print_comparison(results, tax_rate=args.tax_rate)
# Print income events summary
print("\nINCOME EVENTS (taxed as ordinary income):")
print("-" * 50)
for t in engine._trades:
if t.action in ("airdrop", "staking_reward"):
income = t.qty * t.price_usd
label = "Airdrop" if t.action == "airdrop" else "Staking"
print(f" {t.date} | {label:>8} | {t.qty:>12.2f} {t.token} "
f"@ ${t.price_usd:.6f} = ${income:>10.2f} income")
if args.detail:
for name, res in results.items():
engine.print_detailed(res)
print()
print("Disclaimer: This output is for informational and educational")
print("purposes only. It does not constitute tax or financial advice.")
if __name__ == "__main__":
main()
Related skills
FAQ
Which cost-basis methods does it support?
FIFO, LIFO, HIFO, Specific Identification, and proportional average cost, with a comparison of tax liability across methods.
Does it require external libraries?
No. It uses the Python standard library only and needs Python 3.10+.