Skip to content

Commit

Permalink
#44 Add MEXC exchange (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
pantunes authored Jan 30, 2024
1 parent 875ca4b commit fc7d30f
Show file tree
Hide file tree
Showing 23 changed files with 321 additions and 101 deletions.
1 change: 1 addition & 0 deletions .envs/.local/.producer
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ KRAKEN_COINS=BTC/USD ETH/USD ETH/BTC LINK/USD FTM/USD PEPE/USD
OKX_COINS=ETH-USDT ETH-BTC BTC-USDT LINK-USDT FTM-USDT
BYBIT_COINS=BTCUSDT ETHUSDT LINKUSDT
BITSTAMP_COINS=BTCUSD ETHUSD LINKUSD
MEXC_COINS=BTCUSDT ETHUSDT LINKUSDT
1 change: 1 addition & 0 deletions .envs/.local/.web
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ KUCOIN=LTO,ETH,BTC,LINK,FTM,PEPE
OKX=ETH,BTC,LINK,FTM
BYBIT=ETH,BTC,LINK
BITSTAMP=BTC,ETH,LINK
MEXC=BTC,ETH,LINK
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
Welcome to Exchange Radar, your gateway to real-time trade data from major cryptocurrency exchanges.

### Supported Exchanges
Exchange Radar currently supports the following top exchanges by trading volume:
Exchange Radar currently supports the following top exchanges by reputation and trading volume:
- Binance
- Coinbase
- Kraken
- KuCoin
- OKX
- Bybit
- Bitstamp
- MEXC

### Build & Run
Get started effortlessly:
Expand Down
53 changes: 53 additions & 0 deletions compose/producer/mexc/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
ARG PYTHON_VERSION=3.11.3-slim-bullseye

FROM python:${PYTHON_VERSION} as python

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_HOME="/opt/poetry" \
POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1 \
PYSETUP_PATH="/app" \
VENV_PATH="/app/.venv"

ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"


FROM python as python-build-stage

RUN apt-get update && apt-get install --no-install-recommends -y \
curl \
build-essential

ENV POETRY_VERSION=1.7.1
RUN curl -sSL https://install.python-poetry.org | python

WORKDIR $PYSETUP_PATH

COPY ./poetry.lock ./pyproject.toml ./
RUN poetry install --only main --no-root


FROM python as python-run-stage

COPY --from=python-build-stage $POETRY_HOME $POETRY_HOME
COPY --from=python-build-stage $PYSETUP_PATH $PYSETUP_PATH

COPY ./compose/producer/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint

COPY ./compose/producer/mexc/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start

WORKDIR $PYSETUP_PATH

COPY . .

RUN poetry install

ENTRYPOINT ["/entrypoint"]
7 changes: 7 additions & 0 deletions compose/producer/mexc/start
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

set -o errexit
set -o pipefail
set -o nounset

exec poetry run producer ${MEXC_COINS} --exchange mexc
23 changes: 23 additions & 0 deletions exchange_radar/producer/serializers/mexc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from datetime import datetime
from functools import cached_property

from pydantic import Field, computed_field, condecimal

from exchange_radar.producer.serializers.base import BaseSerializer


class MexcTradeSerializer(BaseSerializer):
symbol: str = Field(alias="s")
price: condecimal(ge=0, decimal_places=8) = Field(alias="p")
quantity: condecimal(ge=0, decimal_places=8) = Field(alias="v")
trade_time: datetime = Field(alias="t")
side: int = Field(alias="S", exclude=True)

@computed_field
def exchange(self) -> str:
return "MEXC"

@computed_field
@cached_property
def is_seller(self) -> bool:
return True if self.side == 2 else False
2 changes: 2 additions & 0 deletions exchange_radar/producer/settings/exchanges.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"kraken": "KrakenTradesTask",
"okx": "OkxTradesTask",
"bitstamp": "BitstampTradesTask",
"mexc": "MexcTradesTask",
}

EXCHANGES_LIST = list(EXCHANGES.keys())
Expand All @@ -22,3 +23,4 @@
KRAKEN_COINS = env.list("KRAKEN_COINS", delimiter=" ")
OKX_COINS = env.list("OKX_COINS", delimiter=" ")
BITSTAMP_COINS = env.list("BITSTAMP_COINS", delimiter=" ")
MEXC_COINS = env.list("MEXC_COINS", delimiter=" ")
4 changes: 2 additions & 2 deletions exchange_radar/producer/tasks/bybit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ async def task(self, symbols: tuple[str]):
async def process(self, symbol_or_symbols: str | tuple):
def callback(message):
try:
for message in message["data"]:
data = BybitTradeSerializer(**message)
for msg in message["data"]:
data = BybitTradeSerializer(**msg)
publish(data)
except Exception as error1:
logger.error(f"ERROR(1): {error1}")
Expand Down
4 changes: 2 additions & 2 deletions exchange_radar/producer/tasks/coinbase.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import asyncio
import logging
import time

from copra.websocket import Channel, Client

Expand Down Expand Up @@ -36,7 +36,7 @@ def on_message(self, message):
except Exception as error:
logger.error(f"GENERAL ERROR: {error}")
logger.error(f"Trying again in {ITER_SLEEP} seconds...")
time.sleep(ITER_SLEEP)
asyncio.sleep(ITER_SLEEP)

def start(self, symbols: tuple[str, ...]):
logger.info("Starting Task...")
Expand Down
31 changes: 31 additions & 0 deletions exchange_radar/producer/tasks/mexc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import asyncio
import logging

from pymexc import spot

from exchange_radar.producer.publisher import publish
from exchange_radar.producer.serializers.mexc import MexcTradeSerializer
from exchange_radar.producer.tasks.base import Task

logger = logging.getLogger(__name__)


ITER_SLEEP = 10.0


class MexcTradesTask(Task):
async def process(self, symbol_or_symbols: str | tuple):
def callback(message):
try:
for msg in message["d"]["deals"]:
msg.update({"s": message["s"]})
data = MexcTradeSerializer(**msg)
publish(data)
except Exception as error:
logger.error(f"ERROR: {error}")

ws = spot.WebSocket()
ws.deals_stream(callback, symbol_or_symbols)

while True:
await asyncio.sleep(ITER_SLEEP)
4 changes: 2 additions & 2 deletions exchange_radar/producer/tasks/okx.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def callback(message):
self.num_events = 0

try:
for message in json.loads(message)["data"]:
data = OkxTradeSerializer(**message)
for msg in json.loads(message)["data"]:
data = OkxTradeSerializer(**msg)
publish(data)
except Exception as error1:
logger.error(f"ERROR(1): {error1}")
Expand Down
39 changes: 39 additions & 0 deletions exchange_radar/producer/tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from exchange_radar.producer.serializers.coinbase import CoinbaseTradeSerializer
from exchange_radar.producer.serializers.kraken import KrakenTradeSerializer
from exchange_radar.producer.serializers.kucoin import KucoinTradeSerializer
from exchange_radar.producer.serializers.mexc import MexcTradeSerializer
from exchange_radar.producer.serializers.okx import OkxTradeSerializer


Expand Down Expand Up @@ -310,3 +311,41 @@ def test_serializer_bitstamp(mock_redis):
"exchange": "Bitstamp",
"is_seller": False,
}


@patch("exchange_radar.producer.models.redis")
def test_serializer_mexc(mock_redis):
mock_redis.hincrbyfloat.return_value = 3.7335
mock_redis.pipeline().__enter__().execute = MagicMock(return_value=[1.0, 100.0])

msg = {
"c": "[email protected]@BTCUSDT",
"d": {
"deals": [{"p": "43469.99", "v": "0.002153", "S": 1, "t": 1706615234824}],
"e": "[email protected]",
},
"s": "BTCUSDT",
"t": 1706615234826,
}

_msg = msg["d"]["deals"][0]
_msg.update({"s": msg["s"]})
payload = MexcTradeSerializer(**_msg)

assert payload.model_dump() == {
"symbol": "BTCUSDT",
"price": Decimal("43469.99"),
"quantity": Decimal("0.002153"),
"trade_time": datetime.datetime(2024, 1, 30, 11, 47, 14),
"total": Decimal("93.59088847"),
"currency": "USDT",
"trade_symbol": "BTC",
"volume": 3.7335,
"volume_trades": (1.0, 100.0),
"number_trades": (1, 100),
"trade_time_ts": 1706615234,
"message": "2024-01-30 11:47:14 | <span class='mexc'>MEXC </span> | 43469.99000000 USDT |"
" 0.00215300 BTC | 93.59088847 USDT",
"exchange": "MEXC",
"is_seller": False,
}
4 changes: 2 additions & 2 deletions exchange_radar/producer/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def test_get_ranking__error():
@pytest.mark.parametrize(
"coin, expected",
[
("BTC", "Binance, Coinbase, Kraken, Kucoin, OKX, Bybit, Bitstamp"),
("ETH", "Binance, Coinbase, Kraken, Kucoin, OKX, Bybit, Bitstamp"),
("BTC", "Binance, Coinbase, Kraken, Kucoin, OKX, Bybit, Bitstamp, MEXC"),
("ETH", "Binance, Coinbase, Kraken, Kucoin, OKX, Bybit, Bitstamp, MEXC"),
("LTO", "Binance, Kucoin"),
],
)
Expand Down
2 changes: 2 additions & 0 deletions exchange_radar/producer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
COINBASE_COINS,
KRAKEN_COINS,
KUCOIN_COINS,
MEXC_COINS,
OKX_COINS,
)

Expand Down Expand Up @@ -53,6 +54,7 @@ def get_exchanges(coin: str) -> str:
(OKX_COINS, "OKX"),
(BYBIT_COINS, "Bybit"),
(BITSTAMP_COINS, "Bitstamp"),
(MEXC_COINS, "MEXC"),
]
coin_length = len(coin)
exchanges_selected = []
Expand Down
3 changes: 2 additions & 1 deletion exchange_radar/web/src/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
KUCOIN = env.list("KUCOIN")
OKX = env.list("OKX")
BITSTAMP = env.list("BITSTAMP")
MEXC = env.list("MEXC")

COINS = list(set(BINANCE + COINBASE + KRAKEN + KUCOIN + OKX + BITSTAMP))
COINS = list(set(BINANCE + COINBASE + KRAKEN + KUCOIN + OKX + BITSTAMP + MEXC))
4 changes: 2 additions & 2 deletions exchange_radar/web/src/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
"<span class='binance'>Binance</span> <span class='coinbase'>Coinbase</span> "
"<span class='kraken'>Kraken</span> <span class='kucoin'>KuCoin</span> "
"<span class='okx'>OKX</span> <span class='bybit'>Bybit</span> "
"<span class='bitstamp'>Bitstamp</span>",
"<span class='bitstamp'>Bitstamp</span> <span class='mexc'>MEXC</span>",
),
(
"ETH",
"<span class='binance'>Binance</span> <span class='coinbase'>Coinbase</span> "
"<span class='kraken'>Kraken</span> <span class='kucoin'>KuCoin</span> "
"<span class='okx'>OKX</span> <span class='bybit'>Bybit</span> "
"<span class='bitstamp'>Bitstamp</span>",
"<span class='bitstamp'>Bitstamp</span> <span class='mexc'>MEXC</span>",
),
("LTO", "<span class='binance'>Binance</span> <span class='kucoin'>KuCoin</span>"),
],
Expand Down
2 changes: 2 additions & 0 deletions exchange_radar/web/src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def get_exchanges(coin: str) -> str:
exchanges.append("<span class='bybit'>Bybit</span>")
if coin in settings.BITSTAMP:
exchanges.append("<span class='bitstamp'>Bitstamp</span>")
if coin in settings.MEXC:
exchanges.append("<span class='mexc'>MEXC</span>")

if len(exchanges) == 0:
raise ValueError(f"No exchanges found for the coin {coin}")
Expand Down
5 changes: 5 additions & 0 deletions exchange_radar/web/static/css/exchanges.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@
background: var(--bitstamp-background-color);
color: var(--bitstamp-text-color);
}

.mexc {
background: var(--mexc-background-color);
color: var(--mexc-text-color);
}
4 changes: 4 additions & 0 deletions exchange_radar/web/static/css/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
--bybit-text-color: #ffbf00;
--bitstamp-background-color: #07342a;
--bitstamp-text-color: #2ffb97;
--mexc-background-color: #2d70eb;
--mexc-text-color: white;
}

[data-theme="dark"] {
Expand All @@ -44,4 +46,6 @@
--bybit-text-color: #997300;
--bitstamp-background-color: #041c17;
--bitstamp-text-color: #367a59;
--mexc-background-color: #3c5582;
--mexc-text-color: #b8b8b8;
}
17 changes: 17 additions & 0 deletions local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,23 @@ services:
- ./exchange_radar/producer:/app/exchange_radar/producer
command: /start

producer-mexc:
build:
context: .
dockerfile: compose/producer/mexc/Dockerfile
image: exchange_radar_producer_mexc
container_name: exchange-radar-producer-mexc
env_file:
- ./.envs/.local/.rabbitmq
- ./.envs/.local/.producer
- ./.envs/.local/.redis
depends_on:
- rabbitmq
- redis
volumes:
- ./exchange_radar/producer:/app/exchange_radar/producer
command: /start

consumer:
build:
context: .
Expand Down
Loading

0 comments on commit fc7d30f

Please sign in to comment.