diff --git a/api/alembic/versions/c5d6e7f8a9b0_add_signal_ids_to_bots_and_grid_ladders.py b/api/alembic/versions/c5d6e7f8a9b0_add_signal_ids_to_bots_and_grid_ladders.py new file mode 100644 index 000000000..be490386c --- /dev/null +++ b/api/alembic/versions/c5d6e7f8a9b0_add_signal_ids_to_bots_and_grid_ladders.py @@ -0,0 +1,57 @@ +"""add signal ids to bots and grid ladders + +Revision ID: c5d6e7f8a9b0 +Revises: b1c2d3e4f5a6 +Create Date: 2026-08-15 00:00:00.000000 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "c5d6e7f8a9b0" +down_revision: str | Sequence[str] | None = "b1c2d3e4f5a6" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column("bot", sa.Column("signal_id", sa.Integer(), nullable=True)) + op.create_foreign_key( + "fk_bot_signal_id_signals", + "bot", + "signals", + ["signal_id"], + ["id"], + ondelete="SET NULL", + ) + op.create_index("ix_bot_signal_id", "bot", ["signal_id"], unique=False) + + op.add_column("grid_ladder", sa.Column("signal_id", sa.Integer(), nullable=True)) + op.create_foreign_key( + "fk_grid_ladder_signal_id_signals", + "grid_ladder", + "signals", + ["signal_id"], + ["id"], + ondelete="SET NULL", + ) + op.create_index( + "ix_grid_ladder_signal_id", "grid_ladder", ["signal_id"], unique=False + ) + + +def downgrade() -> None: + op.drop_index("ix_grid_ladder_signal_id", table_name="grid_ladder") + op.drop_constraint( + "fk_grid_ladder_signal_id_signals", + "grid_ladder", + type_="foreignkey", + ) + op.drop_column("grid_ladder", "signal_id") + + op.drop_index("ix_bot_signal_id", table_name="bot") + op.drop_constraint("fk_bot_signal_id_signals", "bot", type_="foreignkey") + op.drop_column("bot", "signal_id") diff --git a/api/databases/crud/grid_ladder_crud.py b/api/databases/crud/grid_ladder_crud.py index 2da17c4ee..cb279cf70 100644 --- a/api/databases/crud/grid_ladder_crud.py +++ b/api/databases/crud/grid_ladder_crud.py @@ -55,6 +55,7 @@ def __init__(self, session: Session): def create( self, *, + signal_id: int | None = None, symbol: str, fiat: str, exchange: ExchangeId | str, @@ -71,6 +72,7 @@ def create( context: dict, ) -> GridLadderTable: ladder = GridLadderTable( + signal_id=signal_id, symbol=symbol, fiat=fiat, exchange=exchange, diff --git a/api/databases/tables/bot_table.py b/api/databases/tables/bot_table.py index 83bb79a80..d1bb828fb 100644 --- a/api/databases/tables/bot_table.py +++ b/api/databases/tables/bot_table.py @@ -30,6 +30,9 @@ class BotTable(SQLModel, table=True): id: Optional[UUID] = Field( default_factory=uuid4, primary_key=True, index=True, nullable=False, unique=True ) + signal_id: int | None = Field( + default=None, foreign_key="signals.id", ondelete="SET NULL", index=True + ) pair: str = Field(index=True) fiat: str = Field(default="USDC", index=True) quote_asset: QuoteAssets = Field(default=QuoteAssets.USDC) diff --git a/api/databases/tables/grid_ladder_table.py b/api/databases/tables/grid_ladder_table.py index 16f353c55..a1c2914ab 100644 --- a/api/databases/tables/grid_ladder_table.py +++ b/api/databases/tables/grid_ladder_table.py @@ -31,6 +31,9 @@ class GridLadderTable(SQLModel, table=True): ) id: UUID = Field(default_factory=uuid4, primary_key=True, index=True, unique=True) + signal_id: int | None = Field( + default=None, foreign_key="signals.id", ondelete="SET NULL", index=True + ) symbol: str = Field(index=True) fiat: str = Field(default="USDC", index=True) exchange: ExchangeId = Field( diff --git a/api/grid_ladders/models.py b/api/grid_ladders/models.py index 297f3717a..b23855a37 100644 --- a/api/grid_ladders/models.py +++ b/api/grid_ladders/models.py @@ -5,6 +5,7 @@ class GridLadderCreate(BaseModel): + signal_id: int | None = None symbol: str = Field(min_length=1) fiat: str = Field(default="USDC", min_length=1) exchange: ExchangeId = Field(default=ExchangeId.KUCOIN) diff --git a/api/grid_ladders/routes.py b/api/grid_ladders/routes.py index b52ca59f7..04bc70d03 100644 --- a/api/grid_ladders/routes.py +++ b/api/grid_ladders/routes.py @@ -306,6 +306,7 @@ def post_grid_ladder( reserved_margin = sum(level.margin_required for level in calculated.levels) ladder = grid_ladder_crud.create( + signal_id=payload.signal_id, symbol=payload.symbol, fiat=payload.fiat, exchange=payload.exchange, diff --git a/api/tests/test_bots.py b/api/tests/test_bots.py index f76f9315e..9bbf233eb 100644 --- a/api/tests/test_bots.py +++ b/api/tests/test_bots.py @@ -1,3 +1,4 @@ +from datetime import UTC, datetime from unittest.mock import patch from fastapi.testclient import TestClient from pytest import fixture, raises @@ -16,6 +17,7 @@ from api.databases.tables.deal_table import DealTable from api.databases.tables.grid_ladder_table import GridLadderTable from api.databases.tables.recovery_bot_table import RecoveryBotTable +from api.databases.tables.signals_table import SignalsTable from uuid import UUID, uuid4 @@ -191,8 +193,23 @@ def test_get_algo_ranking_ordered_by_count(client: TestClient): assert profits == sorted(profits, reverse=True) -def test_create_bot(client: TestClient): - response = client.post("/bot", json=mock_bot_data_superusdt) +def test_create_bot_persists_signal_id(client: TestClient, create_test_tables): + signal_id = 9001 + with Session(create_test_tables) as session: + session.add( + SignalsTable( + id=signal_id, + algorithm_name="test_bot", + symbol=mock_bot_data_superusdt["pair"], + generated_at=datetime.now(UTC), + direction="long", + ) + ) + session.commit() + + response = client.post( + "/bot", json={**mock_bot_data_superusdt, "signal_id": signal_id} + ) assert response.status_code == 200 content = response.json() @@ -202,6 +219,7 @@ def test_create_bot(client: TestClient): assert ( content["data"]["fiat_order_size"] == mock_bot_data_superusdt["fiat_order_size"] ) + assert content["data"]["signal_id"] == signal_id def test_create_bot_without_recovery_params_creates_no_recovery_row( diff --git a/api/tests/test_grid_ladders.py b/api/tests/test_grid_ladders.py index cfc466e32..8eb206f28 100644 --- a/api/tests/test_grid_ladders.py +++ b/api/tests/test_grid_ladders.py @@ -625,15 +625,34 @@ def test_calculate_grid_ladder_disables_unaffordable_levels(client, monkeypatch) assert all(level["side"] == "neutral" for level in detail["levels"]) -def test_post_grid_ladder_persists_ladder_and_levels(client, monkeypatch): +def test_post_grid_ladder_persists_ladder_signal_and_levels( + client, monkeypatch, create_test_tables +): _patch_balance(monkeypatch, 10_000) _patch_contract_meta(monkeypatch) + signal_id = 9002 + with Session(create_test_tables) as session: + session.add( + SignalsTable( + id=signal_id, + algorithm_name="fixed_grid", + symbol="ADAUSDC", + generated_at=datetime.now(UTC), + direction="grid", + signal_kind="grid_deploy", + ) + ) + session.commit() - response = client.post("/grid-ladders", json=_payload()) + payload = _payload() + payload["signal_id"] = signal_id + + response = client.post("/grid-ladders", json=payload) assert response.status_code == 200 body = response.json() ladder = body["detail"] + assert ladder["signal_id"] == signal_id assert ladder["symbol"] == "ADAUSDC" assert ladder["status"] == "pending" assert ladder["grid_step"] == 5 diff --git a/pyproject.toml b/pyproject.toml index f5558bf8b..e4aa163d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "sqlalchemy>=2.0.30", "alembic>=1.18.4", "alembic-postgresql-enum", - "pybinbot>=1.11.13", + "pybinbot>=1.11.15", ] [project.urls] diff --git a/uv.lock b/uv.lock index d94f3a3f5..a9f2cf291 100644 --- a/uv.lock +++ b/uv.lock @@ -366,7 +366,7 @@ requires-dist = [ { name = "psycopg2-binary", specifier = ">=2.9.12" }, { name = "py3cw", specifier = ">=0.0.39" }, { name = "py4j", specifier = ">=0.10.9" }, - { name = "pybinbot", specifier = ">=1.11.13" }, + { name = "pybinbot", specifier = ">=1.11.15" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.2.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.3.0" }, @@ -2114,7 +2114,7 @@ wheels = [ [[package]] name = "pybinbot" -version = "1.11.13" +version = "1.11.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2128,9 +2128,9 @@ dependencies = [ { name = "requests" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/4c/13a48e7557a94edaad5df3e94c2d378321cf02cf2d03025a007f0ad69b05/pybinbot-1.11.13.tar.gz", hash = "sha256:bf451a1d8e5e06fd8b145b1a5b3633a61905aef936bc2535cc1f29cc9e8c0ac4", size = 84347, upload-time = "2026-08-15T18:08:24.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/49/377a33006d864a5f7e8e9c25a0f75297ca4f46ec70ac7f836471cb07110e/pybinbot-1.11.15.tar.gz", hash = "sha256:16574e78c9571022291abd05af597d63743f94195da9da7242df2030cac03a6d", size = 84420, upload-time = "2026-08-15T21:16:04.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/8a/98f92aff5a2059c7b2000f012f9248126e3e5ed07a207e2986e145754460/pybinbot-1.11.13-py3-none-any.whl", hash = "sha256:ac92109032bef911dc65524c0dbfbebdc8b3ad39c2d2c6104b671e5776b81154", size = 76979, upload-time = "2026-08-15T18:08:23.117Z" }, + { url = "https://files.pythonhosted.org/packages/6b/96/42971645d9043629f22452aae85741d7e44e1c42bbe81022ec7f853afdf5/pybinbot-1.11.15-py3-none-any.whl", hash = "sha256:15e157d0fffc223fc29ae644505ae59b0a476f744d6dab7ab114af8ba7e27927", size = 77004, upload-time = "2026-08-15T21:16:03.2Z" }, ] [[package]]