Skip to content

Commit

Permalink
Merge branch 'feature/backtesting_order_info' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
gbeced committed Feb 18, 2025
2 parents 1e01217 + 508b5a7 commit 8f70b1e
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 61 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## TBD

### Features

* `backtesting.exchange.OrderInfo` now includes pair and order fills.

## 1.7.1

### Bug fixes
Expand Down Expand Up @@ -47,7 +53,7 @@

## 1.5.0

* `basana.backtesting.fees.Percentage` now supports a minimum fee.
* `backtesting.fees.Percentage` now supports a minimum fee.

## 1.4.1

Expand Down
1 change: 1 addition & 0 deletions basana/backtesting/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

BarEventHandler = bar.BarEventHandler
Error = errors.Error
Fill = orders.Fill
LiquidityStrategyFactory = Callable[[], liquidity.LiquidityStrategy]
OrderEvent = order_mgr.OrderEvent
OrderEventHandler = order_mgr.OrderEventHandler
Expand Down
25 changes: 16 additions & 9 deletions basana/backtesting/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,22 @@ class OrderState(enum.Enum):
CANCELED = 102


@dataclasses.dataclass
class Fill:
#: The time when the fill took place.
when: datetime.datetime
#: The balance updates.
balance_updates: Dict[str, Decimal]
#: The fees.
fees: Dict[str, Decimal]


@dataclasses.dataclass
class OrderInfo:
#: The order id.
id: str
#: The pair.
pair: Pair
#: True if the order is open, False otherwise.
is_open: bool
#: The operation.
Expand All @@ -62,6 +74,8 @@ class OrderInfo:
stop_price: Optional[Decimal] = None
#: The ids of the associated loans.
loan_ids: List[str] = dataclasses.field(default_factory=list)
#: The fills.
fills: List[Fill] = dataclasses.field(default_factory=list)

@property
def fill_price(self) -> Optional[Decimal]:
Expand All @@ -72,13 +86,6 @@ def fill_price(self) -> Optional[Decimal]:
return fill_price


@dataclasses.dataclass
class Fill:
when: datetime.datetime
balance_updates: Dict[str, Decimal]
fees: Dict[str, Decimal]


# This is an internal abstraction to be used by the exchange.
class Order(metaclass=abc.ABCMeta):
def __init__(
Expand Down Expand Up @@ -171,11 +178,11 @@ def add_loan(self, loan_id: str):

def get_order_info(self) -> OrderInfo:
return OrderInfo(
id=self.id, is_open=self._state == OrderState.OPEN, operation=self.operation,
id=self.id, pair=self.pair, is_open=self._state == OrderState.OPEN, operation=self.operation,
amount=self.amount, amount_filled=self.amount_filled, amount_remaining=self.amount_pending,
quote_amount_filled=self.quote_amount_filled,
fees={symbol: -amount for symbol, amount in self._fees.items() if amount},
loan_ids=[loan_id for loan_id in self._loan_ids]
loan_ids=[loan_id for loan_id in self._loan_ids], fills=self.fills
)

@abc.abstractmethod
Expand Down
2 changes: 2 additions & 0 deletions docs/backtesting_exchange.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ basana.backtesting.exchange
:members:
.. autoclass:: basana.backtesting.exchange.CanceledOrder
:members:
.. autoclass:: basana.backtesting.exchange.Fill
:members:
.. autoclass:: basana.backtesting.exchange.OrderInfo
:members:
.. autoclass:: basana.backtesting.exchange.OpenOrder
Expand Down
72 changes: 33 additions & 39 deletions tests/test_backtesting_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import dataclasses
from decimal import Decimal
import asyncio
import datetime
Expand Down Expand Up @@ -265,8 +264,7 @@ async def impl():
# Optional[expected_order_events],
# ),
# ],
# }
# },
{
datetime.date(2000, 1, 4): [
# Stop order canceled due to insufficient funds. Need to tweak the amount and stop price to get the order
Expand Down Expand Up @@ -301,15 +299,15 @@ async def impl():
OrderOperation.BUY, Pair("ORCL", "USD"), Decimal("2")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 1, 5, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("2"), "USD": Decimal("-231.00")},
fees={"USD": Decimal("-0.58")},
),
],
[
orders.OrderInfo(
id="any",
dict(
pair=Pair("ORCL", "USD"),
is_open=True,
operation=OrderOperation.BUY,
amount=Decimal("2"),
Expand All @@ -318,15 +316,14 @@ async def impl():
quote_amount_filled=Decimal("0"),
fees={}
),
orders.OrderInfo(
id="any",
dict(
is_open=False,
operation=OrderOperation.BUY,
amount=Decimal("2"),
amount_filled=Decimal("2"),
amount_remaining=Decimal("0"),
quote_amount_filled=Decimal("231.00"),
fees={"USD": Decimal("0.58")}
fees={"USD": Decimal("0.58")},
),
]
),
Expand All @@ -336,7 +333,7 @@ async def impl():
OrderOperation.BUY, Pair("ORCL", "USD"), Decimal("4"), Decimal("110.01")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 1, 5, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("4"), "USD": Decimal("-440.04")},
fees={"USD": Decimal("-1.11")},
Expand All @@ -352,7 +349,7 @@ async def impl():
OrderOperation.SELL, Pair("ORCL", "USD"), Decimal("1")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 1, 19, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("-1"), "USD": Decimal("107.87")},
fees={"USD": Decimal("-0.27")},
Expand All @@ -366,7 +363,7 @@ async def impl():
OrderOperation.SELL, Pair("ORCL", "USD"), Decimal("1"), Decimal("108")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 1, 19, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("-1"), "USD": Decimal("108.00")},
fees={"USD": Decimal("-0.27")},
Expand All @@ -380,7 +377,7 @@ async def impl():
OrderOperation.SELL, Pair("ORCL", "USD"), Decimal("1"), Decimal("108")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 1, 19, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("-1"), "USD": Decimal("107.87")},
fees={"USD": Decimal("-0.27")},
Expand All @@ -397,7 +394,7 @@ async def impl():
Decimal("58.03")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 1, 25, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("5"), "USD": Decimal("-290.15")},
fees={"USD": Decimal("-0.73")},
Expand All @@ -414,7 +411,7 @@ async def impl():
Decimal("80.24")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 3, 14, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("10"), "USD": Decimal("-785.00")},
fees={"USD": Decimal("-1.97")},
Expand All @@ -429,7 +426,7 @@ async def impl():
Decimal("81")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 3, 11, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("9"), "USD": Decimal("-729.00")},
fees={"USD": Decimal("-1.83")},
Expand All @@ -444,7 +441,7 @@ async def impl():
Decimal("78.75")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 3, 14, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("-1"), "USD": Decimal("78.75")},
fees={"USD": Decimal("-0.20")},
Expand All @@ -459,7 +456,7 @@ async def impl():
Decimal("83.65")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 3, 15, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("-1"), "USD": Decimal("83.65")},
fees={"USD": Decimal("-0.21")},
Expand All @@ -474,7 +471,7 @@ async def impl():
Decimal("83.80")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 3, 16, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("-1"), "USD": Decimal("84.00")},
fees={"USD": Decimal("-0.21")},
Expand All @@ -492,20 +489,19 @@ async def impl():
OrderOperation.BUY, Pair("ORCL", "USD"), Decimal("50"), Decimal("10")
),
[
orders.Fill(
dict(
when=datetime.datetime(2001, 1, 4, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("25"), "USD": Decimal("-137.50")},
fees={"USD": Decimal("-0.35")},
),
orders.Fill(
dict(
when=datetime.datetime(2001, 1, 5, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("25"), "USD": Decimal("-137.50")},
fees={"USD": Decimal("-0.34")},
),
],
[
orders.OrderInfo(
id="any",
dict(
is_open=True,
operation=OrderOperation.BUY,
amount=Decimal("50"),
Expand All @@ -515,8 +511,7 @@ async def impl():
fees={},
limit_price=Decimal("10"),
),
orders.OrderInfo(
id="any",
dict(
is_open=True,
operation=OrderOperation.BUY,
amount=Decimal("50"),
Expand All @@ -526,8 +521,7 @@ async def impl():
fees={"USD": Decimal("0.35")},
limit_price=Decimal("10"),
),
orders.OrderInfo(
id="any",
dict(
is_open=False,
operation=OrderOperation.BUY,
amount=Decimal("50"),
Expand All @@ -549,7 +543,7 @@ async def impl():
OrderOperation.BUY, Pair("ORCL", "USD"), Decimal("8600"), Decimal("115.50")
),
[
orders.Fill(
dict(
when=datetime.datetime(2000, 1, 5, tzinfo=tz.tzlocal()),
balance_updates={"ORCL": Decimal("8600"), "USD": Decimal("-993300.00")},
fees={"USD": Decimal("-2483.25")},
Expand Down Expand Up @@ -627,23 +621,23 @@ async def impl():

for order_id, expected_attrs in expected.items():
order_info = await e.get_order_info(order_id)
assert order_info is not None
assert not order_info.is_open, order_info

exchange_order = e._order_mgr._orders.get(order_id)
assert exchange_order is not None
assert exchange_order.fills == expected_attrs["fills"]
# Check fills.
if expected_attrs["fills"] is not None:
for i, expected_fill in enumerate(expected_attrs["fills"]):
order_fill = order_info.fills[i]
for attr, expected_value in expected_fill.items():
assert getattr(order_fill, attr) == expected_value

# Check order events
if expected_attrs["order_events"] is not None:
def normalize_order_info(order):
ret = dataclasses.asdict(order)
ret.pop("id")
return ret

assert order_id in order_events
assert len(order_events[order_id]) == len(expected_attrs["order_events"])
for lhs, rhs in zip(order_events[order_id], expected_attrs["order_events"]):
assert normalize_order_info(lhs) == normalize_order_info(rhs)
for i, expected_order_event in enumerate(expected_attrs["order_events"]):
order_event = order_events[order_id][i]
for attr, expected_value in expected_order_event.items():
assert getattr(order_event, attr) == expected_value

assert len(expected) == sum([len(orders) for orders in order_plan.values()])

Expand Down
Loading

0 comments on commit 8f70b1e

Please # to comment.