Skip to content

Commit

Permalink
Merge branch 'hotfix/1.6.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
gbeced committed Aug 14, 2024
2 parents d3b06ba + a9c1680 commit b9fdb68
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 22 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.6.2

### Bug fixes

* VolumeShareImpact.calculate_price and VolumeShareImpact.calculate_amount were failing when there was no available liquidity.

## 1.6.1

### Bug fixes
Expand Down
38 changes: 26 additions & 12 deletions basana/backtesting/liquidity.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from decimal import Decimal
import abc

from basana.backtesting import errors
from basana.core import bar


Expand Down Expand Up @@ -93,7 +94,8 @@ def calculate_amount(self, price_impact: Decimal) -> Decimal:


class VolumeShareImpact(LiquidityStrategy):
"""The price impact is calculated by multiplying the price impact constant by the square of the ratio of the used
"""
The price impact is calculated by multiplying the price impact constant by the square of the ratio of the used
volume to the total volume.
:param volume_limit_pct: Maximum percentage of volume that can be used from each bar.
Expand All @@ -116,18 +118,23 @@ def on_bar(self, bar: bar.Bar):
def _volume_share_impact(self, used_liquidity: Decimal) -> Decimal:
# impact = (used_liquidity / (used_liquidity + available_liquidity)) ** 2 * price_impact
assert used_liquidity >= Decimal(0), f"Invalid used_liquidity {used_liquidity}"
assert used_liquidity <= self._total_liquidity, f"Invalid used_liquidity {used_liquidity}"
assert used_liquidity <= self._total_liquidity, f"used_liquidity {used_liquidity} too high"

used_pct = used_liquidity / self._total_liquidity
return used_pct ** Decimal(2) * self._price_impact_pct
if used_liquidity == Decimal(0):
ret = Decimal(0)
else:
used_pct = used_liquidity / self._total_liquidity
ret = used_pct ** Decimal(2) * self._price_impact_pct
return ret

@property
def available_liquidity(self) -> Decimal:
return self._total_liquidity - self._used_liquidity

def take_liquidity(self, amount: Decimal) -> Decimal:
assert amount > 0, f"Invalid amount {amount}"
assert amount <= self.available_liquidity, f"amount {amount} too high"
assert amount >= Decimal(0), f"Invalid amount {amount}"
if amount > self.available_liquidity:
raise errors.Error("Not enough liquidity")

impact_pre = self._volume_share_impact(self._used_liquidity)
self._used_liquidity += amount
Expand All @@ -138,7 +145,8 @@ def take_liquidity(self, amount: Decimal) -> Decimal:

def calculate_price_impact(self, amount: Decimal) -> Decimal:
assert amount >= Decimal(0), f"Invalid amount {amount}"
assert amount <= self.available_liquidity, f"amount {amount} too high"
if amount > self.available_liquidity:
raise errors.Error("Not enough liquidity")

return self._volume_share_impact(self._used_liquidity + amount)

Expand All @@ -150,8 +158,14 @@ def calculate_amount(self, price_impact: Decimal) -> Decimal:
# sqrt(price_impact / self._price_impact_pct) = used_liquidity / self._total_liquidity
# used_liquidity = self._total_liquidity * sqrt(price_impact / self._price_impact_pct)

price_impact = min(price_impact, self._price_impact_pct)
used_liquidity = self._total_liquidity * (price_impact / self._price_impact_pct).sqrt()

assert used_liquidity <= self._total_liquidity
return max(Decimal(0), used_liquidity - self._used_liquidity)
if price_impact == Decimal(0):
ret = Decimal(0)
elif self.available_liquidity == Decimal(0) or self._price_impact_pct == Decimal(0):
raise errors.Error("Not enough liquidity")
else:
price_impact = min(price_impact, self._price_impact_pct)
used_liquidity = self._total_liquidity * (price_impact / self._price_impact_pct).sqrt()

assert used_liquidity <= self._total_liquidity
ret = max(Decimal(0), used_liquidity - self._used_liquidity)
return ret
13 changes: 8 additions & 5 deletions basana/backtesting/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,11 @@ def __init__(
self._limit_price = limit_price

def get_balance_updates(self, bar: bar.Bar, liquidity_strategy: liquidity.LiquidityStrategy) -> Dict[str, Decimal]:
price = None
amount = min(self.amount_pending, liquidity_strategy.available_liquidity)
if not amount:
return {}

price = None
base_sign = helpers.get_base_sign_for_operation(self.operation)

if self.operation == OrderOperation.BUY:
Expand All @@ -275,7 +278,7 @@ def get_balance_updates(self, bar: bar.Bar, liquidity_strategy: liquidity.Liquid
price = self._limit_price

ret = {}
if price:
if amount and price:
ret = {
self.pair.base_symbol: amount * base_sign,
self.pair.quote_symbol: price * amount * -base_sign
Expand Down Expand Up @@ -384,8 +387,8 @@ def get_balance_updates_before_stop_hit(
assert not self._stop_price_hit

price = None
amount = min(self.amount_pending, liquidity_strategy.available_liquidity)
base_sign = helpers.get_base_sign_for_operation(self.operation)
amount = min(self.amount_pending, liquidity_strategy.available_liquidity)

if self.operation == OrderOperation.BUY:
# Stop price was hit at bar open.
Expand Down Expand Up @@ -428,7 +431,7 @@ def get_balance_updates_before_stop_hit(
price = slipped_price(price, self.operation, amount, liquidity_strategy, cap_low=self._limit_price)

ret = {}
if price is not None:
if amount and price:
ret = {
self.pair.base_symbol: amount * base_sign,
self.pair.quote_symbol: price * amount * -base_sign
Expand Down Expand Up @@ -460,7 +463,7 @@ def get_balance_updates_after_stop_hit(
price = self._limit_price

ret = {}
if price:
if amount and price:
ret = {
self.pair.base_symbol: amount * base_sign,
self.pair.quote_symbol: price * amount * -base_sign
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "basana"
version = "1.6.1"
version = "1.6.2"
homepage = "https://github.com/gbeced/basana"
repository = "https://github.com/gbeced/basana"
documentation = "https://basana.readthedocs.io/en/latest/"
Expand Down
62 changes: 58 additions & 4 deletions tests/test_backtesting_liquidity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

from decimal import Decimal

import pytest

from basana.core import pair, bar, dt
from basana.backtesting import liquidity

Expand Down Expand Up @@ -62,10 +64,62 @@ def test_volume_share_impact():
assert strat.available_liquidity == Decimal("2500")

cummulative_slippage = Decimal(0)
for i in range(10):
for _ in range(10):
cummulative_slippage += strat.take_liquidity(Decimal("250"))
assert cummulative_slippage == Decimal("0.1")
assert strat.available_liquidity == Decimal("0")

with pytest.raises(Exception, match="Not enough liquidity"):
strat.calculate_amount(Decimal("0.1"))
with pytest.raises(Exception, match="Not enough liquidity"):
strat.calculate_price_impact(Decimal("1"))


def test_volume_share_impact_without_liquidity():
strat = liquidity.VolumeShareImpact()
strat.on_bar(
bar.Bar(
dt.utc_now(), pair.Pair("BTC", "USD"),
Decimal("50000"), Decimal("70000"), Decimal("49900"), Decimal("69999.07"), Decimal(0)
)
)

assert strat.available_liquidity == Decimal(0)
assert strat.calculate_price_impact(Decimal(0)) == Decimal(0)
assert strat.calculate_amount(Decimal(0)) == Decimal(0)
assert strat.take_liquidity(Decimal(0)) == Decimal(0)

error_msg = "Not enough liquidity"
with pytest.raises(Exception, match=error_msg):
strat.calculate_price_impact(Decimal("0.00001"))
with pytest.raises(Exception, match=error_msg):
strat.calculate_amount(Decimal("0.01"))
with pytest.raises(Exception, match=error_msg):
strat.take_liquidity(Decimal("0.00001"))


def test_volume_share_impact_with_zero_price_impact():
strat = liquidity.VolumeShareImpact(price_impact=Decimal(0))
strat.on_bar(
bar.Bar(
dt.utc_now(), pair.Pair("BTC", "USD"),
Decimal("50000"), Decimal("70000"), Decimal("49900"), Decimal("69999.07"), Decimal("100")
)
)

assert strat.available_liquidity == Decimal(25)
assert strat.calculate_price_impact(Decimal(0)) == Decimal(0)
assert strat.calculate_price_impact(Decimal(1)) == Decimal(0)
assert strat.calculate_price_impact(Decimal(25)) == Decimal(0)
with pytest.raises(Exception, match="Not enough liquidity"):
strat.calculate_price_impact(Decimal(26))

assert strat.calculate_amount(Decimal(0)) == Decimal(0)
with pytest.raises(Exception, match="Not enough liquidity"):
strat.calculate_amount(Decimal(1))

assert strat.calculate_amount(Decimal("0.1")) == Decimal("0")
assert strat.calculate_amount(Decimal("0.01")) == Decimal("0")
assert strat.calculate_amount(Decimal("0.001")) == Decimal("0")
assert strat.take_liquidity(Decimal(0)) == Decimal(0)
assert strat.take_liquidity(Decimal(5)) == Decimal(0)
assert strat.take_liquidity(Decimal(20)) == Decimal(0)
with pytest.raises(Exception, match="Not enough liquidity"):
strat.take_liquidity(Decimal(1))
29 changes: 29 additions & 0 deletions tests/test_backtesting_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,32 @@ def test_get_balance_updates_with_finite_liquidity(order, expected_balance_updat
balance_updates = value_map.ValueMap(order.get_balance_updates(b, ls))
e._order_mgr._round_balance_updates(balance_updates, order.pair)
assert balance_updates == expected_balance_updates


@pytest.mark.parametrize(
"order", [
LimitOrder(
uuid4().hex, OrderOperation.SELL, Pair("BTC", "USD"), Decimal("1"), Decimal("39000.01"), OrderState.OPEN
),
LimitOrder(
uuid4().hex, OrderOperation.BUY, Pair("BTC", "USD"), Decimal("1"), Decimal("49000.01"), OrderState.OPEN
),
StopLimitOrder(
uuid4().hex, OrderOperation.SELL, Pair("BTC", "USD"), Decimal("1"), Decimal("39000.01"),
Decimal("39000.01"), OrderState.OPEN
),
]
)
def test_no_liquidity_calculating_balance_updates(order, backtesting_dispatcher):
e = exchange.Exchange(backtesting_dispatcher, {}) # Just for rounding purposes
p = Pair("BTC", "USD")
e.set_pair_info(p, PairInfo(8, 2))

ls = liquidity.VolumeShareImpact()
b = bar.Bar(
dt.local_now(), p,
Decimal("40001.76"), Decimal("50401.01"), Decimal("30000"), Decimal("45157.09"), Decimal("0")
)
ls.on_bar(b)

assert order.get_balance_updates(b, ls) == {}

0 comments on commit b9fdb68

Please sign in to comment.