Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve "Extend integration tests" #41

Merged
merged 5 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions src/kraken_infinity_grid/gridbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def __init__(
)
self.database.init_db()

async def on_message( # noqa: C901,PLR0912,PLR0911
async def on_message( # noqa: C901, PLR0912
self: Self,
message: dict | list,
) -> None:
Expand Down Expand Up @@ -281,10 +281,10 @@ async def on_message( # noqa: C901,PLR0912,PLR0911
# 108297.6, 'change': -2800.0, 'change_pct': -2.62}]}
self.configuration.update({"last_price_time": datetime.now()})

last_price = self.ticker.last
# last_price = self.ticker.last
self.ticker = SimpleNamespace(last=float(data[0]["last"]))
if last_price == self.ticker.last:
return
# if last_price == self.ticker.last:
# return

if self.unsold_buy_order_txids.count() != 0:
self.om.add_missed_sell_orders()
Expand Down
15 changes: 13 additions & 2 deletions src/kraken_infinity_grid/order_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ def new_buy_order(
if txid_to_delete is not None:
self.__s.orderbook.remove(filters={"txid": txid_to_delete})

if len(self.__s.get_active_buy_orders().all()) >= self.__s.n_open_buy_orders: # type: ignore[no-untyped-call]
return

# Check if algorithm reached the max_investment value
if self.__s.max_investment_reached:
return
Expand Down Expand Up @@ -421,6 +424,8 @@ def new_sell_order( # noqa: C901

# ======================================================================
if txid_id_to_delete is not None: # If corresponding buy order filled
# GridSell always has txid_id_to_delete set.

# Add the txid of the corresponding buy order to the unsold buy
# order txids in order to ensure that the corresponding sell order
# will be placed - even if placing now fails.
Expand Down Expand Up @@ -476,7 +481,13 @@ def new_sell_order( # noqa: C901
),
)

if self.__s.strategy in {"GridHODL", "SWING"}:
if self.__s.strategy in {"GridHODL", "SWING"} or (
self.__s.strategy == "GridSell" and volume is None
):
# For GridSell: This is only the case if there is no corresponding
# buy order and the sell order was placed, e.g. due to an extra sell
# order via selling of partially filled buy orders.

# Respect the fee to not reduce the quote currency over time, while
# accumulating the base currency.
volume = float(
Expand All @@ -487,7 +498,6 @@ def new_sell_order( # noqa: C901
pair=self.__s.symbol,
),
)

# ======================================================================

# Check if there is enough base currency available for selling.
Expand Down Expand Up @@ -674,6 +684,7 @@ def handle_cancel_order(self: OrderManager, txid: str) -> None:
if self.__s.dry_run:
LOG.info("DRY RUN: Not cancelling order: %s", txid)
return

self.__s.trade.cancel_order(txid=txid)
self.__s.orderbook.remove(filters={"txid": txid})

Expand Down
25 changes: 21 additions & 4 deletions tests/integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,34 @@ mainly focus on mocking the external dependencies (python-kraken-sdk) in order
to simulate the real-world exchange and the behavior of the system on new ticker
events, executions and so on.

**Characteristics:**
**Main Test Cases**

The following cases are implemented in the integration tests.

| Test Case | GridSell | GridHODL | SWING | DCA |
| ---------------------------------------------- | -------- | -------- | ----- | --- |
| Run through initial setup | x | x | x | x |
| Placement of $n$ buy orders | x | x | x | x |
| Shifting-up buy orders | x | x | x | x |
| Filling a buy order | x | x | x | x |
| Ensuring $n$ open buy orders after X | x | x | x | x |
| Filling a sell order | x | x | x | - |
| Rapid price drop | x | x | x | x |
| Rapid price rise | x | x | x | x |
| Max investment reached | x | x | x | x |
| Handling surplus from partly filled buy orders | x | x | x | - |

**Characteristics**

- Mocks external dependencies (Kraken API)
- Tests multiple components working together (Focuses on component interactions)
- Tests full system behavior (Verifies business logic flows end-to-end)
- Verifies component interactions (Uses mocks only for external systems)
- Tests database interactions (Uses in-memory database)
- Tests database interactions (Uses sqlite database)
- Tests message flow and state changes (Validates state changes)

**It does not include the following:**
**It does not include the following**

- Live tests against the live Kraken API
- Tests against the CLI (which would be E2E)
- Tests against the CLI and validating DB (which would be E2E)
- Input validation (as the input is fixed)
189 changes: 159 additions & 30 deletions tests/integration/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
""" Helper data structures used for integration testing. """

import uuid
from typing import Self
from typing import Any, Callable, Self

from kraken.spot import Trade, User

Expand All @@ -32,52 +32,181 @@ def __init__(self: Self, **kwargs) -> None: # noqa: ANN003
"XXBT": {"balance": "100.0", "hold_trade": "0.0"},
"ZUSD": {"balance": "1000000.0", "hold_trade": "0.0"},
}

def create_order(self: Self, **kwargs) -> None: # noqa: ANN003
self.__orders |= {
(txid := str(uuid.uuid4()).upper()): {
"userref": kwargs["userref"],
"descr": {
"pair": "BTCUSD",
"type": kwargs["side"],
"ordertype": kwargs["ordertype"],
"price": kwargs["price"],
},
"status": "open",
"vol": kwargs["volume"],
"vol_exec": "0.0",
"cost": "0.0",
"fee": "0.0",
self.__fee = 0.0025

def create_order(self: Self, **kwargs) -> dict: # noqa: ANN003
"""Create a new order and update balances if needed."""
txid = str(uuid.uuid4()).upper()
order = {
"userref": kwargs["userref"],
"descr": {
"pair": "BTCUSD",
"type": kwargs["side"],
"ordertype": kwargs["ordertype"],
"price": kwargs["price"],
},
"status": "open",
"vol": kwargs["volume"],
"vol_exec": "0.0",
"cost": "0.0",
"fee": "0.0",
}

if kwargs["side"] == "buy":
required_balance = float(kwargs["price"]) * float(kwargs["volume"])
if float(self.__balances["ZUSD"]["balance"]) < required_balance:
raise ValueError("Insufficient balance to create buy order")
self.__balances["ZUSD"]["balance"] = str(
float(self.__balances["ZUSD"]["balance"]) - required_balance,
)
self.__balances["ZUSD"]["hold_trade"] = str(
float(self.__balances["ZUSD"]["hold_trade"]) + required_balance,
)
elif kwargs["side"] == "sell":
if float(self.__balances["XXBT"]["balance"]) < float(kwargs["volume"]):
raise ValueError("Insufficient balance to create sell order")
self.__balances["XXBT"]["balance"] = str(
float(self.__balances["XXBT"]["balance"]) - float(kwargs["volume"]),
)
self.__balances["XXBT"]["hold_trade"] = str(
float(self.__balances["XXBT"]["hold_trade"]) + float(kwargs["volume"]),
)

self.__orders[txid] = order
return {"txid": [txid]}

def cancel_order(self: Self, txid: str) -> None:
self.__orders.get(txid, {}).update({"status": "canceled"})

def fill_order(self: Self, txid: str) -> None:
def fill_order(self: Self, txid: str, volume: float | None = None) -> None:
"""Fill an order and update balances."""
order = self.__orders.get(txid, {})
order["fee"] = str(float(order["vol"]) * 0.0025)
order |= {
"status": "closed",
"vol_exec": order["vol"],
"cost": str(float(order["vol"]) + float(order["fee"])),
}
if not order:
return

if volume is None:
volume = float(order["vol"])

if volume > float(order["vol"]) - float(order["vol_exec"]):
raise ValueError(
"Cannot fill order with volume higher than remaining order volume",
)

executed_volume = float(order["vol_exec"]) + volume
remaining_volume = float(order["vol"]) - executed_volume

order["fee"] = str(float(order["vol_exec"]) * self.__fee)
order["vol_exec"] = str(executed_volume)
order["cost"] = str(
executed_volume * float(order["descr"]["price"]) + float(order["fee"]),
)

if remaining_volume <= 0:
order["status"] = "closed"
else:
order["status"] = "open"

self.__orders[txid] = order

if order["descr"]["type"] == "buy":
self.__balances["XXBT"]["balance"] = str(
float(self.__balances["XXBT"]["balance"]) + volume,
)
self.__balances["ZUSD"]["balance"] = str(
float(self.__balances["ZUSD"]["balance"]) - float(order["cost"]),
)
self.__balances["ZUSD"]["hold_trade"] = str(
float(self.__balances["ZUSD"]["hold_trade"]) - float(order["cost"]),
)
elif order["descr"]["type"] == "sell":
self.__balances["XXBT"]["balance"] = str(
float(self.__balances["XXBT"]["balance"]) - volume,
)
self.__balances["XXBT"]["hold_trade"] = str(
float(self.__balances["XXBT"]["hold_trade"]) - volume,
)
self.__balances["ZUSD"]["balance"] = str(
float(self.__balances["ZUSD"]["balance"]) + float(order["cost"]),
)

async def on_ticker_update(self: Self, callback: Callable, last: float) -> None:
"""Update the ticker and fill orders if needed."""
await callback(
{
"channel": "ticker",
"data": [{"symbol": "BTC/USD", "last": last}],
},
)

async def fill_order(txid: str) -> None:
self.fill_order(txid)
await callback(
{
"channel": "executions",
"type": "update",
"data": [{"exec_type": "filled", "order_id": txid}],
},
)

for order in self.get_open_orders()["open"].values():
if (
order["descr"]["type"] == "buy"
and float(order["descr"]["price"]) >= last
) or (
order["descr"]["type"] == "sell"
and float(order["descr"]["price"]) <= last
):
await fill_order(order["txid"])

def cancel_all_orders(self, **kwargs) -> None: # noqa: ARG002,ANN003
def cancel_order(self: Self, txid: str) -> None:
"""Cancel an order and update balances if needed."""
order = self.__orders.get(txid, {})
if not order:
return

order.update({"status": "canceled"})
self.__orders[txid] = order

if order["descr"]["type"] == "buy":
executed_cost = float(order["vol_exec"]) * float(order["descr"]["price"])
remaining_cost = (
float(order["vol"]) * float(order["descr"]["price"]) - executed_cost
)
self.__balances["ZUSD"]["balance"] = str(
float(self.__balances["ZUSD"]["balance"]) + remaining_cost,
)
self.__balances["ZUSD"]["hold_trade"] = str(
float(self.__balances["ZUSD"]["hold_trade"]) - remaining_cost,
)
self.__balances["XXBT"]["balance"] = str(
float(self.__balances["XXBT"]["balance"]) - float(order["vol_exec"]),
)
elif order["descr"]["type"] == "sell":
remaining_volume = float(order["vol"]) - float(order["vol_exec"])
self.__balances["XXBT"]["balance"] = str(
float(self.__balances["XXBT"]["balance"]) + remaining_volume,
)
self.__balances["XXBT"]["hold_trade"] = str(
float(self.__balances["XXBT"]["hold_trade"]) - remaining_volume,
)
self.__balances["ZUSD"]["balance"] = str(
float(self.__balances["ZUSD"]["balance"]) - float(order["cost"]),
)

def cancel_all_orders(self: Self, **kwargs: Any) -> None: # noqa: ARG002
"""Cancel all open orders."""
for txid in self.__orders:
self.cancel_order(txid)

def get_open_orders(self, **kwargs) -> dict: # noqa: ARG002,ANN003
def get_open_orders(self, **kwargs: Any) -> dict: # noqa: ARG002
"""Get all open orders."""
return {
"open": {k: v for k, v in self.__orders.items() if v["status"] == "open"},
}

def get_orders_info(self: Self, txid: str) -> dict:
"""Get information about a specific order."""
if (order := self.__orders.get(txid, None)) is not None:
return {txid: order}
return {}

def get_balances(self, **kwargs) -> dict: # noqa: ARG002, ANN003
def get_balances(self: Self, **kwargs: Any) -> dict: # noqa: ARG002
"""Get the user's current balances."""
return self.__balances
Loading
Loading