Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7a92f02
Added cctx extra.
gbeced Jun 16, 2026
c5895d4
CCXT Support
gbeced Jun 21, 2026
b3b5e3f
Support for fetching open orders and renamed price to limit_price.
gbeced Jun 21, 2026
1f6eb81
Added optional client session.
gbeced Jun 21, 2026
ad76146
Updated class docs.
gbeced Jun 21, 2026
717a734
Added support for bar events.
gbeced Jul 2, 2026
4a6f6cd
Added support for trade events.
gbeced Jul 3, 2026
5e41c0a
Added support for order book events.
gbeced Jul 4, 2026
4ac17bd
Renamed json to raw
gbeced Jul 4, 2026
f8d2ac5
Added example for websocket events.
gbeced Jul 5, 2026
c9e9561
Added support for order events.
gbeced Jul 5, 2026
75eadde
Fixed typo in optional feature and udpated dependencies.
gbeced Jul 5, 2026
e8f73fa
Docs.
gbeced Jul 6, 2026
a956cfa
Fixed out of order processing of bars and orders.
gbeced Jul 10, 2026
c301a42
Replaced assert with exception.
gbeced Jul 10, 2026
246c199
Added checks to PairInfo constructor.
gbeced Jul 10, 2026
859444f
Bug fixed parsing datetimes.
gbeced Jul 10, 2026
97c46b1
Potential fix for pull request finding
gbeced Jul 10, 2026
b241320
Potential fix for pull request finding
gbeced Jul 10, 2026
228b257
Precision is always >= 0.
gbeced Jul 10, 2026
a095102
Added missing testcases.
gbeced Jul 10, 2026
34825b8
Bug fix
gbeced Jul 11, 2026
2fc0b52
Regression test
gbeced Jul 11, 2026
b9bacca
Removed extra space.
gbeced Jul 10, 2026
4adc1a3
Normalized clientOrderId to align with type hint.
gbeced Jul 10, 2026
ad5fb4d
Normalized clientOrderId to align with type hint.
gbeced Jul 10, 2026
6dd3af9
Normalized clientOrderId to align with type hint.
gbeced Jul 10, 2026
8576e1c
Normalized clientOrderId to align with type hint.
gbeced Jul 10, 2026
c514fd1
Getting rid of duplicate code.
gbeced Jul 11, 2026
4d35439
Updated testcase to, hopefully, stop failing on Win32.
gbeced Jul 11, 2026
ab8bb8f
Extracted common code from WatchXYZ event sources to a common base cl…
gbeced Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

* [CCXT](https://docs.ccxt.com) integration.
* `fees.Percentage` now supports charging fees in base currency.
* Both `BacktestingDispatcher` and `RealtimeDispatcher` now support `subscribe_event_loop_started` to register handlers that are called once when the dispatch loop starts.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
## Key Features

* Backtesting exchange so you can try your trading strategies before using real funds.
* Live trading at [Binance](https://www.binance.com/) and [Bitstamp](https://www.bitstamp.net/) crypto currency exchanges.
* Live trading at [Binance](https://www.binance.com/), [Bitstamp](https://www.bitstamp.net/) and 100+ crypto currency exchanges via [CCXT](https://docs.ccxt.com) integration.
* Asynchronous I/O and event driven.

## Getting Started

### Installation

```
$ pip install basana[charts]
$ pip install basana[charts,ccxt]
```

The examples use [TALIpp](https://github.com/nardew/talipp) for the technical indicators, pandas, statsmodels and also [Textual](https://textual.textualize.io/) if you want to run the Binance order book mirror.
Expand Down
1 change: 1 addition & 0 deletions basana/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from .core.enums import (
OrderOperation,
Position,
PrecisionMode,
)

from .core.helpers import (
Expand Down
2 changes: 1 addition & 1 deletion basana/backtesting/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def _get_order_fills(self, op: OrderOperation) -> TimeSeries:
for fill in order.fills:
base_amount = fill.balance_updates[order.pair.base_symbol]
quote_amount = fill.balance_updates[order.pair.quote_symbol]
price = -helpers.truncate_decimal(quote_amount / base_amount, pair_info.quote_precision)
price = -pair_info.truncate_quote(quote_amount / base_amount)
ret.add_value(fill.when, price)
return ret

Expand Down
18 changes: 7 additions & 11 deletions basana/backtesting/order_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,27 +361,23 @@ def _push_order_update(self, order: Order):
def round_balance_updates(order: Order, balance_updates: ValueMap):
# For the base amount we truncate instead of rounding to avoid exceeding available liquidity.
if (base_amount := balance_updates.get(order.pair.base_symbol)) is not None:
balance_updates[order.pair.base_symbol] = core_helpers.truncate_decimal(
base_amount, order.pair_info.base_precision
)
balance_updates[order.pair.base_symbol] = order.pair_info.truncate_base(base_amount)

# For the quote amount we simply round.
if (quote_amount := balance_updates.get(order.pair.quote_symbol)) is not None:
balance_updates[order.pair.quote_symbol] = core_helpers.round_decimal(
quote_amount, order.pair_info.quote_precision
)
balance_updates[order.pair.quote_symbol] = order.pair_info.round_quote(quote_amount)

balance_updates.prune()


def round_fees(order: Order, fees: ValueMap):
precisions = {
order.pair.base_symbol: order.pair_info.base_precision,
order.pair.quote_symbol: order.pair_info.quote_precision,
round_fns = {
order.pair.base_symbol: lambda amount: order.pair_info.round_base(amount, rounding=decimal.ROUND_UP),
order.pair.quote_symbol: lambda amount: order.pair_info.round_quote(amount, rounding=decimal.ROUND_UP),
}
for symbol, precision in precisions.items():
for symbol, round_fn in round_fns.items():
if (amount := fees.get(symbol)) is not None:
fees[symbol] = core_helpers.round_decimal(amount, precision, rounding=decimal.ROUND_UP)
fees[symbol] = round_fn(amount)

fees.prune()

18 changes: 9 additions & 9 deletions basana/backtesting/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from basana.backtesting import helpers, liquidity, value_map
from basana.core import bar, logs
from basana.core.enums import OrderOperation
from basana.core.helpers import round_decimal, truncate_decimal

from basana.core.pair import Pair, PairInfo


Expand Down Expand Up @@ -262,7 +262,7 @@ def try_fill(self, bar: bar.Bar, liquidity_strategy: liquidity.LiquidityStrategy
when=bar.begin,
balance_updates={
self.pair.base_symbol: amount * base_sign,
self.pair.quote_symbol: round_decimal(price * amount * -base_sign, self.pair_info.quote_precision)
self.pair.quote_symbol: self.pair_info.round_quote(price * amount * -base_sign)
},
fees={},
price=price
Expand All @@ -281,7 +281,7 @@ def __init__(

def try_fill(self, bar: bar.Bar, liquidity_strategy: liquidity.LiquidityStrategy) -> Optional[Fill]:
amount = min(self.amount_pending, liquidity_strategy.available_liquidity)
amount = truncate_decimal(amount, self.pair_info.base_precision)
amount = self.pair_info.truncate_base(amount)
if not amount:
return None

Expand Down Expand Up @@ -314,7 +314,7 @@ def try_fill(self, bar: bar.Bar, liquidity_strategy: liquidity.LiquidityStrategy
when=fill_dt,
balance_updates={
self.pair.base_symbol: amount * base_sign,
self.pair.quote_symbol: round_decimal(price * amount * -base_sign, self.pair_info.quote_precision)
self.pair.quote_symbol: self.pair_info.round_quote(price * amount * -base_sign)
},
fees={},
price=price
Expand Down Expand Up @@ -391,7 +391,7 @@ def try_fill(self, bar: bar.Bar, liquidity_strategy: liquidity.LiquidityStrategy
when=fill_dt,
balance_updates={
self.pair.base_symbol: amount * base_sign,
self.pair.quote_symbol: round_decimal(price * amount * -base_sign, self.pair_info.quote_precision)
self.pair.quote_symbol: self.pair_info.round_quote(price * amount * -base_sign)
},
fees={},
price=price
Expand Down Expand Up @@ -487,7 +487,7 @@ def try_fill_before_stop_hit(
when=fill_dt,
balance_updates={
self.pair.base_symbol: amount * base_sign,
self.pair.quote_symbol: round_decimal(price * amount * -base_sign, self.pair_info.quote_precision)
self.pair.quote_symbol: self.pair_info.round_quote(price * amount * -base_sign)
},
fees={},
price=price
Expand Down Expand Up @@ -526,7 +526,7 @@ def try_fill_after_stop_hit(
when=fill_dt,
balance_updates={
self.pair.base_symbol: amount * base_sign,
self.pair.quote_symbol: round_decimal(price * amount * -base_sign, self.pair_info.quote_precision)
self.pair.quote_symbol: self.pair_info.round_quote(price * amount * -base_sign)
},
fees={},
price=price
Expand All @@ -535,7 +535,7 @@ def try_fill_after_stop_hit(

def try_fill(self, bar: bar.Bar, liquidity_strategy: liquidity.LiquidityStrategy) -> Optional[Fill]:
amount = min(self.amount_pending, liquidity_strategy.available_liquidity)
amount = truncate_decimal(amount, self.pair_info.base_precision)
amount = self.pair_info.truncate_base(amount)
if not amount:
return None

Expand Down Expand Up @@ -578,4 +578,4 @@ def slipped_price(
if cap_high is not None:
price = min(price, cap_high)

return round_decimal(price, order.pair_info.quote_precision)
return order.pair_info.round_quote(price)
6 changes: 2 additions & 4 deletions basana/backtesting/prices.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from typing import Dict, Tuple

from basana.backtesting import config, errors, value_map
from basana.core import helpers as core_helpers
from basana.core.bar import Bar, BarEvent
from basana.core.pair import Pair

Expand All @@ -44,9 +43,8 @@ def get_bid_ask(self, pair: Pair) -> Tuple[Decimal, Decimal]:

last_price = last_bar.close
pair_info = self._config.get_pair_info(pair)
half_spread = core_helpers.truncate_decimal(
(last_price * self._bid_ask_spread_pct / Decimal("100")) / Decimal(2),
pair_info.quote_precision
half_spread = pair_info.truncate_quote(
(last_price * self._bid_ask_spread_pct / Decimal("100")) / Decimal(2)
)
bid = last_price - half_spread
ask = last_price + half_spread
Expand Down
42 changes: 32 additions & 10 deletions basana/backtesting/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,28 @@
# limitations under the License.

from decimal import Decimal
from typing import Union
import abc

from basana.backtesting import errors, orders
from basana.core import helpers
from basana.core.enums import OrderOperation
from basana.core.enums import OrderOperation, PrecisionMode
from basana.core.pair import Pair, PairInfo


def base_precision_limit(pair_info: PairInfo) -> Union[int, Decimal]:
if pair_info.precision_mode == PrecisionMode.TICK_SIZE:
assert pair_info.base_tick_size is not None
return pair_info.base_tick_size
return pair_info.base_precision


def quote_precision_limit(pair_info: PairInfo) -> Union[int, Decimal]:
if pair_info.precision_mode == PrecisionMode.TICK_SIZE:
assert pair_info.quote_tick_size is not None
return pair_info.quote_tick_size
return pair_info.quote_precision


class ExchangeOrder(metaclass=abc.ABCMeta):
def __init__(
self, operation: OrderOperation, pair: Pair, pair_info: PairInfo,
Expand Down Expand Up @@ -62,9 +76,11 @@ def auto_repay(self) -> bool:
def validate(self, pair_info: PairInfo):
if self.amount <= Decimal(0):
raise errors.Error("Amount must be > 0")
if self.amount != helpers.truncate_decimal(self.amount, pair_info.base_precision):
if self.amount != pair_info.truncate_base(self.amount):
raise errors.Error(
"{} exceeds maximum precision of {} decimal digits".format(self.amount, pair_info.base_precision)
"{} exceeds maximum precision of {} {}".format(
self.amount, base_precision_limit(pair_info), pair_info.precision_unit
)
)

@abc.abstractmethod
Expand Down Expand Up @@ -113,9 +129,11 @@ def validate(self, pair_info: PairInfo):
super().validate(pair_info)
if self.limit_price <= Decimal(0):
raise errors.Error("Limit price must be > 0")
if self.limit_price != helpers.truncate_decimal(self.limit_price, pair_info.quote_precision):
if self.limit_price != pair_info.truncate_quote(self.limit_price):
raise errors.Error(
"{} exceeds maximum precision of {} decimal digits".format(self.limit_price, pair_info.quote_precision)
"{} exceeds maximum precision of {} {}".format(
self.limit_price, quote_precision_limit(pair_info), pair_info.precision_unit
)
)

def create_order(self, id: str) -> orders.Order:
Expand Down Expand Up @@ -152,9 +170,11 @@ def validate(self, pair_info: PairInfo):
super().validate(pair_info)
if self.stop_price <= Decimal(0):
raise errors.Error("Stop price must be > 0")
if self.stop_price != helpers.truncate_decimal(self.stop_price, pair_info.quote_precision):
if self.stop_price != pair_info.truncate_quote(self.stop_price):
raise errors.Error(
"{} exceeds maximum precision of {} decimal digits".format(self.stop_price, pair_info.quote_precision)
"{} exceeds maximum precision of {} {}".format(
self.stop_price, quote_precision_limit(pair_info), pair_info.precision_unit
)
)

def create_order(self, id: str) -> orders.Order:
Expand Down Expand Up @@ -197,9 +217,11 @@ def validate(self, pair_info: PairInfo):
if self.limit_price <= Decimal(0):
raise errors.Error("Limit price must be > 0")
for value in [self.stop_price, self.limit_price]:
if value != helpers.truncate_decimal(value, pair_info.quote_precision):
if value != pair_info.truncate_quote(value):
raise errors.Error(
"{} exceeds maximum precision of {} decimal digits".format(value, pair_info.quote_precision)
"{} exceeds maximum precision of {} {}".format(
value, quote_precision_limit(pair_info), pair_info.precision_unit
)
)

def create_order(self, id: str) -> orders.Order:
Expand Down
12 changes: 12 additions & 0 deletions basana/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ def __str__(self):
}[self]


@enum.unique
class PrecisionMode(enum.Enum):
"""Enumeration for precision modes."""

#:
DECIMAL_PLACES = 1
#:
SIGNIFICANT_DIGITS = 2
#:
TICK_SIZE = 3


@enum.unique
class Position(enum.Enum):
"""Enumeration for positions."""
Expand Down
Loading
Loading