Skip to content

Commit

Permalink
Merge pull request #99 from casesandberg/feature/finance-fixes
Browse files Browse the repository at this point in the history
Fix Realized Gains Calculation
  • Loading branch information
casesandberg authored Sep 11, 2024
2 parents c5a781a + b703cef commit 89e3e20
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 41 deletions.
25 changes: 24 additions & 1 deletion packages/finance/lib/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Decimal from 'decimal.js'
import { mockBalance } from '@play-money/database/mocks'
import { calculateBalanceSubtotals, marketOptionBalancesToProbabilities } from './helpers'
import * as ECONOMY from '@play-money/finance/economy'
import { calculateBalanceSubtotals, calculateRealizedGainsTax, marketOptionBalancesToProbabilities } from './helpers'

Object.defineProperty(ECONOMY, 'REALIZED_GAINS_TAX', { value: 0.05 })

describe('calculateBalanceSubtotals', () => {
const mockTx = {
Expand Down Expand Up @@ -116,3 +119,23 @@ describe('marketOptionBalancesToProbabilities', () => {
expect(result).toEqual({})
})
})

describe('calculateRealizedGainsTax', () => {
it('should calculate tax correctly for a gain', () => {
const cost = new Decimal(100)
const salePrice = new Decimal(150)

const result = calculateRealizedGainsTax({ cost, salePrice })

expect(result).toEqual(new Decimal(2.5))
})

it('should not apply tax for a loss', () => {
const cost = new Decimal(150)
const salePrice = new Decimal(100)

const result = calculateRealizedGainsTax({ cost, salePrice })

expect(result).toEqual(new Decimal(0))
})
})
11 changes: 11 additions & 0 deletions packages/finance/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Decimal from 'decimal.js'
import { TransactionClient } from '@play-money/database'
import { AssetTypeType } from '@play-money/database/zod/inputTypeSchemas/AssetTypeSchema'
import { calculateProbability } from '../amms/maniswap-v1.1'
import { REALIZED_GAINS_TAX } from '../economy'
import { TransactionEntryInput } from '../types'
import { NetBalance, NetBalanceAsNumbers } from './getBalances'

Expand Down Expand Up @@ -117,3 +118,13 @@ export function marketOptionBalancesToProbabilities(balances: Array<NetBalance |
{} as Record<string, number>
)
}

export function calculateRealizedGainsTax({ cost, salePrice }: { cost: Decimal; salePrice: Decimal }) {
const gainOrLoss = salePrice.sub(cost)

if (gainOrLoss.isNegative()) {
return new Decimal(0) // Assuming no tax is applied for a loss scenario
}

return gainOrLoss.times(REALIZED_GAINS_TAX).toDecimalPlaces(4)
}
37 changes: 27 additions & 10 deletions packages/markets/components/MarketBalanceBreakdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react'
import { MarketOption } from '@play-money/database'
import { CurrencyDisplay } from '@play-money/finance/components/CurrencyDisplay'
import { MarketOptionPositionAsNumbers, NetBalanceAsNumbers } from '@play-money/finance/lib/getBalances'
import { Tooltip, TooltipContent, TooltipTrigger } from '@play-money/ui/tooltip'

const transactionLabels: Record<string, string> = {
TRADE_BUY: 'Bought positions',
Expand Down Expand Up @@ -61,16 +62,32 @@ export function MarketBalanceBreakdown({
const changeLabel = `(${change > 0 ? '+' : ''}${change}%)`

return value.toNumber() ? (
<div key={position.optionId} className="flex justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<div className="size-2 rounded-md" style={{ backgroundColor: option.color }} />
<span className="font-mono">{option.name}</span>
</div>
<div className="flex gap-2">
{change ? <span className={change > 0 ? 'text-lime-500' : 'text-red-400'}>{changeLabel}</span> : null}
<CurrencyDisplay value={position.value} />
</div>
</div>
<Tooltip key={position.optionId}>
<TooltipTrigger className="flex w-full justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<div className="size-2 rounded-md" style={{ backgroundColor: option.color }} />
<span className="font-mono">{option.name}</span>
</div>
<div className="flex gap-2">
{change ? (
<span className={change > 0 ? 'text-lime-500' : 'text-red-400'}>{changeLabel}</span>
) : null}
<CurrencyDisplay value={position.value} />
</div>
</TooltipTrigger>
<TooltipContent className="max-w-sm" align="start">
<div className="line-clamp-2">{option.name}</div>
<div className="text-xs text-muted-foreground">
Cost: {new Decimal(position.cost).toDecimalPlaces(4).toString()}
</div>
<div className="text-xs text-muted-foreground">
Quantity: {new Decimal(position.quantity).toDecimalPlaces(4).toString()}
</div>
<div className="text-xs text-muted-foreground">
Value: {new Decimal(position.value).toDecimalPlaces(4).toString()}
</div>
</TooltipContent>
</Tooltip>
) : null
})}

Expand Down
15 changes: 5 additions & 10 deletions packages/markets/lib/createMarketResolveWinTransactions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Decimal from 'decimal.js'
import db, { Transaction } from '@play-money/database'
import { REALIZED_GAINS_TAX } from '@play-money/finance/economy'
import { executeTransaction } from '@play-money/finance/lib/executeTransaction'
import { getHouseAccount } from '@play-money/finance/lib/getHouseAccount'
import { calculateRealizedGainsTax } from '@play-money/finance/lib/helpers'
import { getMarket } from './getMarket'
import { getMarketAmmAccount } from './getMarketAmmAccount'
import { getMarketClearingAccount } from './getMarketClearingAccount'
Expand Down Expand Up @@ -34,11 +33,7 @@ export async function createMarketResolveWinTransactions({

// Transfer winning shares back to the AMM and convert to primary currency
for (const position of winningPositions) {
const taxedValue = position.quantity.sub(position.quantity.times(REALIZED_GAINS_TAX))
const valueTaxedIfGains = taxedValue.gt(position.cost) ? taxedValue : position.quantity

const amountTaxed = position.quantity.sub(valueTaxedIfGains)
const isTaxed = amountTaxed.toDecimalPlaces(4).gt(0)
const tax = calculateRealizedGainsTax({ cost: position.cost, salePrice: position.quantity })

const entries = [
{
Expand All @@ -64,13 +59,13 @@ export async function createMarketResolveWinTransactions({
toAccountId: position.accountId,
assetType: 'CURRENCY',
assetId: 'PRIMARY',
amount: isTaxed ? valueTaxedIfGains : position.quantity,
amount: tax.gt(0) ? position.quantity.sub(tax) : position.quantity,
} as const,
]

if (isTaxed) {
if (tax.gt(0)) {
entries.push({
amount: amountTaxed,
amount: tax,
assetType: 'CURRENCY',
assetId: 'PRIMARY',
fromAccountId: clearingAccount.id,
Expand Down
14 changes: 5 additions & 9 deletions packages/markets/lib/executeTrade.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Decimal from 'decimal.js'
import { trade, quote } from '@play-money/finance/amms/maniswap-v1.1'
import { REALIZED_GAINS_TAX } from '@play-money/finance/economy'
import { getBalance, getMarketBalances } from '@play-money/finance/lib/getBalances'
import { getHouseAccount } from '@play-money/finance/lib/getHouseAccount'
import { calculateRealizedGainsTax } from '@play-money/finance/lib/helpers'
import { TransactionEntryInput } from '@play-money/finance/types'
import { getMarketOptionPosition } from '@play-money/users/lib/getMarketOptionPosition'
import { getMarketAmmAccount } from './getMarketAmmAccount'
Expand Down Expand Up @@ -125,11 +125,7 @@ export async function executeTrade({
throw new Error('User does not have position in market')
}

const taxedValue = receivedShares.sub(receivedShares.times(REALIZED_GAINS_TAX))
const valueTaxedIfGains = taxedValue.gt(position.cost) ? taxedValue : receivedShares

const amountTaxed = receivedShares.sub(valueTaxedIfGains)
const isTaxed = amountTaxed.toDecimalPlaces(4).gt(0)
const tax = calculateRealizedGainsTax({ cost: position.cost, salePrice: receivedShares })

entries.push(
...ammAssetBalances.map((balance) => {
Expand All @@ -142,17 +138,17 @@ export async function executeTrade({
} as const
}),
{
amount: isTaxed ? valueTaxedIfGains : receivedShares,
amount: tax.gt(0) ? receivedShares.sub(tax) : receivedShares,
assetType: 'CURRENCY',
assetId: 'PRIMARY',
fromAccountId: clearingAccount.id,
toAccountId: accountId,
}
)

if (isTaxed) {
if (tax.gt(0)) {
entries.push({
amount: amountTaxed,
amount: tax,
assetType: 'CURRENCY',
assetId: 'PRIMARY',
fromAccountId: clearingAccount.id,
Expand Down
8 changes: 3 additions & 5 deletions packages/markets/lib/getMarketQuote.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Decimal from 'decimal.js'
import { quote } from '@play-money/finance/amms/maniswap-v1.1'
import { REALIZED_GAINS_TAX } from '@play-money/finance/economy'
import { getMarketBalances } from '@play-money/finance/lib/getBalances'
import { calculateRealizedGainsTax } from '@play-money/finance/lib/helpers'
import { getMarketAmmAccount } from './getMarketAmmAccount'

export async function getMarketQuote({
Expand Down Expand Up @@ -30,12 +30,10 @@ export async function getMarketQuote({
shares: optionsShares,
})

if (REALIZED_GAINS_TAX) {
shares = shares.sub(shares.times(REALIZED_GAINS_TAX))
}
const tax = calculateRealizedGainsTax({ cost: amount, salePrice: shares })

return {
probability,
shares,
shares: shares.sub(tax),
}
}
5 changes: 4 additions & 1 deletion packages/markets/lib/updateMarketPositionValues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TransactionClient } from '@play-money/database'
import { mockAccount, mockBalance, mockMarketOptionPosition } from '@play-money/database/mocks'
import * as ECONOMY from '@play-money/finance/economy'
import { getMarketBalances } from '@play-money/finance/lib/getBalances'
import { BalanceChange, findBalanceChange } from '@play-money/finance/lib/helpers'
import { BalanceChange, calculateRealizedGainsTax, findBalanceChange } from '@play-money/finance/lib/helpers'
import { getMarketAmmAccount } from './getMarketAmmAccount'
import { updateMarketPositionValues } from './updateMarketPositionValues'

Expand Down Expand Up @@ -50,6 +50,9 @@ describe('updateMarketPositionValues', () => {
] as Array<BalanceChange>

jest.mocked(getMarketAmmAccount).mockResolvedValue(mockAccount({ id: mockAmmAccountId }))
jest.mocked(calculateRealizedGainsTax).mockImplementation(() => {
return new Decimal(0)
})

jest.mocked(getMarketBalances).mockResolvedValue([
mockBalance({
Expand Down
8 changes: 3 additions & 5 deletions packages/markets/lib/updateMarketPositionValues.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Decimal from 'decimal.js'
import { TransactionClient } from '@play-money/database'
import { quote } from '@play-money/finance/amms/maniswap-v1.1'
import { REALIZED_GAINS_TAX } from '@play-money/finance/economy'
import { getMarketBalances } from '@play-money/finance/lib/getBalances'
import { BalanceChange, findBalanceChange } from '@play-money/finance/lib/helpers'
import { BalanceChange, calculateRealizedGainsTax, findBalanceChange } from '@play-money/finance/lib/helpers'
import { getMarketAmmAccount } from './getMarketAmmAccount'

export async function updateMarketPositionValues({
Expand Down Expand Up @@ -49,13 +48,12 @@ export async function updateMarketPositionValues({
return
}

const taxedValue = newValue.shares.sub(newValue.shares.times(REALIZED_GAINS_TAX))
const valueTaxedIfGains = taxedValue.gt(position.cost) ? taxedValue : newValue.shares
const tax = calculateRealizedGainsTax({ cost: position.cost, salePrice: newValue.shares })

return tx.marketOptionPosition.update({
where: { id: position.id },
data: {
value: valueTaxedIfGains,
value: new Decimal(newValue.shares).sub(tax),
},
})
})
Expand Down

0 comments on commit 89e3e20

Please sign in to comment.