Skip to content

Commit f260ea7

Browse files
authored
#64 Add alerts using a scheduler (#65)
1 parent 4aa6d0b commit f260ea7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1458
-864
lines changed

.envs/.local/.producer

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
DEBUG=True
22

3-
REDIS_EXPIRATION=90
4-
53
BINANCE_COINS=LTOUSDT LTOBTC ETHUSDT ETHBTC BTCUSDT LINKUSDT LINKBTC FTMUSDT FTMBTC PEPEUSDT
64
KUCOIN_COINS=LTO-USDT LTO-BTC ETH-USDT ETH-BTC BTC-USDT LINK-USDT FTM-USDT PEPE-USDT
75
COINBASE_COINS=BTC-USD ETH-USD LINK-USD

.envs/.local/.redis

+3
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
REDIS_OM_URL=redis://@redis:6379/0
2+
3+
REDIS_MAX_ROWS=64
4+
REDIS_EXPIRATION=90

.envs/.local/.scheduler

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
DEBUG=True
2+
3+
BINANCE=LTO,ETH,BTC,LINK,FTM,PEPE
4+
COINBASE=ETH,BTC,LINK
5+
KRAKEN=ETH,BTC,LINK,FTM,PEPE
6+
KUCOIN=LTO,ETH,BTC,LINK,FTM,PEPE
7+
OKX=ETH,BTC,LINK,FTM
8+
BYBIT=ETH,BTC,LINK
9+
BITSTAMP=BTC,ETH,LINK
10+
MEXC=BTC,ETH,LINK
11+
HTX=BTC,ETH,LINK

.envs/.local/.web

-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
DEBUG=True
22

3-
REDIS_MAX_ROWS=64
4-
REDIS_EXPIRATION=90
5-
63
BINANCE=LTO,ETH,BTC,LINK,FTM,PEPE
74
COINBASE=ETH,BTC,LINK
85
KRAKEN=ETH,BTC,LINK,FTM,PEPE

benchmarks/endpoints.sh

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# frontend
1+
# web / frontend
22
ab -n 10000 "http://127.0.0.1:9000/BTC"
33

44
ab -n 10000 -c 50 "http://127.0.0.1:9000/BTC"
55

66
ab -n 10000 -c 50 -k "http://127.0.0.1:9000/BTC"
77

8-
# backend
8+
# web / backend
99
ab -n 10000 -c 50 -p $1 -T application/json http://127.0.0.1:9000/api/feed/BTC
1010

1111
ab -n 10000 -c 50 -p $1 -T application/json http://127.0.0.1:9000/api/feed/BTC/whales

compose/scheduler/local/Dockerfile

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
ARG PYTHON_VERSION=3.12.3-slim-bullseye
2+
3+
FROM python:${PYTHON_VERSION} AS python
4+
5+
ENV PYTHONUNBUFFERED=1 \
6+
PYTHONDONTWRITEBYTECODE=1 \
7+
PIP_NO_CACHE_DIR=off \
8+
PIP_DISABLE_PIP_VERSION_CHECK=on \
9+
PIP_DEFAULT_TIMEOUT=100 \
10+
POETRY_HOME="/opt/poetry" \
11+
POETRY_VIRTUALENVS_IN_PROJECT=true \
12+
POETRY_NO_INTERACTION=1 \
13+
PYSETUP_PATH="/app" \
14+
VENV_PATH="/app/.venv"
15+
16+
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
17+
18+
19+
FROM python AS python-build-stage
20+
21+
RUN apt-get update && apt-get install --no-install-recommends -y \
22+
curl \
23+
build-essential
24+
25+
ENV POETRY_VERSION=1.7.1
26+
RUN curl -sSL https://install.python-poetry.org | python
27+
28+
WORKDIR $PYSETUP_PATH
29+
30+
COPY ./poetry.lock ./pyproject.toml ./
31+
RUN poetry install --only main --no-root
32+
33+
34+
FROM python AS python-run-stage
35+
36+
COPY --from=python-build-stage $POETRY_HOME $POETRY_HOME
37+
COPY --from=python-build-stage $PYSETUP_PATH $PYSETUP_PATH
38+
39+
COPY ./compose/scheduler/local/start /start
40+
RUN sed -i 's/\r$//g' /start
41+
RUN chmod +x /start
42+
43+
WORKDIR $PYSETUP_PATH
44+
45+
COPY . .
46+
47+
RUN poetry install

compose/scheduler/local/start

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
3+
set -o errexit
4+
set -o pipefail
5+
set -o nounset
6+
7+
exec poetry run scheduler
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
ARG PYTHON_VERSION=3.12.3-slim-bullseye
2+
3+
FROM python:${PYTHON_VERSION} AS python
4+
5+
ENV PYTHONUNBUFFERED=1 \
6+
PYTHONDONTWRITEBYTECODE=1 \
7+
PIP_NO_CACHE_DIR=off \
8+
PIP_DISABLE_PIP_VERSION_CHECK=on \
9+
PIP_DEFAULT_TIMEOUT=100 \
10+
POETRY_HOME="/opt/poetry" \
11+
POETRY_VIRTUALENVS_IN_PROJECT=true \
12+
POETRY_NO_INTERACTION=1 \
13+
PYSETUP_PATH="/app" \
14+
VENV_PATH="/app/.venv"
15+
16+
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
17+
18+
19+
FROM python AS python-build-stage
20+
21+
RUN apt-get update && apt-get install --no-install-recommends -y \
22+
curl \
23+
build-essential
24+
25+
ENV POETRY_VERSION=1.7.1
26+
RUN curl -sSL https://install.python-poetry.org | python
27+
28+
WORKDIR $PYSETUP_PATH
29+
30+
COPY ./poetry.lock ./pyproject.toml ./
31+
RUN poetry install --only main --no-root
32+
33+
34+
FROM python AS python-run-stage
35+
36+
COPY --from=python-build-stage $POETRY_HOME $POETRY_HOME
37+
COPY --from=python-build-stage $PYSETUP_PATH $PYSETUP_PATH
38+
39+
COPY ./compose/scheduler/production/start /start
40+
RUN sed -i 's/\r$//g' /start
41+
RUN chmod +x /start
42+
43+
WORKDIR $PYSETUP_PATH
44+
45+
COPY . .
46+
47+
RUN poetry install

compose/scheduler/production/start

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
3+
set -o errexit
4+
set -o pipefail
5+
set -o nounset
6+
7+
exec poetry run scheduler

exchange_radar/producer/models.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ def __init__(self, *args, **kwargs):
2424
# noinspection PyUnresolvedReferences
2525
pipe.hsetnx(self._name, f"{self.trade_symbol}_EXCHANGES", get_exchanges(coin=self.trade_symbol))
2626
# noinspection PyUnresolvedReferences
27-
pipe.set(f"LAST_TS_{self.exchange.upper()}", time.time())
27+
pipe.hset("PING", self.exchange.upper(), time.time())
28+
# noinspection PyUnresolvedReferences
29+
pipe.hset("PRICE", self.trade_symbol, float(self.price))
2830
pipe.execute()
2931

3032
def volume(self) -> float:

exchange_radar/producer/publisher.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
StreamLostError,
1010
)
1111

12-
from exchange_radar.producer.serializers.base import BaseSerializer
12+
from exchange_radar.producer.serializers.base import FeedSerializer
1313
from exchange_radar.producer.settings import base as settings
1414
from exchange_radar.producer.settings.routing_keys import ROUTING_KEYS
1515

@@ -70,7 +70,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
7070
}
7171

7272

73-
def publish(data: BaseSerializer) -> None:
73+
def publish(data: FeedSerializer) -> None:
7474
logger.info(f"PRODUCER - start: {data.trade_time} {data.symbol}")
7575

7676
body = data.model_dump_json().encode()

exchange_radar/producer/serializers/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from exchange_radar.producer.utils import get_ranking
1111

1212

13-
class BaseSerializer(RedisMixin, BaseModel):
13+
class FeedSerializer(RedisMixin, BaseModel):
1414
@field_validator("trade_time", check_fields=False) # noqa
1515
@classmethod
1616
def trade_time_normalization(cls, v) -> str:

exchange_radar/producer/serializers/binance.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
from pydantic import Field, computed_field
66

7-
from exchange_radar.producer.serializers.base import BaseSerializer
7+
from exchange_radar.producer.serializers.base import FeedSerializer
88

99

10-
class BinanceTradeSerializer(BaseSerializer):
10+
class BinanceTradeSerializer(FeedSerializer):
1111
symbol: str = Field(alias="s")
1212
price: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="p")]
1313
quantity: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="q")]

exchange_radar/producer/serializers/bitstamp.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from pydantic import Field, computed_field, field_validator
77

8-
from exchange_radar.producer.serializers.base import BaseSerializer
8+
from exchange_radar.producer.serializers.base import FeedSerializer
99

1010

11-
class BitstampTradeSerializer(BaseSerializer):
11+
class BitstampTradeSerializer(FeedSerializer):
1212
symbol: str = Field(alias="channel")
1313
price: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="price_str")]
1414
quantity: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="amount_str")]

exchange_radar/producer/serializers/bybit.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from pydantic import Field, computed_field
77

8-
from exchange_radar.producer.serializers.base import BaseSerializer
8+
from exchange_radar.producer.serializers.base import FeedSerializer
99

1010

11-
class BybitTradeSerializer(BaseSerializer):
11+
class BybitTradeSerializer(FeedSerializer):
1212
symbol: str = Field(alias="s")
1313
price: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="p")]
1414
quantity: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="v")]

exchange_radar/producer/serializers/coinbase.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from pydantic import Field, computed_field, field_validator
77

8-
from exchange_radar.producer.serializers.base import BaseSerializer
8+
from exchange_radar.producer.serializers.base import FeedSerializer
99

1010

11-
class CoinbaseTradeSerializer(BaseSerializer):
11+
class CoinbaseTradeSerializer(FeedSerializer):
1212
symbol: str = Field(alias="product_id")
1313
price: Annotated[Decimal, Field(ge=0, decimal_places=8)]
1414
quantity: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="size")]

exchange_radar/producer/serializers/htx.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from pydantic import Field, computed_field, field_validator
77

8-
from exchange_radar.producer.serializers.base import BaseSerializer
8+
from exchange_radar.producer.serializers.base import FeedSerializer
99

1010

11-
class HtxTradeSerializer(BaseSerializer):
11+
class HtxTradeSerializer(FeedSerializer):
1212
symbol: str = Field(alias="channel")
1313
price: Annotated[Decimal, Field(ge=0, decimal_places=8)]
1414
quantity: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="amount")]

exchange_radar/producer/serializers/kraken.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from pydantic import Field, computed_field, field_validator
77

8-
from exchange_radar.producer.serializers.base import BaseSerializer
8+
from exchange_radar.producer.serializers.base import FeedSerializer
99

1010

11-
class KrakenTradeSerializer(BaseSerializer):
11+
class KrakenTradeSerializer(FeedSerializer):
1212
symbol: str
1313
price: Annotated[Decimal, Field(ge=0, decimal_places=12)]
1414
quantity: Annotated[Decimal, Field(ge=0, decimal_places=9)]

exchange_radar/producer/serializers/kucoin.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from pydantic import Field, computed_field, field_validator
77

8-
from exchange_radar.producer.serializers.base import BaseSerializer
8+
from exchange_radar.producer.serializers.base import FeedSerializer
99

1010

11-
class KucoinTradeSerializer(BaseSerializer):
11+
class KucoinTradeSerializer(FeedSerializer):
1212
symbol: str
1313
price: Annotated[Decimal, Field(ge=0, decimal_places=10)]
1414
quantity: Annotated[Decimal, Field(ge=0, decimal_places=9, alias="size")]

exchange_radar/producer/serializers/mexc.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from pydantic import Field, computed_field
77

8-
from exchange_radar.producer.serializers.base import BaseSerializer
8+
from exchange_radar.producer.serializers.base import FeedSerializer
99

1010

11-
class MexcTradeSerializer(BaseSerializer):
11+
class MexcTradeSerializer(FeedSerializer):
1212
symbol: str = Field(alias="s")
1313
price: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="p")]
1414
quantity: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="v")]

exchange_radar/producer/serializers/okx.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from pydantic import Field, computed_field, field_validator
77

8-
from exchange_radar.producer.serializers.base import BaseSerializer
8+
from exchange_radar.producer.serializers.base import FeedSerializer
99

1010

11-
class OkxTradeSerializer(BaseSerializer):
11+
class OkxTradeSerializer(FeedSerializer):
1212
symbol: str = Field(alias="instId")
1313
price: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="px")]
1414
quantity: Annotated[Decimal, Field(ge=0, decimal_places=8, alias="sz")]

exchange_radar/producer/tasks/bybit.py

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def callback(message):
3333

3434
while True:
3535
await asyncio.sleep(self.ITER_SLEEP)
36+
3637
except Exception as error2:
3738
logger.error(f"EXIT ERROR: {error2}")
3839
sys.exit(1)

exchange_radar/scheduler/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import logging
2+
3+
from exchange_radar.scheduler.settings import base as settings
4+
5+
logging.basicConfig(
6+
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
7+
level=logging.INFO if settings.DEBUG else logging.WARNING,
8+
)

exchange_radar/scheduler/alerts/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)