Managing a cryptocurrency portfolio across multiple exchanges, wallets, and DeFi protocols can be challenging. This comprehensive guide shows you how to set up and maintain accurate cryptocurrency portfolio tracking using Beancount.io's powerful plain-text accounting system.
Explore a live cryptocurrency example ledger:
Why Traditional Portfolio Trackers Fall Short
Common Problems with Crypto Portfolio Tools
- Limited Exchange Support: Many tools don't support all exchanges or DeFi protocols
- Inaccurate Cost Basis: Simplified FIFO/LIFO calculations miss complex scenarios
- Missing Transactions: API limitations and manual entry gaps
- No Customization: Fixed categories and reporting structures
- Privacy Concerns: Sharing API keys with third-party services
- Vendor Lock-in: Proprietary data formats and limited export options
Beancount.io Advantages
- Complete Control: Own your data in plain-text format
- Unlimited Customization: Create any account structure you need
- Precise Cost Basis: Lot-based tracking with specific identification
- Multi-Protocol Support: Handle any exchange, wallet, or DeFi protocol
- Transparent Calculations: See exactly how numbers are computed
- Future-Proof: Plain-text format ensures long-term accessibility
Setting Up Your Crypto Portfolio Structure
Basic Account Hierarchy
Start with a comprehensive account structure that reflects your crypto ecosystem:
; Exchange Accounts - Organized by Platform
1970-01-01 open Assets:Crypto:Coinbase:USD
1970-01-01 open Assets:Crypto:Coinbase:BTC
1970-01-01 open Assets:Crypto:Coinbase:ETH
1970-01-01 open Assets:Crypto:Coinbase:ADA
1970-01-01 open Assets:Crypto:Binance:USD
1970-01-01 open Assets:Crypto:Binance:BTC
1970-01-01 open Assets:Crypto:Binance:ETH
1970-01-01 open Assets:Crypto:Binance:BNB
1970-01-01 open Assets:Crypto:Kraken:USD
1970-01-01 open Assets:Crypto:Kraken:BTC
1970-01-01 open Assets:Crypto:Kraken:ETH
1970-01-01 open Assets:Crypto:Kraken:ADA
1970-01-01 open Assets:Crypto:Binance:ADA
; Wallet Accounts - Organized by Type
1970-01-01 open Assets:Crypto:Wallet:Ledger:BTC
1970-01-01 open Assets:Crypto:Wallet:Ledger:ETH
1970-01-01 open Assets:Crypto:Wallet:MetaMask:ETH
1970-01-01 open Assets:Crypto:Wallet:MetaMask:USDC
1970-01-01 open Assets:Crypto:Wallet:MetaMask:UNI
1970-01-01 open Assets:Crypto:Wallet:TrustWallet:BNB
; DeFi Protocol Accounts
1970-01-01 open Assets:DeFi:Uniswap:ETH-USDC-LP
1970-01-01 open Assets:DeFi:Compound:CUSDC
1970-01-01 open Assets:Staking:Ethereum:ETH
1970-01-01 open Assets:Staking:Cardano:ADA
; Income Tracking
1970-01-01 open Income:Crypto:Staking:ETH
1970-01-01 open Income:Crypto:Staking:ADA
1970-01-01 open Income:Crypto:Mining:BTC
1970-01-01 open Income:Crypto:Airdrops
1970-01-01 open Income:Crypto:DeFi:Yield
1970-01-01 open Income:Crypto:Arbitrage
1970-01-01 open Income:CapitalGains:Crypto
1970-01-01 open Income:CapitalGains:LongTerm
; Expense Tracking
1970-01-01 open Expenses:Crypto:Fees:Trading
1970-01-01 open Expenses:Crypto:Fees:Network
1970-01-01 open Expenses:Crypto:Fees:Withdrawal
1970-01-01 open Expenses:CapitalLoss:CryptoOne naming rule bites here, and the parser enforces it rather than convention: every account component, and every commodity symbol, must start with a capital letter or a digit. Compound writes its receipt token cUSDC throughout its own interface, but Assets:DeFi:Compound:cUSDC and a cUSDC commodity are both lexer errors. Spell them CUSDC and keep the protocol's own spelling in the commodity's name: metadata.
Commodity Definitions with Metadata
Define your cryptocurrencies with rich metadata for better tracking:
1970-01-01 commodity BTC
name: "Bitcoin"
asset-class: "cryptocurrency"
sector: "digital-currency"
price-source: "coinbase"
website: "https://bitcoin.org"
1970-01-01 commodity ETH
name: "Ethereum"
asset-class: "cryptocurrency"
sector: "smart-contract-platform"
price-source: "coinbase"
website: "https://ethereum.org"
1970-01-01 commodity ADA
name: "Cardano"
asset-class: "cryptocurrency"
sector: "smart-contract-platform"
price-source: "binance"
website: "https://cardano.org"
1970-01-01 commodity DOT
name: "Polkadot"
asset-class: "cryptocurrency"
sector: "interoperability"
price-source: "kraken"
website: "https://polkadot.network"Multi-Exchange Portfolio Tracking
Recording Purchases Across Exchanges
Track the same cryptocurrency across different exchanges with precise cost basis:
; Bitcoin purchases on different exchanges
2024-01-15 * "Buy BTC on Coinbase"
Assets:Crypto:Coinbase:BTC 1.0 BTC {45000.00 USD}
Assets:Crypto:Coinbase:USD -45000.00 USD
Expenses:Crypto:Fees:Trading 50.00 USD
Assets:Crypto:Coinbase:USD -50.00 USD
2024-01-20 * "Buy BTC on Binance"
Assets:Crypto:Binance:BTC 0.5 BTC {46000.00 USD}
Assets:Crypto:Binance:USD -23000.00 USD
Expenses:Crypto:Fees:Trading 25.00 USD
Assets:Crypto:Binance:USD -25.00 USD
2024-01-25 * "Buy BTC on Kraken"
Assets:Crypto:Kraken:BTC 0.8 BTC {44000.00 USD}
Assets:Crypto:Kraken:USD -35200.00 USD
Expenses:Crypto:Fees:Trading 30.00 USD
Assets:Crypto:Kraken:USD -30.00 USDCross-Exchange Transfers
Track transfers between exchanges while maintaining cost basis:
2024-02-01 * "Transfer BTC from Coinbase to Ledger"
Assets:Crypto:Coinbase:BTC -0.5 BTC {45000.00 USD}
Assets:Crypto:Wallet:Ledger:BTC 0.5 BTC {45000.00 USD}
Expenses:Crypto:Fees:Withdrawal 0.0005 BTC {45000.00 USD}
Assets:Crypto:Coinbase:BTC -0.0005 BTC {45000.00 USD}Arbitrage Opportunities
An arbitrage is one economic event across two venues, so record it as one transaction. This entry sells 10 ETH you already hold on Coinbase at $2,580.00 and immediately restores the position on Binance at $2,500.00:
2024-02-10 * "Arbitrage: Buy ETH on Binance, Sell on Coinbase"
; Buy leg on Binance
Assets:Crypto:Binance:ETH 10 ETH {2500.00 USD}
Assets:Crypto:Binance:USD -25000.00 USD
Expenses:Crypto:Fees:Trading 25.00 USD
Assets:Crypto:Binance:USD -25.00 USD
; Sell leg on Coinbase, at the higher price
Assets:Crypto:Coinbase:ETH -10 ETH {2500.00 USD} @ 2580.00 USD
Assets:Crypto:Coinbase:USD 25800.00 USD
Expenses:Crypto:Fees:Trading 30.00 USD
Assets:Crypto:Coinbase:USD -30.00 USD
Income:Crypto:Arbitrage -800.00 USD ; 25,800.00 proceeds - 25,000.00 basisThree things to copy:
- Never leave a blank line between postings. A blank line ends the transaction, and Beancount reports a syntax error on the postings after it. Use indented
;comments to label the legs instead — which is what the two comment lines above are doing. - The gain is the disposal's gain, $800.00, and it is negative because
Incomeaccounts hold credits. The $55.00 of fees is separate: it is what turns $800.00 of gross spread into $745.00 of profit, and it is already recorded on its own two postings. - The sell leg reduces a lot you must already hold. This entry assumes 10 ETH bought earlier on Coinbase at $2,500.00; without that lot the reduction has nothing to book against.
Advanced Cost Basis Management
Lot-Based Tracking
Beancount.io's lot-based system provides precise cost basis tracking:
; Multiple purchases at different prices
2024-01-01 * "BTC Purchase Lot 1"
Assets:Crypto:Coinbase:BTC 1.0 BTC {40000.00 USD}
Assets:Crypto:Coinbase:USD -40000.00 USD
2024-02-01 * "BTC Purchase Lot 2"
Assets:Crypto:Coinbase:BTC 1.0 BTC {45000.00 USD}
Assets:Crypto:Coinbase:USD -45000.00 USD
2024-03-01 * "BTC Purchase Lot 3"
Assets:Crypto:Coinbase:BTC 1.0 BTC {50000.00 USD}
Assets:Crypto:Coinbase:USD -50000.00 USDThe sign convention for gains and losses
Income accounts hold credits, so Beancount stores them as negative numbers. Every gain below is therefore a negative posting to an income account, and every loss is a positive posting to an expense account. The fee is separate from both: it is an expense in its own right, so subtracting it from the gain as well would deduct it twice and leave the transaction unbalanced.
For one disposal the arithmetic is always:
gain (or loss) = gross proceeds - cost basis of the lots removed
net cash = gross proceeds - feesSpecific Identification Method
Sell specific lots for optimal tax management. Naming the lot's cost in the reduction posting is what "specific identification" means in Beancount, and it works under the default STRICT booking:
; Sell the most expensive lot: 1.0 BTC bought at $50,000.00
2024-04-01 * "Sell BTC Lot 3 for tax optimization"
Assets:Crypto:Coinbase:BTC -1.0 BTC {50000.00 USD} @ 48000.00 USD
Assets:Crypto:Coinbase:USD 48000.00 USD ; gross proceeds
Expenses:Crypto:Fees:Trading 50.00 USD
Assets:Crypto:Coinbase:USD -50.00 USD
Expenses:CapitalLoss:Crypto 2000.00 USD ; 50,000.00 basis - 48,000.00 proceedsSelling the oldest or the newest lot
Naming a different cost selects a different lot. These two sales take the $40,000.00 lot and then the $45,000.00 lot — the two still standing after the $50,000.00 one went in the example above — which is the same selection FIFO and LIFO would make automatically, spelled out by hand:
; Sell the oldest lot (bought 2024-01-01 at $40,000.00)
2024-05-01 * "Sell 0.5 BTC from the oldest lot"
Assets:Crypto:Coinbase:BTC -0.5 BTC {40000.00 USD} @ 52000.00 USD
Assets:Crypto:Coinbase:USD 26000.00 USD ; gross proceeds, 0.5 * 52,000.00
Expenses:Crypto:Fees:Trading 30.00 USD
Assets:Crypto:Coinbase:USD -30.00 USD
Income:CapitalGains:Crypto -6000.00 USD ; 26,000.00 proceeds - 20,000.00 basis
; Sell the newest remaining lot (bought 2024-02-01 at $45,000.00)
2024-05-02 * "Sell 0.5 BTC from the newest lot"
Assets:Crypto:Coinbase:BTC -0.5 BTC {45000.00 USD} @ 52000.00 USD
Assets:Crypto:Coinbase:USD 26000.00 USD
Expenses:Crypto:Fees:Trading 30.00 USD
Assets:Crypto:Coinbase:USD -30.00 USD
Income:CapitalGains:Crypto -3500.00 USD ; 26,000.00 proceeds - 22,500.00 basisSame sale price, different lot, $2,500.00 difference in realized gain. If you would rather Beancount choose the lot for you, set a booking method on the account instead of naming a cost — "FIFO", "LIFO" or "HIFO" on the open directive, described in Inventory Management.
Portfolio Performance Analysis
Price Tracking Setup
Set up automated price feeds for accurate valuation:
; Daily price updates
2024-01-15 price BTC 45000.00 USD
2024-01-15 price ETH 2500.00 USD
2024-01-15 price ADA 0.50 USD
2024-01-16 price BTC 46000.00 USD
2024-01-16 price ETH 2550.00 USD
2024-01-16 price ADA 0.52 USDPortfolio Allocation Tracking
Use Beancount.io's reporting features to analyze allocation:
-- Query for portfolio allocation by asset
SELECT
account,
sum(position) as balance,
value(sum(position)) as market_value
WHERE account ~ "Assets:Crypto"
GROUP BY 1
ORDER BY market_value DESCPerformance Metrics
Track key performance indicators:
-- Total portfolio value query
SELECT
sum(value(position)) as total_portfolio_value
WHERE account ~ "Assets:Crypto"
-- Realized gains/losses
SELECT
sum(position) as realized_gains
WHERE account ~ "Income:CapitalGains:Crypto"Staking and DeFi Integration
Staking Rewards Tracking
Record staking rewards with proper income recognition. The units arriving and the income recognized are the same event seen from two sides, so their weights must cancel: [units] × [price] of asset in, the same number of dollars of income out — and income is a credit, so it is negative.
2024-01-31 * "ETH Staking Rewards - January"
Assets:Staking:Ethereum:ETH 0.08 ETH {2500.00 USD}
Income:Crypto:Staking:ETH -200.00 USD ; 0.08 * 2,500.00
2024-01-31 * "ADA Staking Rewards - January"
Assets:Staking:Cardano:ADA 25 ADA {0.50 USD}
Income:Crypto:Staking:ADA -12.50 USD ; 25 * 0.50The {2,500.00 USD} and {0.50 USD} are external valuations. Beancount stores whatever you write; it does not fetch a price or decide which quote is the right one. Pick a source — the exchange you would actually sell on, or a published index — and use it consistently, because that number is both your income and your future cost basis.
The reward's basis is recognized once
The $200.00 recognized above becomes the 0.08 ETH's cost basis. Selling it later realizes only the movement since the reward date:
2024-06-30 * "Sell the January ETH staking reward"
Assets:Staking:Ethereum:ETH -0.08 ETH {2500.00 USD} @ 3000.00 USD
Assets:Crypto:Coinbase:USD 240.00 USD ; gross proceeds, 0.08 * 3,000.00
Income:CapitalGains:Crypto -40.00 USD ; 240.00 proceeds - 200.00 basis$40.00 of capital gain, not $240.00 — the first $200.00 was already income in January. Booking the whole $240.00 as gain is the most common way reward income gets counted twice. When the reward becomes taxable, and at what rate, varies by jurisdiction; the ledger records receipt and disposal separately so either treatment can be reported from it.
DeFi Yield Tracking
Track complex DeFi positions:
Providing liquidity swaps two commodities for a third, so every leg has to weigh in the same currency before the transaction can balance. The ETH and LP legs already do, because they carry a cost; the stablecoin leg needs an explicit @ 1.00 USD, otherwise Beancount sees 25,000 units of USDC leaving and $25,000.00 of value arriving, which are different commodities and do not cancel.
2024-02-01 * "Uniswap LP Position"
Assets:Crypto:Wallet:MetaMask:ETH -10 ETH {2500.00 USD}
Assets:Crypto:Wallet:MetaMask:USDC -25000 USDC @ 1.00 USD
Assets:DeFi:Uniswap:ETH-USDC-LP 100 UNI-V2-ETH-USDC {500.00 USD}
2024-02-28 * "Uniswap LP Rewards - February"
Assets:Crypto:Wallet:MetaMask:UNI 50 UNI {8.00 USD}
Income:Crypto:DeFi:Yield -400.00 USD ; 50 * 8.00Automated Portfolio Management
Importing exchange data
Beancount ships no exchange API client, and there is no configuration file that makes it talk to Coinbase or Binance. The supported path is an importer: you download the exchange's CSV export (or fetch it with your own script) and convert it to Beancount transactions with beangulp, the importer framework that succeeded Beancount 2's built-in beancount.ingest. beancount.ingest does not exist in Beancount 3.
If you do write a fetch script, its credentials file is yours alone — nothing in the Beancount toolchain reads it:
# Configuration for YOUR OWN fetch script. No Beancount tool reads this file.
exchanges:
coinbase:
api_key: "your_api_key"
api_secret: "your_api_secret"
passphrase: "your_passphrase"
binance:
api_key: "your_api_key"
api_secret: "your_api_secret"Automated Reconciliation
Set up automated balance verification:
; Balance assertions for automated verification
2024-01-31 balance Assets:Crypto:Coinbase:BTC 2.5 BTC
2024-01-31 balance Assets:Crypto:Binance:ETH 15.0 ETH
2024-01-31 balance Assets:Crypto:Kraken:ADA 1000.0 ADAThose three numbers come from your exchange screens, not from this page, so the block above will not verify against anything until your own transactions are in the ledger. Two rules decide whether it verifies at all. A balance directive asserts the balance at the start of the named day, so an assertion dated 2024-01-31 does not see anything posted on 2024-01-31 — date it the day after the period you are closing. And every asserted account must be open, which is why Assets:Crypto:Kraken:ADA appears in the Basic Account Hierarchy section above.
Keeping prices up to date
Prices live in the ledger as price directives, and nothing in Beancount 3.2.3 fetches them for you. There is no custom "price-source" directive — a bare commodity symbol is not even a valid custom value, so that line is a syntax error — and no beancount.plugins.forecast module; the loader stops with an import error if you name it.
What does exist is beanprice, a separate package (pip install beanprice) providing the bean-price command. It reads a price: metadata field on each commodity directive, in the form "<quote currency>:<source module>/<ticker>", and writes price directives you append to your ledger:
1970-01-01 commodity BTC
name: "Bitcoin"
asset-class: "cryptocurrency"
price: "USD:beanprice.sources.coinbase/BTC-USD"
1970-01-01 commodity ETH
name: "Ethereum"
asset-class: "cryptocurrency"
price: "USD:beanprice.sources.coinbase/ETH-USD"
; What bean-price appends - and what you would otherwise type by hand
2024-01-15 price BTC 45000.00 USD
2024-01-15 price ETH 2500.00 USDAny other metadata you attach to a commodity — sector, website, or the price-source field used earlier in this guide — is free-form documentation for your own benefit. The loader stores it and no tool acts on it.
Tax Optimization Strategies
Tax Loss Harvesting
Implement systematic tax loss harvesting:
; The lot being harvested, so this block stands on its own
2024-03-01 * "Buy ADA on Binance"
Assets:Crypto:Binance:ADA 1000 ADA {0.60 USD}
Assets:Crypto:Binance:USD -600.00 USD
; Identify positions with unrealized losses
2024-12-15 * "Tax loss harvesting - Sell ADA at loss"
Assets:Crypto:Binance:ADA -1000 ADA {0.60 USD} @ 0.45 USD
Assets:Crypto:Binance:USD 450.00 USD ; gross proceeds
Expenses:Crypto:Fees:Trading 5.00 USD
Assets:Crypto:Binance:USD -5.00 USD
Expenses:CapitalLoss:Crypto 150.00 USD ; 600.00 basis - 450.00 proceeds
; Repurchase after wash sale period (31 days)
2025-01-16 * "Repurchase ADA after wash sale period"
Assets:Crypto:Binance:ADA 1000 ADA {0.45 USD}
Assets:Crypto:Binance:USD -450.00 USD
Expenses:Crypto:Fees:Trading 5.00 USD
Assets:Crypto:Binance:USD -5.00 USDLong-term vs Short-term Gains
Track holding periods for tax optimization:
; Use metadata to track purchase dates
2024-01-01 * "BTC Purchase - Long-term hold" ^long-term-btc
Assets:Crypto:Coinbase:BTC 1.0 BTC {40000.00 USD}
Assets:Crypto:Coinbase:USD -40000.00 USD
; Sell after one year for long-term capital gains treatment
2025-01-02 * "BTC Sale - Long-term capital gains" ^long-term-btc
Assets:Crypto:Coinbase:BTC -1.0 BTC {40000.00 USD} @ 55000.00 USD
Assets:Crypto:Coinbase:USD 55000.00 USD
Income:CapitalGains:LongTerm -15000.00 USD ; 55,000.00 proceeds - 40,000.00 basisThe ^long-term-btc link on both entries is what lets you pull the purchase and the sale up together later; Beancount does not track holding periods itself.
Reporting and Analytics
Portfolio Summary Reports
Generate comprehensive portfolio reports:
-- Portfolio allocation by cryptocurrency
SELECT
commodity,
sum(position) as total_units,
value(sum(position)) as market_value,
value(sum(position)) / (
SELECT value(sum(position))
FROM positions
WHERE account ~ "Assets:Crypto"
) * 100 as allocation_percentage
WHERE account ~ "Assets:Crypto"
GROUP BY commodity
ORDER BY market_value DESC;Performance Analytics
Track portfolio performance over time:
-- Monthly portfolio performance
SELECT
year(date) as year,
month(date) as month,
value(sum(position)) as portfolio_value
WHERE account ~ "Assets:Crypto"
GROUP BY year, month
ORDER BY year, month;Income Analysis
Analyze income sources:
-- Income breakdown by source
SELECT
account,
sum(position) as total_income
WHERE account ~ "Income:Crypto"
GROUP BY account
ORDER BY total_income DESC;Best Practices and Tips
1. Consistent Recording
- Record transactions immediately after execution
- Use standardized transaction descriptions
- Include transaction hashes in metadata
2. Regular Reconciliation
- Verify balances weekly across all platforms
- Use balance assertions to catch discrepancies
- Monitor for missing transactions
3. Backup and Security
- Regularly backup your Beancount files
- Use version control (Git) for change tracking
- Encrypt sensitive data
4. Documentation
- Document your account structure decisions
- Maintain notes on complex transactions
- Keep records of API configurations
5. Tax Preparation
- Generate reports quarterly for tax planning
- Maintain detailed records for audit purposes
- Consult with tax professionals for complex situations
Conclusion
Effective cryptocurrency portfolio tracking requires precision, consistency, and the right tools. Beancount.io provides the flexibility and power needed to manage complex crypto portfolios across multiple exchanges, wallets, and DeFi protocols.
Key benefits of using Beancount.io for crypto portfolio tracking:
- Complete Data Ownership: Your data in plain-text format
- Precise Cost Basis: Lot-based tracking with specific identification
- Unlimited Flexibility: Custom account structures and reporting
- Tax Optimization: Advanced strategies for minimizing tax liability
- Future-Proof: Open format ensures long-term accessibility
Start with a basic setup and gradually expand your tracking as your portfolio grows in complexity. The investment in proper setup will pay dividends in accurate reporting, tax optimization, and portfolio insights.
Ready to take control of your cryptocurrency portfolio? Get started with Beancount.io today.