Skip to content
Open
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
10 changes: 1 addition & 9 deletions api/exchange_apis/kucoin/futures/futures_deal.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@
load_futures_order_book,
)
from api.tools.constants import (
RELATIVE_STRENGTH_IMPULSE_RIDER_ALGO,
RELATIVE_STRENGTH_IMPULSE_RIDER_PENDING_ENTRY_CANDLES,
TOP_GAINER_EARLY_MOMENTUM_ALGO,
TOP_GAINER_EARLY_MOMENTUM_PENDING_ENTRY_CANDLES,
TOP_GAINER_FAILURE_REVERSAL_ALGO,
Expand Down Expand Up @@ -1607,13 +1605,7 @@ def base_order(self) -> BotModel:
self.active_bot.deal.opening_timestamp = order.timestamp
self.active_bot.status = Status.pending
pending_entry_minutes = 5
if self.active_bot.name == RELATIVE_STRENGTH_IMPULSE_RIDER_ALGO:
pending_entry_minutes = (
self.interval_ms
* RELATIVE_STRENGTH_IMPULSE_RIDER_PENDING_ENTRY_CANDLES
// 60_000
)
elif self.active_bot.name == TOP_GAINER_EARLY_MOMENTUM_ALGO:
if self.active_bot.name == TOP_GAINER_EARLY_MOMENTUM_ALGO:
pending_entry_minutes = (
self.interval_ms
* TOP_GAINER_EARLY_MOMENTUM_PENDING_ENTRY_CANDLES
Expand Down
60 changes: 4 additions & 56 deletions api/tests/test_kucoin_futures_contract_sizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -964,25 +964,6 @@ def test_non_recovery_entry_uses_body_capped_limit_price(monkeypatch):
assert any("Body-capped entry" in log for log in deal.active_bot.logs)


def test_relative_strength_impulse_rider_uses_prompt_body_capped_entry_after_reclaim(
monkeypatch,
):
deal = prepare_recovery_entry_deal(
monkeypatch,
position=Position.long,
previous_close=100.0,
current_open=101.0,
candle_range=2.0,
)
deal.active_bot.name = "relative_strength_impulse_rider"
deal.active_bot.recovery_params = None

limit_price = deal.body_capped_entry_limit_price()

assert limit_price == 102.0
assert any("Body-capped entry" in log for log in deal.active_bot.logs)


def test_top_gainer_failure_reversal_anchors_prompt_entry_to_current_close(
monkeypatch,
):
Expand Down Expand Up @@ -1405,39 +1386,6 @@ def test_unfilled_capped_base_order_uses_pending_entry_ttl_not_legacy_age_expiry
)


@pytest.mark.parametrize("interval_minutes", [5, 15, 60])
def test_relative_strength_impulse_rider_pending_entry_waits_one_candle(
interval_minutes,
):
position = cast(Any, FuturesPosition.__new__(FuturesPosition))
order = OrderModel(
order_id="impulse-retest-entry",
order_type="limit",
pair="KATUSDTM",
timestamp=1,
order_side="buy",
qty=150,
price=0.00625,
status=OrderStatus.NEW,
time_in_force="GTC",
deal_type=DealType.base_order,
)
position.execution = types.SimpleNamespace(
active_bot=BotModel(
pair="KATUSDTM",
name="relative_strength_impulse_rider",
status=Status.pending,
)
)
interval_ms = interval_minutes * 60 * 1000
position.base_streaming = types.SimpleNamespace(
interval=types.SimpleNamespace(get_ms=lambda: interval_ms)
)

assert position.is_pending_base_entry_expired(order, now_ms=interval_ms) is False
assert position.is_pending_base_entry_expired(order, now_ms=interval_ms + 2) is True


@pytest.mark.parametrize("interval_minutes", [5, 15, 60])
def test_top_gainer_retest_entry_waits_one_configured_candle(interval_minutes):
position = cast(Any, FuturesPosition.__new__(FuturesPosition))
Expand Down Expand Up @@ -1496,7 +1444,7 @@ def test_top_gainer_failure_reversal_pending_entry_expires_after_two_minutes():
assert position.is_pending_base_entry_expired(order, now_ms=120_002) is True


def test_relative_strength_impulse_rider_delayed_fill_starts_holding_clock_at_fill(
def test_top_gainer_short_delayed_fill_starts_holding_clock_at_fill(
monkeypatch,
):
fill_time = datetime(2026, 8, 6, 12, 0, tzinfo=timezone.utc)
Expand All @@ -1510,7 +1458,7 @@ def test_relative_strength_impulse_rider_delayed_fill_starts_holding_clock_at_fi
execution = cast(Any, KucoinPositionDeal.__new__(KucoinPositionDeal))
execution.active_bot = BotModel(
pair="KATUSDTM",
name="relative_strength_impulse_rider",
name="top_gainer_short",
status=Status.pending,
)
position.execution = execution
Expand All @@ -1523,7 +1471,7 @@ def test_relative_strength_impulse_rider_delayed_fill_starts_holding_clock_at_fi
created_at=int(observation_time.timestamp() * 1000),
),
types.SimpleNamespace(
order_id="filled-impulse-retest-entry",
order_id="filled-top-gainer-short-entry",
trade_time=fill_timestamp_ms * 1_000_000,
created_at=fill_timestamp_ms,
),
Expand All @@ -1532,7 +1480,7 @@ def test_relative_strength_impulse_rider_delayed_fill_starts_holding_clock_at_fi
)
execution.open_deal = lambda: execution.active_bot
order = OrderModel(
order_id="filled-impulse-retest-entry",
order_id="filled-top-gainer-short-entry",
order_type="limit",
pair="KATUSDTM",
timestamp=1,
Expand Down
17 changes: 0 additions & 17 deletions api/tests/test_kucoin_futures_stop_loss.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,28 +193,11 @@ def close_all(algorithmic_close: bool = False) -> BotModel:
assert close_calls == [True]


def test_relative_strength_impulse_rider_closes_at_eight_completed_candles() -> None:
close_calls: list[bool] = []
deal = _make_strategy_at_holding_limit(name="relative_strength_impulse_rider")

def close_all(algorithmic_close: bool = False) -> BotModel:
close_calls.append(algorithmic_close)
return deal.execution.active_bot

deal.execution.close_all = close_all

result = Lifecycle.exit(deal, close_price=100.0)

assert result is deal.execution.active_bot
assert close_calls == [True]


def test_fixed_take_profit_precedes_max_holding_close() -> None:
for name in (
"mean_reversion_fade",
"liquidation_sweep_pump",
"coinrule_price_tracker",
"relative_strength_impulse_rider",
):
exit_calls: list[str] = []
deal = _make_strategy_at_holding_limit(
Expand Down
12 changes: 0 additions & 12 deletions api/tests/test_streaming_lifecycle_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@
from streaming.strategies.mean_reversion_fade import (
MeanReversionFadeLifecycleStrategy,
)
from streaming.strategies.relative_strength_impulse_rider import (
RelativeStrengthImpulseRiderLifecycleStrategy,
)
from streaming.strategies.top_gainer_short import TopGainerShortLifecycleStrategy
from streaming.strategies.top_gainer_early_momentum import (
TopGainerEarlyMomentumLifecycleStrategy,
Expand Down Expand Up @@ -142,10 +139,6 @@ def _context(
[
("mean_reversion_fade", MeanReversionFadeLifecycleStrategy),
("liquidation_sweep_pump", LiquidationSweepPumpLifecycleStrategy),
(
"relative_strength_impulse_rider",
RelativeStrengthImpulseRiderLifecycleStrategy,
),
("top_gainer_early_momentum", TopGainerEarlyMomentumLifecycleStrategy),
("top_loser_early_momentum", TopGainerEarlyMomentumLifecycleStrategy),
("top_gainer_short", TopGainerShortLifecycleStrategy),
Expand All @@ -167,13 +160,9 @@ def test_strategy_policies_replace_lifecycle_name_branches() -> None:
evaluator = LifecycleContextEvaluator()

mean_reversion = evaluator.evaluate(_context(name="mean_reversion_fade"))
relative_strength = evaluator.evaluate(
_context(name="relative_strength_impulse_rider")
)
liquidation_sweep = evaluator.evaluate(_context(name="liquidation_sweep_pump"))

assert mean_reversion.policy.low_price_stop_floor_pct is None
assert relative_strength.policy.low_price_stop_floor_pct is None
assert liquidation_sweep.policy.emergency_stop_bounds.minimum_pct == 0.35
assert liquidation_sweep.policy.emergency_stop_bounds.maximum_pct == 0.75
assert evaluator.evaluate(_context()).policy.low_price_stop_floor_pct == 4.0
Expand All @@ -199,7 +188,6 @@ def test_chop_prone_strategies_block_reversal_after_loss(
("liquidation_sweep_pump", Position.short, False),
("coinrule_price_tracker", Position.long, True),
("coinrule_price_tracker", Position.short, False),
("relative_strength_impulse_rider", Position.long, True),
("another_strategy", Position.long, False),
],
)
Expand Down
3 changes: 0 additions & 3 deletions api/tools/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,3 @@

TOP_GAINER_FAILURE_REVERSAL_ALGO = "top_gainer_failure_reversal"
TOP_GAINER_FAILURE_REVERSAL_PENDING_ENTRY_TTL_MS = 2 * 60 * 1000

RELATIVE_STRENGTH_IMPULSE_RIDER_ALGO = "relative_strength_impulse_rider"
RELATIVE_STRENGTH_IMPULSE_RIDER_PENDING_ENTRY_CANDLES = 1
4 changes: 0 additions & 4 deletions streaming/context_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
from streaming.strategies.mean_reversion_fade import (
MeanReversionFadeLifecycleStrategy,
)
from streaming.strategies.relative_strength_impulse_rider import (
RelativeStrengthImpulseRiderLifecycleStrategy,
)
from streaming.strategies.top_gainer_short import TopGainerShortLifecycleStrategy
from streaming.strategies.top_gainer_early_momentum import (
TopGainerEarlyMomentumLifecycleStrategy,
Expand All @@ -37,7 +34,6 @@ class LifecycleContextEvaluator:
STRATEGY_TYPES = (
MeanReversionFadeLifecycleStrategy,
LiquidationSweepPumpLifecycleStrategy,
RelativeStrengthImpulseRiderLifecycleStrategy,
TopGainerEarlyMomentumLifecycleStrategy,
TopGainerShortLifecycleStrategy,
PriceTrackerLifecycleStrategy,
Expand Down
20 changes: 3 additions & 17 deletions streaming/futures_position.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
load_futures_order_book,
)
from api.tools.constants import (
RELATIVE_STRENGTH_IMPULSE_RIDER_ALGO,
RELATIVE_STRENGTH_IMPULSE_RIDER_PENDING_ENTRY_CANDLES,
TOP_GAINER_EARLY_MOMENTUM_ALGO,
TOP_GAINER_SHORT_ALGO,
TOP_GAINER_EARLY_MOMENTUM_PENDING_ENTRY_CANDLES,
Expand Down Expand Up @@ -72,12 +70,7 @@ def __init__(

def is_pending_base_entry_expired(self, order: OrderModel, now_ms: int) -> bool:
pending_entry_ttl_ms = self.PENDING_ENTRY_TTL_MS
if self.execution.active_bot.name == RELATIVE_STRENGTH_IMPULSE_RIDER_ALGO:
pending_entry_ttl_ms = (
self.base_streaming.interval.get_ms()
* RELATIVE_STRENGTH_IMPULSE_RIDER_PENDING_ENTRY_CANDLES
)
elif self.execution.active_bot.name == TOP_GAINER_EARLY_MOMENTUM_ALGO:
if self.execution.active_bot.name == TOP_GAINER_EARLY_MOMENTUM_ALGO:
pending_entry_ttl_ms = (
self.base_streaming.interval.get_ms()
* TOP_GAINER_EARLY_MOMENTUM_PENDING_ENTRY_CANDLES
Expand Down Expand Up @@ -176,8 +169,7 @@ def _activate_filled_base_order(
self.execution.active_bot.deal.base_order_size = order.qty
self.execution.active_bot.deal.opening_timestamp = (
self.execution.matching_exchange_fill_timestamp(order)
if self.execution.active_bot.name
in {RELATIVE_STRENGTH_IMPULSE_RIDER_ALGO, TOP_GAINER_SHORT_ALGO}
if self.execution.active_bot.name == TOP_GAINER_SHORT_ALGO
else order.timestamp
)
if log_message:
Expand Down Expand Up @@ -571,13 +563,7 @@ def _expire_unfilled_base_order(
self.execution.controller.update_order(order)
self.execution.active_bot.status = Status.error
pending_entry_minutes = self.PENDING_ENTRY_TTL_MS // 60_000
if self.execution.active_bot.name == RELATIVE_STRENGTH_IMPULSE_RIDER_ALGO:
pending_entry_minutes = (
self.base_streaming.interval.get_ms()
* RELATIVE_STRENGTH_IMPULSE_RIDER_PENDING_ENTRY_CANDLES
// 60_000
)
elif self.execution.active_bot.name == TOP_GAINER_EARLY_MOMENTUM_ALGO:
if self.execution.active_bot.name == TOP_GAINER_EARLY_MOMENTUM_ALGO:
pending_entry_minutes = (
self.base_streaming.interval.get_ms()
* TOP_GAINER_EARLY_MOMENTUM_PENDING_ENTRY_CANDLES
Expand Down
27 changes: 0 additions & 27 deletions streaming/strategies/relative_strength_impulse_rider.py

This file was deleted.

2 changes: 1 addition & 1 deletion terminal/src/app/components/BotDetailTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const BotDetailTabs: FC<{
const handleActivation = async (id: string) => {
try {
const data = await updateBot({ body: bot, id }).unwrap();
if (data) {
if (data?.bot) {
await trigger(id).unwrap();
navigate(getEditPath(id));
}
Expand Down
47 changes: 47 additions & 0 deletions terminal/src/features/bots/botsApiSlice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { vi } from "vitest";
import { makeStore } from "../../app/store";
import { singleBot } from "./botInitialState";
import { botsApiSlice } from "./botsApiSlice";

class MockRequest {
url: string;
init?: RequestInit;

constructor(input: RequestInfo | URL, init?: RequestInit) {
this.url = String(input);
this.init = init;
}

clone() {
return this;
}
}

afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

it("returns the edited bot from the API data field so Deal can activate it", async () => {
const bot = { ...singleBot, id: "bot-1" };
vi.stubGlobal("Request", MockRequest);
const fetchMock = vi.fn(
async () =>
new Response(
JSON.stringify({
message: "Successfully edited bot.",
error: 0,
data: bot,
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetchMock);

const result = await makeStore().dispatch(
botsApiSlice.endpoints.editBot.initiate({ id: bot.id, body: bot }),
);

expect(result.data).toEqual({ bot });
expect(fetchMock).toHaveBeenCalledOnce();
});
11 changes: 4 additions & 7 deletions terminal/src/features/bots/botsApiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { Bot, BotEntity } from "./botInitialState";
import { computeTotalProfit } from "./profits";
import { BotStatus } from "../../utils/enums";
import type {
CreateBotResponse,
DefaultBotsResponse,
EditBotParams,
SingleBotResponse,
Expand Down Expand Up @@ -126,7 +125,7 @@ export const botsApiSlice = userApiSlice.injectEndpoints({
};
},
}),
editBot: build.mutation<CreateBotResponse, EditBotParams>({
editBot: build.mutation<SingleBotResponse | null, EditBotParams>({
query: ({ body, id }) => ({
url: `${import.meta.env.VITE_GET_BOTS}/${id}`,
method: "PUT",
Expand All @@ -136,15 +135,13 @@ export const botsApiSlice = userApiSlice.injectEndpoints({
"bots",
{ type: "bot", id },
],
transformResponse: ({ botId, message, error }, meta, arg) => {
transformResponse: ({ data, message, error }, meta, arg) => {
if (error && error === 1) {
notifification("error", message);
// Let UI know so we don't navigate
return null;
} else {
notifification("success", message);
}
return botId;
notifification("success", message);
return { bot: data };
},
}),
deleteBot: build.mutation<DefaultBotsResponse, string[]>({
Expand Down
Loading
Loading