Skip to content

Commit 26ee869

Browse files
committed
Added base currency support to backtesting's Percentage fee strategy.
1 parent 65fd295 commit 26ee869

2 files changed

Lines changed: 71 additions & 7 deletions

File tree

basana/backtesting/fees.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,20 @@
1717
from decimal import Decimal
1818
from typing import Dict
1919
import abc
20+
import enum
2021

2122
from . import orders
2223

2324

25+
class FeeCurrency(enum.Enum):
26+
"""The currency in which fees are charged."""
27+
28+
#: Fees are charged in quote currency.
29+
QUOTE = "quote"
30+
#: Fees are charged in base currency.
31+
BASE = "base"
32+
33+
2434
class FeeStrategy(metaclass=abc.ABCMeta):
2535
"""Base class for strategies that model fee schemes.
2636
@@ -44,30 +54,37 @@ def calculate_fees(self, order: orders.Order, balance_updates: Dict[str, Decimal
4454

4555

4656
class Percentage(FeeStrategy):
47-
"""This strategy applies a fixed percentage per trade, in quote currency.
57+
"""This strategy applies a fixed percentage per trade.
4858
4959
:param percentage: The percentage to apply.
50-
:param min_fee: Minimum fee amount, in quote currency.
60+
:param min_fee: Minimum fee amount, in the fee currency.
61+
:param fee_currency: The currency in which fees are charged. Defaults to :attr:`FeeCurrency.QUOTE`.
5162
"""
5263

53-
def __init__(self, percentage: Decimal, min_fee: Decimal = Decimal(0)):
64+
def __init__(
65+
self, percentage: Decimal, min_fee: Decimal = Decimal(0),
66+
fee_currency: FeeCurrency = FeeCurrency.QUOTE
67+
):
5468
assert percentage >= 0 and percentage < 100, f"Invalid percentage {percentage}"
5569
assert min_fee >= 0, f"Minimum fee cannot be negative {min_fee}"
5670
self._percentage = percentage
5771
self._min_fee = min_fee
72+
self._fee_currency = fee_currency
5873

5974
def calculate_fees(self, order: orders.Order, balance_updates: Dict[str, Decimal]) -> Dict[str, Decimal]:
6075
ret = {}
6176

62-
# Fees are always charged in quote amount.
63-
symbol = order.pair.quote_symbol
77+
if self._fee_currency == FeeCurrency.BASE:
78+
symbol = order.pair.base_symbol
79+
else:
80+
symbol = order.pair.quote_symbol
6481

6582
# Rounding may have taken place in previous fills, so fees may have been overcharged. For that reason we
6683
# calculate the total fees to charge, and subtract what we have charged so far.
6784
charged_fee_amount = order.fees.get(symbol, Decimal(0))
6885
assert charged_fee_amount <= Decimal(0), "Fees should always be negative"
69-
total_quote_amount = order.balance_updates.get(symbol, Decimal(0)) + balance_updates.get(symbol, Decimal(0))
70-
total_fee_amount = -max(abs(total_quote_amount) * self._percentage / Decimal(100), self._min_fee)
86+
total_amount = order.balance_updates.get(symbol, Decimal(0)) + balance_updates.get(symbol, Decimal(0))
87+
total_fee_amount = -max(abs(total_amount) * self._percentage / Decimal(100), self._min_fee)
7188
pending_fee = total_fee_amount - charged_fee_amount
7289
if pending_fee < Decimal(0):
7390
ret[symbol] = pending_fee

tests/test_backtesting_fees.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,50 @@ def test_percentage_fee_with_minium():
6767
"USD": Decimal("-50.15"),
6868
}
6969
assert fee_strategy.calculate_fees(order, balance_updates) == {"USD": Decimal("-5")}
70+
71+
72+
def test_percentage_fee_base_currency_with_partial_fills():
73+
fee_strategy = fees.Percentage(Decimal("1"), fee_currency=fees.FeeCurrency.BASE)
74+
order = orders.MarketOrder("1", OrderOperation.BUY, btc_pair, btc_pair_info, Decimal("0.01"))
75+
76+
# Fill #1 - fee = 0.001 * 1% = 0.00001
77+
balance_updates = {
78+
"BTC": Decimal("0.001"),
79+
"USD": Decimal("-0.9"),
80+
}
81+
assert fee_strategy.calculate_fees(order, balance_updates) == {"BTC": Decimal("-0.00001")}
82+
order.add_fill(
83+
Fill(dt.utc_now(), balance_updates, {"BTC": Decimal("-0.00001")}, Decimal(900))
84+
)
85+
86+
# Fill #2 - cumulative base = 0.002, total fee = 0.00002, already charged 0.00001, pending = 0.00001
87+
balance_updates = {
88+
"BTC": Decimal("0.001"),
89+
"USD": Decimal("-0.9"),
90+
}
91+
assert fee_strategy.calculate_fees(order, balance_updates) == {"BTC": Decimal("-0.00001")}
92+
order.add_fill(
93+
Fill(dt.utc_now(), balance_updates, {"BTC": Decimal("-0.00001")}, Decimal(900))
94+
)
95+
96+
# Fill #3 - cumulative base = 0.01, total fee = 0.0001, already charged 0.00002, pending = 0.00008
97+
balance_updates = {
98+
"BTC": Decimal("0.008"),
99+
"USD": Decimal("-7.2"),
100+
}
101+
assert fee_strategy.calculate_fees(order, balance_updates) == {"BTC": Decimal("-0.00008")}
102+
order.add_fill(
103+
Fill(dt.utc_now(), balance_updates, {"BTC": Decimal("-0.00008")}, Decimal(900))
104+
)
105+
106+
107+
def test_percentage_fee_base_currency_with_minimum():
108+
fee_strategy = fees.Percentage(Decimal("1"), min_fee=Decimal("0.001"), fee_currency=fees.FeeCurrency.BASE)
109+
order = orders.MarketOrder("1", OrderOperation.BUY, btc_pair, btc_pair_info, Decimal("0.05"))
110+
111+
# 0.05 BTC * 1% = 0.0005 BTC fee, but min_fee=0.001, so fee = 0.001 BTC
112+
balance_updates = {
113+
"BTC": Decimal("0.05"),
114+
"USD": Decimal("-2500"),
115+
}
116+
assert fee_strategy.calculate_fees(order, balance_updates) == {"BTC": Decimal("-0.001")}

0 commit comments

Comments
 (0)