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
4 changes: 0 additions & 4 deletions .github/workflows/staging-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,17 @@ env:
jobs:
full-stack-deploy:
runs-on: ubuntu-latest
continue-on-error: true
timeout-minutes: 20

steps:
- name: Connect to Tailscale
continue-on-error: true
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TAILSCALE_CLIENT_ID }}
oauth-secret: ${{ secrets.TAILSCALE_CLIENT_SECRET }}
tags: tag:ci

- name: Install SSH key
continue-on-error: true
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
Expand All @@ -34,7 +31,6 @@ jobs:
ssh-keyscan -p 2222 100.64.83.67 >> ~/.ssh/known_hosts

- name: Deploy stack
continue-on-error: true
run: |
ssh -i ~/.ssh/home_server_key \
-p 2222 \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Add generated_at index for signal listing

Revision ID: f1a2b3c4d5e6
Revises: e9f0a1b2c3d4
Create Date: 2026-05-10 00:00:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "f1a2b3c4d5e6"
down_revision: Union[str, Sequence[str], None] = "e9f0a1b2c3d4"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
op.create_index(
"ix_signals_generated_at",
"signals",
[sa.text("generated_at DESC")],
unique=False,
)


def downgrade() -> None:
"""Downgrade schema."""
op.drop_index("ix_signals_generated_at", table_name="signals")
43 changes: 0 additions & 43 deletions api/charts/routes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session
from user.models.user import UserTokenData
from user.services.auth import get_current_user
from databases.utils import get_session
from tools.handle_error import (
json_response,
Expand All @@ -13,47 +11,6 @@
charts_blueprint = APIRouter()


@charts_blueprint.get("/top-gainers", tags=["charts"])
def top_gainers(session: Session = Depends(get_session)):
try:
gainers, losers = MarketDominationController().gainers_losers()
if gainers:
return json_response(
{
"data": gainers,
"message": "Successfully retrieved top gainers data.",
"error": 0,
}
)
else:
raise HTTPException(404, detail="No data found")

except Exception as error:
return json_response_error(f"Failed to retrieve top gainers data: {error}")


@charts_blueprint.get("/top-losers", tags=["charts"])
def top_losers(
session: Session = Depends(get_session),
_: UserTokenData = Depends(get_current_user),
):
try:
gainers, losers = MarketDominationController().gainers_losers()
if losers:
return json_response(
{
"data": losers,
"message": "Successfully retrieved top losers data.",
"error": 0,
}
)
else:
raise HTTPException(404, detail="No data found")

except Exception as error:
return json_response_error(f"Failed to retrieve top gainers data: {error}")


@charts_blueprint.get(
"/adr-series",
tags=["charts"],
Expand Down
94 changes: 82 additions & 12 deletions api/databases/crud/signals_crud.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime
from typing import Any, Sequence
from typing import Any, Sequence, cast

from sqlalchemy.orm import load_only
from sqlmodel import Session, col, select

from databases.tables.signals_table import SignalsTable
Expand Down Expand Up @@ -58,6 +59,85 @@ def query(
limit: int = 100,
offset: int = 0,
) -> Sequence[SignalsTable]:
stmt = self._filtered_query(
algorithm_name=algorithm_name,
symbol=symbol,
current_regime=current_regime,
autotrade=autotrade,
since=since,
until=until,
)
stmt = (
stmt.order_by(col(SignalsTable.generated_at).desc())
.offset(offset)
.limit(limit)
)
with get_db_session(self._external_session) as session:
rows = session.exec(stmt).all()
if self._external_session is None:
for row in rows:
session.expunge(row)
return rows

def query_summary(
self,
algorithm_name: str | None = None,
symbol: str | None = None,
current_regime: str | None = None,
autotrade: bool | None = None,
since: datetime | None = None,
until: datetime | None = None,
limit: int = 100,
offset: int = 0,
) -> list[dict[str, Any]]:
stmt = self._filtered_query(
algorithm_name=algorithm_name,
symbol=symbol,
current_regime=current_regime,
autotrade=autotrade,
since=since,
until=until,
)
stmt = (
stmt.options(
load_only(
cast(Any, SignalsTable.id),
cast(Any, SignalsTable.algorithm_name),
cast(Any, SignalsTable.symbol),
cast(Any, SignalsTable.generated_at),
cast(Any, SignalsTable.direction),
cast(Any, SignalsTable.autotrade),
cast(Any, SignalsTable.current_regime),
)
)
.order_by(col(SignalsTable.generated_at).desc())
.offset(offset)
.limit(limit)
)
with get_db_session(self._external_session) as session:
rows = session.exec(stmt).all()
return [
{
"id": row.id,
"algorithm_name": row.algorithm_name,
"symbol": row.symbol,
"generated_at": row.generated_at,
"direction": row.direction,
"autotrade": row.autotrade,
"current_regime": row.current_regime,
}
for row in rows
]

def _filtered_query(
self,
algorithm_name: str | None = None,
symbol: str | None = None,
current_regime: str | None = None,
autotrade: bool | None = None,
since: datetime | None = None,
until: datetime | None = None,
) -> Any:
stmt = select(SignalsTable)
if algorithm_name is not None:
stmt = stmt.where(SignalsTable.algorithm_name == algorithm_name)
Expand All @@ -71,14 +151,4 @@ def query(
stmt = stmt.where(SignalsTable.generated_at >= since)
if until is not None:
stmt = stmt.where(SignalsTable.generated_at <= until)
stmt = (
stmt.order_by(col(SignalsTable.generated_at).desc())
.offset(offset)
.limit(limit)
)
with get_db_session(self._external_session) as session:
rows = session.exec(stmt).all()
if self._external_session is None:
for row in rows:
session.expunge(row)
return rows
return stmt
2 changes: 1 addition & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ dependencies = [
"alembic>=1.18.4",
"alembic-postgresql-enum",
"pydantic-settings>=2.10.1",
"pybinbot>=1.8.4",
"pybinbot>=1.8.8",
]

[project.urls]
Expand Down
15 changes: 14 additions & 1 deletion api/signals/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,22 @@ class SignalRecord(SignalCreate):
id: int


class SignalListRecord(BaseModel):
id: int
algorithm_name: str = Field(..., max_length=128)
symbol: str = Field(..., max_length=64)
generated_at: datetime
direction: str = Field(..., max_length=16)
autotrade: bool = False
current_regime: str | None = Field(default=None, max_length=32)
context: dict[str, Any] | None = None
bot_params: dict[str, Any] | None = None
indicators: dict[str, Any] | None = None


class SignalResponse(StandardResponse):
data: SignalRecord


class SignalListResponse(StandardResponse):
data: list[SignalRecord]
data: list[SignalListRecord]
33 changes: 23 additions & 10 deletions api/signals/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,33 @@ def list_signals(
until: datetime | None = Query(default=None),
limit: int = Query(default=100, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
include_payload: bool = Query(default=True),
session: Session = Depends(get_session),
_: UserTokenData = Depends(get_current_user),
):
crud = SignalsCrud(session)
rows = crud.query(
algorithm_name=algorithm_name,
symbol=symbol,
current_regime=current_regime,
autotrade=autotrade,
since=since,
until=until,
limit=limit,
offset=offset,
)
if include_payload:
rows = crud.query(
algorithm_name=algorithm_name,
symbol=symbol,
current_regime=current_regime,
autotrade=autotrade,
since=since,
until=until,
limit=limit,
offset=offset,
)
else:
rows = crud.query_summary(
algorithm_name=algorithm_name,
symbol=symbol,
current_regime=current_regime,
autotrade=autotrade,
since=since,
until=until,
limit=limit,
offset=offset,
)
return {"message": "Signals retrieved", "data": rows, "error": 0}


Expand Down
153 changes: 0 additions & 153 deletions api/tests/cassettes/test_top_gainers.yaml

This file was deleted.

153 changes: 0 additions & 153 deletions api/tests/cassettes/test_top_losers.yaml

This file was deleted.

21 changes: 0 additions & 21 deletions api/tests/test_charts.py

This file was deleted.

50 changes: 50 additions & 0 deletions api/tests/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,33 @@ def test_query_orders_newest_first():
assert rows[0].generated_at > rows[1].generated_at > rows[2].generated_at


def test_query_can_skip_payload_columns():
session = _make_session()
crud = SignalsCrud(session)
crud.create(
algorithm_name="apex_flow",
symbol="BTCUSDC",
generated_at=datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc),
direction="LONG",
context={"large": "payload"},
bot_params={"pair": "BTCUSDC"},
indicators={"score": 0.83},
)

rows = crud.query_summary()

assert len(rows) == 1
assert rows[0]["algorithm_name"] == "apex_flow"
assert rows[0]["symbol"] == "BTCUSDC"
assert rows[0]["generated_at"] == datetime(2026, 4, 30, 12, 0)
assert rows[0]["direction"] == "LONG"
assert rows[0]["autotrade"] is False
assert rows[0]["current_regime"] is None
assert "context" not in rows[0]
assert "bot_params" not in rows[0]
assert "indicators" not in rows[0]


def test_post_signal_endpoint(client):
response = client.post(
"/signals",
Expand Down Expand Up @@ -151,3 +178,26 @@ def test_get_signals_endpoint_filters(client):
payload = response.json()["data"]
assert len(payload) == 1
assert payload[0]["algorithm_name"] == "spike_hunter_v3"


def test_get_signals_endpoint_can_skip_payload(client):
session = _make_session()
crud = SignalsCrud(session)
crud.create(
algorithm_name="spike_hunter_v3",
symbol="BTCUSDC",
generated_at=datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc),
direction="LONG",
context={"large": "payload"},
bot_params={"pair": "BTCUSDC"},
indicators={"score": 0.83},
)

response = client.get("/signals?include_payload=false")

assert response.status_code == 200
signal = response.json()["data"][0]
assert signal["algorithm_name"] == "spike_hunter_v3"
assert signal["context"] is None
assert signal["bot_params"] is None
assert signal["indicators"] is None
Loading
Loading