Skip to content

Commit

Permalink
refactor: improve fees management across engines
Browse files Browse the repository at this point in the history
  • Loading branch information
gianlucapagliara committed Jan 7, 2025
1 parent 03472ef commit 6b59c97
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 118 deletions.
6 changes: 2 additions & 4 deletions financepype/operations/fees.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from enum import Enum
from typing import Self

from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic import BaseModel, Field, model_validator

from financepype.assets.asset import Asset

Expand All @@ -24,10 +24,8 @@ class OperationFee(BaseModel):
associated asset, type, and impact on costs or returns.
"""

model_config = ConfigDict(arbitrary_types_allowed=True)

amount: Decimal = Field(ge=0)
asset: Asset
asset: Asset | None = None
fee_type: FeeType
impact_type: FeeImpactType

Expand Down
2 changes: 1 addition & 1 deletion financepype/operations/orders/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def group_client_order_id(self) -> str | None:
)

@property
def fee_asset(self) -> Asset: # Type depends on the asset implementation
def fee_asset(self) -> Asset | None: # Type depends on the asset implementation
return self.fee.asset

@property
Expand Down
15 changes: 6 additions & 9 deletions financepype/simulations/balances/engines/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import pandas as pd
import streamlit as st

from financepype.assets.factory import AssetFactory
from financepype.markets.trading_pair import TradingPair
from financepype.operations.fees import FeeImpactType, FeeType
from financepype.operations.fees import OperationFee as Fee
Expand All @@ -20,18 +19,16 @@ def create_sample_order(
amount: Decimal,
price: Decimal,
platform_id: str = "binance",
trading_pair: str = "BTC-USDT",
trading_pair: str = "EXAMPLE-USDT",
fee_type: FeeType = FeeType.PERCENTAGE,
fee_impact: FeeImpactType = FeeImpactType.ADDED_TO_COSTS,
fee_amount: Decimal = Decimal("0.001"),
fee_amount: Decimal = Decimal("0.1"),
) -> OrderDetails:
"""Create a sample order for simulation."""
platform = Platform(platform_id)

trading_pair_obj = TradingPair(name=trading_pair)

fee_asset = AssetFactory.get_asset(platform, trading_pair_obj.quote)

trading_rule = TradingRule(
trading_pair=trading_pair_obj,
min_order_size=Decimal("0.0001"),
Expand All @@ -52,7 +49,7 @@ def create_sample_order(
position_action=position_action,
index_price=price,
fee=Fee(
asset=fee_asset,
asset=None,
fee_type=fee_type,
impact_type=fee_impact,
amount=fee_amount,
Expand Down Expand Up @@ -97,9 +94,9 @@ def main() -> None:
],
)

trading_pair = st.sidebar.text_input("Trading Pair", value="BTC-USDT")
trading_pair = st.sidebar.text_input("Trading Pair", value="EXAMPLE-USDT")
amount = st.sidebar.number_input("Amount", value=1.0, step=0.1)
price = st.sidebar.number_input("Price", value=50000.0, step=100.0)
price = st.sidebar.number_input("Price", value=100.0, step=100.0)

# Fee settings
st.sidebar.header("Fee Settings")
Expand Down Expand Up @@ -129,7 +126,7 @@ def main() -> None:
trading_pair=trading_pair,
fee_type=fee_type,
fee_impact=fee_impact,
fee_amount=Decimal(str(fee_amount)) / Decimal("100")
fee_amount=Decimal(str(fee_amount))
if fee_type == FeeType.PERCENTAGE
else Decimal(str(fee_amount)),
)
Expand Down
46 changes: 33 additions & 13 deletions financepype/simulations/balances/engines/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ def _get_outflow_asset(cls, order_details: OrderDetails) -> Asset:
"""
raise NotImplementedError

@classmethod
def _get_expected_fee_asset(cls, order_details: OrderDetails) -> Asset:
"""Get the expected fee asset based on the trade type and fee impact type."""
if order_details.fee.impact_type == FeeImpactType.DEDUCTED_FROM_RETURNS:
return cls._get_outflow_asset(order_details)
elif order_details.fee.impact_type == FeeImpactType.ADDED_TO_COSTS:
return cls._get_outflow_asset(order_details)
else:
raise ValueError(
f"Unsupported fee impact type: {order_details.fee.impact_type}"
)

@classmethod
def _get_fee_impact(cls, order_details: OrderDetails) -> dict[Asset, Decimal]:
"""Calculate the fee amount based on fee type and trade details.
Expand All @@ -108,25 +120,30 @@ def _get_fee_impact(cls, order_details: OrderDetails) -> dict[Asset, Decimal]:
NotImplementedError: If fee is in an asset not involved in the trade
ValueError: If fee type is not supported
"""
fee_asset = order_details.fee.asset
collateral_asset = cls._get_outflow_asset(order_details)
expected_asset = cls._get_expected_fee_asset(order_details)

# Validate fee asset is involved in the trade
if fee_asset != collateral_asset:
# If fee asset is specified, verify it matches expected
if (
order_details.fee.asset is not None
and order_details.fee.asset != expected_asset
):
raise NotImplementedError(
f"Fee in {fee_asset} not supported. Expected {collateral_asset}"
"Fee on not involved asset not supported yet. "
f"Fee asset: {str(order_details.fee.asset)}, expected asset: {str(expected_asset)}"
)

# Handle absolute fees (fixed amount)
if order_details.fee.fee_type == FeeType.ABSOLUTE:
return {fee_asset: order_details.fee.amount}
if order_details.fee.asset is None:
raise ValueError("Fee asset is required for absolute fees")
return {expected_asset: order_details.fee.amount}

# Handle percentage fees
if order_details.fee.fee_type == FeeType.PERCENTAGE:
# Calculate fee based on premium
premium = cls._calculate_premium(order_details)
fee_amount = premium * (order_details.fee.amount / Decimal("100"))
return {fee_asset: fee_amount}
return {expected_asset: fee_amount}

# Handle unsupported fee types
raise ValueError(f"Unsupported fee type: {order_details.fee.fee_type}")
Expand Down Expand Up @@ -371,13 +388,14 @@ def get_involved_assets(cls, order_details: OrderDetails) -> list[AssetCashflow]
# Fee
if order_details.fee.impact_type == FeeImpactType.ADDED_TO_COSTS:
fee_impact = cls._get_fee_impact(order_details)
fee_asset = cls._get_expected_fee_asset(order_details)
result.append(
AssetCashflow(
asset=order_details.fee.asset,
asset=fee_asset,
involvement_type=InvolvementType.OPENING,
cashflow_type=CashflowType.OUTFLOW,
reason=CashflowReason.FEE,
amount=fee_impact[order_details.fee.asset],
amount=fee_impact[fee_asset],
)
)

Expand Down Expand Up @@ -419,13 +437,14 @@ def get_opening_outflows(
# Fee
if order_details.fee.impact_type == FeeImpactType.ADDED_TO_COSTS:
fee_impact = cls._get_fee_impact(order_details)
fee_asset = cls._get_expected_fee_asset(order_details)
result.append(
AssetCashflow(
asset=order_details.fee.asset,
asset=fee_asset,
involvement_type=InvolvementType.OPENING,
cashflow_type=CashflowType.OUTFLOW,
reason=CashflowReason.FEE,
amount=fee_impact[order_details.fee.asset],
amount=fee_impact[fee_asset],
)
)

Expand Down Expand Up @@ -483,13 +502,14 @@ def get_closing_outflows(
# Fee
if order_details.fee.impact_type == FeeImpactType.DEDUCTED_FROM_RETURNS:
fee_impact = cls._get_fee_impact(order_details)
fee_asset = cls._get_expected_fee_asset(order_details)
result.append(
AssetCashflow(
asset=order_details.fee.asset,
asset=fee_asset,
involvement_type=InvolvementType.CLOSING,
cashflow_type=CashflowType.OUTFLOW,
reason=CashflowReason.FEE,
amount=fee_impact[order_details.fee.asset],
amount=fee_impact[fee_asset],
)
)

Expand Down
35 changes: 27 additions & 8 deletions financepype/simulations/balances/engines/perpetual.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ def _calculate_pnl(cls, order_details: OrderDetails) -> Decimal:
"""
raise NotImplementedError

@classmethod
def _get_expected_fee_asset(cls, order_details: OrderDetails) -> Asset:
"""Get the expected fee asset based on the trade type and fee impact type."""
if order_details.fee.impact_type == FeeImpactType.DEDUCTED_FROM_RETURNS:
return cls._get_outflow_asset(order_details)
elif order_details.fee.impact_type == FeeImpactType.ADDED_TO_COSTS:
return cls._get_outflow_asset(order_details)
else:
raise ValueError(
f"Unsupported fee impact type: {order_details.fee.impact_type}"
)

@classmethod
def _calculate_fee_amount(cls, order_details: OrderDetails) -> Decimal:
"""Calculate the fee amount based on fee type and trade details.
Expand All @@ -126,17 +138,22 @@ def _calculate_fee_amount(cls, order_details: OrderDetails) -> Decimal:
ValueError: If fee type is not supported
NotImplementedError: If fee is in an asset not involved in the trade
"""
fee_asset = order_details.fee.asset
collateral_asset = cls._get_outflow_asset(order_details)
expected_asset = cls._get_expected_fee_asset(order_details)

# Validate fee asset is involved in the trade
if fee_asset != collateral_asset:
# If fee asset is specified, verify it matches expected
if (
order_details.fee.asset is not None
and order_details.fee.asset != expected_asset
):
raise NotImplementedError(
f"Fee in {fee_asset} not supported. Expected {collateral_asset}"
"Fee on not involved asset not supported yet. "
f"Fee asset: {str(order_details.fee.asset)}, expected asset: {str(expected_asset)}"
)

# Handle absolute fees (fixed amount)
if order_details.fee.fee_type == FeeType.ABSOLUTE:
if order_details.fee.asset is None:
raise ValueError("Fee asset is required for absolute fees")
return order_details.fee.amount

# Handle percentage fees
Expand Down Expand Up @@ -239,7 +256,7 @@ def get_involved_assets(cls, order_details: OrderDetails) -> list[AssetCashflow]

# Fee
if order_details.fee.impact_type == FeeImpactType.ADDED_TO_COSTS:
fee_asset = order_details.fee.asset
fee_asset = cls._get_expected_fee_asset(order_details)
result.append(
AssetCashflow(
asset=fee_asset,
Expand Down Expand Up @@ -272,9 +289,10 @@ def get_opening_outflows(

# Fee
if order_details.fee.impact_type == FeeImpactType.ADDED_TO_COSTS:
fee_asset = cls._get_expected_fee_asset(order_details)
result.append(
AssetCashflow(
asset=order_details.fee.asset,
asset=fee_asset,
involvement_type=InvolvementType.OPENING,
cashflow_type=CashflowType.OUTFLOW,
reason=CashflowReason.FEE,
Expand All @@ -298,9 +316,10 @@ def get_closing_outflows(

# Fee deducted from returns
if order_details.fee.impact_type == FeeImpactType.DEDUCTED_FROM_RETURNS:
fee_asset = cls._get_expected_fee_asset(order_details)
result.append(
AssetCashflow(
asset=order_details.fee.asset,
asset=fee_asset,
involvement_type=InvolvementType.CLOSING,
cashflow_type=CashflowType.OUTFLOW,
reason=CashflowReason.FEE,
Expand Down
66 changes: 34 additions & 32 deletions financepype/simulations/balances/engines/spot.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ def _get_inflow_asset(cls, order_details: OrderDetails) -> Asset:
asset = AssetFactory.get_asset(order_details.platform, symbol)
return asset

@classmethod
def _get_expected_fee_asset(cls, order_details: OrderDetails) -> Asset:
"""Get the expected fee asset based on the trade type and fee impact type."""
if order_details.fee.impact_type == FeeImpactType.DEDUCTED_FROM_RETURNS:
return cls._get_inflow_asset(order_details)
elif order_details.fee.impact_type == FeeImpactType.ADDED_TO_COSTS:
return cls._get_outflow_asset(order_details)
else:
raise ValueError(
f"Unsupported fee impact type: {order_details.fee.impact_type}"
)

@classmethod
def _get_fee_impact(cls, order_details: OrderDetails) -> dict[Asset, Decimal]:
"""Calculate the fee amount based on fee type, fee asset, and trade details.
Expand All @@ -86,37 +98,21 @@ def _get_fee_impact(cls, order_details: OrderDetails) -> dict[Asset, Decimal]:
- Fixed amount in the fee asset
- Example: 10 USDT fee for any trade size
2. Percentage Fees in Quote Currency (e.g., USDT in BTC-USDT):
- Always based on notional value (amount * price)
2. Percentage Fees (e.g., USDT in BTC-USDT):
- BUY with ADDED_TO_COSTS:
* Trading 1 BTC at $50,000 with 0.1% fee
* Fee = $50,000 * 0.1% = $50 USDT
- BUY with DEDUCTED_FROM_RETURNS:
* Same calculation as above
* Fee = $50,000 * 0.1% = $50 USDT
- SELL with ADDED_TO_COSTS:
* Trading 1 BTC at $50,000 with 0.1% fee
* Fee = $50,000 * 0.1% = $50 USDT
- SELL with DEDUCTED_FROM_RETURNS:
* Same calculation as above
* Fee = $50,000 * 0.1% = $50 USDT
3. Percentage Fees in Base Currency (e.g., BTC in BTC-USDT):
- Always based on trade amount
- BUY with ADDED_TO_COSTS:
* Trading 1 BTC with 0.1% fee
* Fee = 1 BTC * 0.1% = 0.001 BTC
- BUY with DEDUCTED_FROM_RETURNS:
* Same calculation as above
* Fee = 1 BTC * 0.1% = 0.001 BTC
- SELL with ADDED_TO_COSTS:
* Trading 1 BTC with 0.1% fee
* Trading 1 BTC at $50,000 with 0.1% fee
* Fee = 1 BTC * 0.1% = 0.001 BTC
- SELL with DEDUCTED_FROM_RETURNS:
* Same calculation as above
* Fee = 1 BTC * 0.1% = 0.001 BTC
* Trading 1 BTC at $50,000 with 0.1% fee
* Fee = $50,000 * 0.1% = $50 USDT
4. Percentage Fees in Other Assets:
3. Percentage Fees in Other Assets:
- Not supported yet
- Would require price data for conversion
Expand All @@ -130,26 +126,32 @@ def _get_fee_impact(cls, order_details: OrderDetails) -> dict[Asset, Decimal]:
NotImplementedError: If fee is in an asset not involved in the trade
ValueError: If fee type is not supported
"""
fee_asset = order_details.fee.asset

# Handle absolute fees (fixed amount)
if order_details.fee.fee_type == FeeType.ABSOLUTE:
return {fee_asset: order_details.fee.amount}
if order_details.fee.asset is None:
raise ValueError("Fee asset is required for absolute fees")
return {order_details.fee.asset: order_details.fee.amount}

# Handle percentage fees
if order_details.fee.fee_type == FeeType.PERCENTAGE:
# Verify fee asset is involved in the trade
expected_asset = cls._get_outflow_asset(order_details)
if fee_asset != expected_asset:
# Get expected fee asset based on impact type
expected_asset = cls._get_expected_fee_asset(order_details)

# If fee asset is specified, verify it matches expected
if (
order_details.fee.asset is not None
and order_details.fee.asset != expected_asset
):
raise NotImplementedError(
"Percentage fee on not involved asset not supported yet"
"Percentage fee on not involved asset not supported yet. "
f"Fee asset: {str(order_details.fee.asset)}, expected asset: {str(expected_asset)}"
)

# Determine if fee should be based on notional value
# Calculate fee amount based on whether it's quote or base currency
quote_asset = AssetFactory.get_asset(
order_details.platform, order_details.trading_pair.quote
)
is_quote_currency_fee = fee_asset == quote_asset
is_quote_currency_fee = expected_asset == quote_asset

# Calculate fee amount
if is_quote_currency_fee:
Expand All @@ -164,7 +166,7 @@ def _get_fee_impact(cls, order_details: OrderDetails) -> dict[Asset, Decimal]:
order_details.fee.amount / Decimal("100")
)

return {fee_asset: fee_amount}
return {expected_asset: fee_amount}

# Handle unsupported fee types
raise ValueError(f"Unsupported fee type: {order_details.fee.fee_type}")
Expand Down Expand Up @@ -212,7 +214,7 @@ def get_involved_assets(

# Fee
if order_details.fee.impact_type == FeeImpactType.ADDED_TO_COSTS:
fee_asset = order_details.fee.asset
fee_asset = cls._get_expected_fee_asset(order_details)
result.append(
AssetCashflow(
asset=fee_asset,
Expand Down
Loading

0 comments on commit 6b59c97

Please sign in to comment.