Skip to content
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
38 changes: 30 additions & 8 deletions api/databases/crud/grid_ladder_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,20 +293,42 @@ def update_error_logs(
ladder_id: UUID,
error: Exception | str,
) -> GridLadderTable | None:
ladder = self.session.get(GridLadderTable, ladder_id)
if ladder is None:
return None

ts = ts_to_humandate(ts=timestamp())
error_message = str(error)
if isinstance(error, Exception):
error_type = error.__class__.__name__
else:
error_type = "error"

return self.update_logs(
ladder_id,
{
"event": "error",
"error_type": error_type,
"message": error_message,
},
)
error_entry = {
"timestamp": ts,
"error_type": error_type,
"message": error_message,
}
logs = list(ladder.logs or [])
logs.append({"event": "error", **error_entry})

context = dict(ladder.context or {})
execution_errors = context.get("execution_error")
if execution_errors is None:
execution_errors = []
elif not isinstance(execution_errors, list):
execution_errors = [execution_errors]
context["execution_error"] = [*execution_errors, error_entry]

ladder.logs = logs
ladder.context = context
ladder.updated_at = timestamp()
flag_modified(ladder, "logs")
flag_modified(ladder, "context")
self.session.add(ladder)
self.session.commit()
self.session.refresh(ladder)
return ladder

def create_levels(
self,
Expand Down
35 changes: 27 additions & 8 deletions api/exchange_apis/kucoin/futures/position_deal.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class PositionDeal(KucoinPositionDeal):
these operations are triggered by websockets
"""

TRAILING_STOP_REFRESH_MIN_IMPROVEMENT_RATIO = 0.002

def __init__(
self,
bot: BotModel,
Expand All @@ -55,6 +57,27 @@ def __init__(
self.api: KucoinApi | KucoinFutures
self.controller: BotTableCrud | PaperTradingTableCrud

def should_refresh_trailing_stop_loss(
self,
current_stop_price: float,
new_stop_price: float,
direction: int,
) -> bool:
if new_stop_price <= 0:
return False

if current_stop_price <= 0:
return True

improvement = (new_stop_price - current_stop_price) * direction
if improvement <= 0:
return False

min_improvement = (
abs(current_stop_price) * self.TRAILING_STOP_REFRESH_MIN_IMPROVEMENT_RATIO
)
return improvement >= min_improvement

def place_reversal_reentry_order(
self,
contracts: float,
Expand Down Expand Up @@ -693,14 +716,10 @@ def exit(self, close_price: float, _: float | None = None) -> BotModel:
# so time to close with net profit
if (
new_trailing_stop_loss - self.active_bot.deal.opening_price
) * direction > 0 and (
self.active_bot.deal.trailing_stop_loss_price == 0
or (
new_trailing_stop_loss
- self.active_bot.deal.trailing_stop_loss_price
)
* direction
> 0
) * direction > 0 and self.should_refresh_trailing_stop_loss(
current_stop_price=self.active_bot.deal.trailing_stop_loss_price,
new_stop_price=new_trailing_stop_loss,
direction=direction,
):
self.active_bot.deal.trailing_stop_loss_price = (
new_trailing_stop_loss
Expand Down
7 changes: 1 addition & 6 deletions api/grid_ladders/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,10 +416,7 @@ def _place_initial_entries(self, ladder: GridLadderTable) -> None:
self.crud.update_status_with_context(
ladder.id,
GridLadderStatus.error,
context_updates={
"execution_error": str(error),
"cancelled_order_ids": placed_order_ids,
},
context_updates={"cancelled_order_ids": placed_order_ids},
)
self.crud.update_error_logs(ladder.id, error)

Expand Down Expand Up @@ -583,7 +580,6 @@ def _mark_order_error(
order: GridOrderTable,
error: Exception | str,
) -> None:
message = str(error)
self.crud.update_order(order.id, status=GRID_ORDER_ERROR_STATUS)
if order.level_id:
self.crud.update_level_order(
Expand All @@ -593,7 +589,6 @@ def _mark_order_error(
self.crud.update_status_with_context(
ladder.id,
GridLadderStatus.error,
context_updates={"execution_error": message},
)
self.crud.recalculate_used_margin(ladder.id)
self.crud.update_error_logs(ladder.id, error)
Expand Down
26 changes: 25 additions & 1 deletion api/tests/test_grid_ladders.py
Original file line number Diff line number Diff line change
Expand Up @@ -1254,14 +1254,38 @@ def test_grid_lifecycle_cancels_partial_initial_orders_on_failure(
"detail"
]
assert detail["status"] == "error"
assert detail["context"]["execution_error"] == "exchange rejected order"
assert detail["context"]["execution_error"][-1]["error_type"] == "RuntimeError"
assert detail["context"]["execution_error"][-1]["message"] == (
"exchange rejected order"
)
assert fake_api.cancelled_symbols == ["ADAUSDCM"]
assert detail["orders"][0]["status"] == OrderStatus.CANCELED.value
assert detail["logs"][-1]["event"] == "error"
assert detail["logs"][-1]["error_type"] == "RuntimeError"
assert detail["logs"][-1]["message"] == "exchange rejected order"


def test_grid_ladder_error_logs_append_to_execution_error(create_test_tables):
with Session(create_test_tables) as session:
ladder = _active_ladder("ERRUSDC")
ladder.context = {"execution_error": "previous error"}
session.add(ladder)
session.commit()
crud = GridLadderCrud(session)

crud.update_error_logs(ladder.id, ValueError("new error"))

updated = crud.get(ladder.id)

assert updated is not None
assert updated.context["execution_error"][0] == "previous error"
assert updated.context["execution_error"][1]["error_type"] == "ValueError"
assert updated.context["execution_error"][1]["message"] == "new error"
assert updated.logs[-1]["event"] == "error"
assert updated.logs[-1]["error_type"] == "ValueError"
assert updated.logs[-1]["message"] == "new error"


def test_sizer_snaps_contracts_to_lot_size():
"""
KuCoin rejects partial lots. Sizer must floor to a multiple of lot_size.
Expand Down
70 changes: 70 additions & 0 deletions api/tests/test_kucoin_futures_stop_loss.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,76 @@ def test_reconcile_trailing_stop_loss_keeps_better_exchange_stop():
assert calls == []


def test_should_refresh_trailing_stop_loss_allows_first_stop():
deal = _make_position_deal()

assert (
PositionDeal.should_refresh_trailing_stop_loss(
deal,
current_stop_price=0.0,
new_stop_price=99.0,
direction=1,
)
is True
)


def test_should_refresh_trailing_stop_loss_blocks_small_long_improvement():
deal = _make_position_deal()

assert (
PositionDeal.should_refresh_trailing_stop_loss(
deal,
current_stop_price=100.0,
new_stop_price=100.1,
direction=1,
)
is False
)


def test_should_refresh_trailing_stop_loss_allows_material_long_improvement():
deal = _make_position_deal()

assert (
PositionDeal.should_refresh_trailing_stop_loss(
deal,
current_stop_price=100.0,
new_stop_price=100.2,
direction=1,
)
is True
)


def test_should_refresh_trailing_stop_loss_blocks_small_short_improvement():
deal = _make_position_deal()

assert (
PositionDeal.should_refresh_trailing_stop_loss(
deal,
current_stop_price=100.0,
new_stop_price=99.9,
direction=-1,
)
is False
)


def test_should_refresh_trailing_stop_loss_allows_material_short_improvement():
deal = _make_position_deal()

assert (
PositionDeal.should_refresh_trailing_stop_loss(
deal,
current_stop_price=100.0,
new_stop_price=99.8,
direction=-1,
)
is True
)


def test_reconcile_exchange_sl_skips_for_margin_short_reversal():
calls: list[str] = []
deal = _make_deal(margin_short_reversal=True)
Expand Down
1 change: 0 additions & 1 deletion terminal/src/app/components/BotCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { type Bot } from "../../features/bots/botInitialState";
import { computeSingleBotProfit } from "../../features/bots/profits";
import { roundDecimals } from "../../utils/math";
import { formatTimestamp, renderDuration } from "../../utils/time";
import { capitalizeFirst } from "../../utils/strings";
import { MarketType } from "../../utils/enums";

type handleCallback = (id: string) => void;
Expand Down
Loading
Loading