Skip to content

Commit 6bdfad7

Browse files
authored
Merge pull request #10 from globophobe/feature/0.1.3
Release v0.1.3
2 parents 4aa492b + 25a0554 commit 6bdfad7

31 files changed

+839
-410
lines changed

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ This is the basis of a pipeline for live data from cryptocurrency exchanges. It
44

55
# How?
66

7-
Sequences of trades that have equal symbol, timestamp, nanoseconds, and tick rule are aggregated. Aggregating trades in this way can increase information, as they are either orders of size or stop loss cascades.
7+
Sequences of trades that have equal symbol, timestamp, and tick rule are aggregated. Aggregating trades in this way can increase information, as they are either orders of size or stop loss cascades.
88

99
As well, the number of messages can be reduced by 30-50%
1010

11-
By filtering aggregated messages, for example only emitting a mesage when an aggregated trade is greater than `min_volume`, the number of messages can be reduced more.
11+
By filtering aggregated messages, for example only emitting a mesage when an aggregated trade is greater than or equal to a `significant_trade_filter`, the number of messages can be reduced more.
1212

1313
Example
1414
-------
@@ -37,7 +37,7 @@ As it was aggregated from 4 raw trades, the second trade has ticks 4.
3737
]
3838
```
3939

40-
An example filtered message, emitted because the second aggregated trade exceeds `min_volume >= 1000`
40+
An example filtered message, emitted because the second aggregated trade exceeds `significant_trade_filter >= 1000`
4141

4242
Information related to the first trade is aggregated with the second.
4343

@@ -64,7 +64,7 @@ Information related to the first trade is aggregated with the second.
6464

6565
For 1m, 5m, 15m candles, there is an optional parameter `window_seconds`.
6666

67-
For settings, see [demo.py](https://github.com/globophobe/cryptofeed-werks/blob/main/demo.py)
67+
For settings, see the [examples](https://github.com/globophobe/cryptofeed-werks/blob/main/examples/)
6868

6969
Supported exchanges
7070
-------------------
@@ -81,16 +81,14 @@ Supported exchanges
8181

8282
:white_check_mark: Coinbase Pro
8383

84-
:white_check_mark: Deribit
85-
8684
:white_check_mark: FTX
8785

8886
:white_check_mark: Upbit
8987

9088
Contributing
9189
------------
9290

93-
Install dependencies with `poetry install`. The demo is built with [invoke tasks](https://github.com/globophobe/cryptofeed-werks/blob/master/tasks.py). For example, `invoke build`
91+
Install dependencies with `poetry install`. The docker example is built with [invoke tasks](https://github.com/globophobe/cryptofeed-werks/blob/master/tasks.py). For example, `invoke build`
9492

9593
Future plans
9694
------------

cryptofeed_werks/exchange.py

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1-
from .binance import BinanceExchange
2-
from .bitfinex import BitfinexExchange
3-
from .bitflyer import BitflyerExchange
4-
from .bitmex import BitmexExchange
5-
from .bybit import BybitExchange
6-
from .coinbase import CoinbaseExchange
7-
from .deribit import DeribitExchange
8-
from .ftx import FTXExchange
9-
from .upbit import UpbitExchange
1+
from .binance import Binance
2+
from .bitfinex import Bitfinex
3+
from .bitflyer import Bitflyer
4+
from .bitmex import Bitmex
5+
from .bybit import Bybit
6+
from .coinbase import Coinbase
7+
from .ftx import FTX
8+
from .upbit import Upbit
109

1110
__all__ = [
12-
"BinanceExchange",
13-
"BitmexExchange",
14-
"BitfinexExchange",
15-
"BitflyerExchange",
16-
"BybitExchange",
17-
"CoinbaseExchange",
18-
"DeribitExchange",
19-
"FTXExchange",
20-
"UpbitExchange",
11+
"Binance",
12+
"Bitmex",
13+
"Bitfinex",
14+
"Bitflyer",
15+
"Bybit",
16+
"Coinbase",
17+
"FTX",
18+
"Upbit",
2119
]

cryptofeed_werks/exchanges/binance.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
1+
from datetime import datetime, timezone
12
from decimal import Decimal
3+
from typing import Tuple
24

5+
import pandas as pd
36
from cryptofeed.defines import TRADES
4-
from cryptofeed.exchanges import Binance
7+
from cryptofeed.exchanges import Binance as BaseBinance
58

6-
from ..exchange import Exchange
9+
from ..feed import Feed
710

811

9-
class BinanceExchange(Exchange, Binance):
12+
class Binance(Feed, BaseBinance):
1013
def __init__(self, *args, **kwargs):
11-
"""
12-
Cryptofeed uses aggregate trade streams.
13-
The raw trade stream is absolute trash and is frequently missing trades.
14-
"""
1514
super().__init__(*args, **kwargs)
1615
self.last_id = None
1716

18-
async def _trade(self, msg: dict, timestamp: float):
17+
def parse_datetime(self, value: int, unit: str = "ms") -> datetime:
18+
"""Parse datetime with pandas."""
19+
return pd.Timestamp(value, unit="ms").replace(tzinfo=timezone.utc)
20+
21+
async def _trade(self, msg: dict, timestamp: float) -> Tuple[str, dict, float]:
1922
"""
23+
Cryptofeed uses aggregate trade streams.
24+
The raw trade stream frequently misses trades, and is absolute trash.
25+
2026
{
2127
"e": "aggTrade", // Event type
2228
"E": 123456789, // Event time
@@ -42,17 +48,16 @@ async def _trade(self, msg: dict, timestamp: float):
4248
volume = price * notional
4349
ticks = msg["l"] - msg["f"] + 1
4450
assert ticks >= 1, "Ticks not greater than or equal to 1"
45-
ts = (self.timestamp_normalize(msg["E"]),)
46-
trade = {
47-
"exchange": self.id,
48-
"uid": int(msg["l"]), # Last trade ID
49-
"symbol": msg["s"], # Do not normalize
50-
"timestamp": ts,
51+
t = {
52+
"exchange": self.id.lower(),
53+
"uid": self.last_id,
54+
"symbol": msg["s"],
55+
"timestamp": self.parse_datetime(msg["T"]),
5156
"price": price,
5257
"volume": volume,
5358
"notional": notional,
5459
"tickRule": -1 if msg["m"] else 1,
5560
"ticks": ticks,
5661
"isSequential": is_sequential,
5762
}
58-
await self.callback(TRADES, trade, ts)
63+
await self.callback(TRADES, t, timestamp)
Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,54 @@
1+
from datetime import datetime, timezone
12
from decimal import Decimal
3+
from typing import Tuple
24

5+
import pandas as pd
36
from cryptofeed.defines import TRADES
4-
from cryptofeed.exchanges import Bitfinex
7+
from cryptofeed.exchanges import Bitfinex as BaseBitfinex
8+
from cryptofeed.exchanges.bitfinex import LOG
59

6-
from ..exchange import Exchange
10+
from ..feed import Feed
711

812

9-
class BitfinexExchange(Exchange, Bitfinex):
10-
def std_symbol_to_exchange_symbol(self, symbol: str) -> str:
11-
return "t" + symbol
13+
class Bitfinex(Feed, BaseBitfinex):
14+
def __init__(self, *args, **kwargs):
15+
super().__init__(*args, **kwargs)
16+
self.is_initialized = False
1217

13-
async def _trades(self, pair: str, msg: dict, timestamp: float):
18+
def parse_datetime(self, value: int, unit: str = "ms") -> datetime:
19+
"""Parse datetime with pandas."""
20+
return pd.Timestamp(value, unit=unit).replace(tzinfo=timezone.utc)
21+
22+
async def _trades(
23+
self, pair: str, msg: list, timestamp: float
24+
) -> Tuple[str, dict, float]:
1425
async def _trade_update(trade: list, timestamp: float):
1526
uid, ts, notional, price = trade
1627
price = Decimal(price)
17-
notional = abs(Decimal(notional))
18-
volume = price * notional
19-
ts = self.timestamp_normalize(ts)
20-
trade = {
21-
"exchange": self.id,
28+
volume = price * abs(notional)
29+
t = {
30+
"exchange": self.id.lower(),
2231
"uid": uid,
23-
"symbol": pair, # Do not normalize
24-
"timestamp": ts,
32+
"symbol": pair,
33+
"timestamp": self.parse_datetime(ts),
2534
"price": price,
2635
"volume": volume,
27-
"notional": notional,
36+
"notional": abs(notional),
2837
"tickRule": -1 if notional < 0 else 1,
2938
}
30-
await self.callback(TRADES, trade, ts)
39+
await self.callback(TRADES, t, timestamp)
40+
41+
# Drop first message.
42+
if self.is_initialized:
43+
if isinstance(msg[1], list):
44+
# Snapshot.
45+
for trade in msg[1]:
46+
await _trade_update(trade, timestamp)
47+
elif msg[1] in ("te", "fte"):
48+
# Update.
49+
await _trade_update(msg[2], timestamp)
50+
elif msg[1] not in ("tu", "ftu", "hb"):
51+
# Ignore trade updates and heartbeats.
52+
LOG.warning("%s %s: Unexpected trade message %s", self.id, pair, msg)
53+
else:
54+
self.is_initialized = True

cryptofeed_werks/exchanges/bitflyer.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
from cryptofeed.defines import TRADES
2-
from cryptofeed.exchanges import Bitflyer
1+
from typing import Tuple
32

4-
from ..exchange import Exchange
3+
from cryptofeed.defines import BUY, TRADES
4+
from cryptofeed.exchanges import Bitflyer as BaseBitflyer
55

6+
from ..feed import Feed
67

7-
class BitflyerExchange(Exchange, Bitflyer):
8-
async def _trade(self, msg: dict, timestamp: float):
8+
9+
class Bitflyer(Feed, BaseBitflyer):
10+
def std_symbol_to_exchange_symbol(self, symbol: str) -> str:
11+
"""Standard symbol to exchange symbol."""
12+
return symbol.replace("/", "_")
13+
14+
def exchange_symbol_to_std_symbol(self, symbol: str) -> str:
15+
"""Exchange symbol to standard symbol."""
16+
return symbol.replace("_", "/")
17+
18+
async def _trade(self, msg: dict, timestamp: float) -> Tuple[str, dict, float]:
919
"""
1020
{
1121
"jsonrpc":"2.0",
@@ -31,15 +41,14 @@ async def _trade(self, msg: dict, timestamp: float):
3141
price = update["price"]
3242
notional = update["size"]
3343
volume = price * notional
34-
ts = self.timestamp_normalize(update["exec_date"])
35-
trade = {
36-
"exchange": self.id,
44+
t = {
45+
"exchange": self.id.lower(),
3746
"uid": update["id"],
38-
"symbol": pair, # Do not normalize
39-
"timestamp": ts,
47+
"symbol": self.exchange_symbol_to_std_symbol(pair),
48+
"timestamp": self.parse_datetime(update["exec_date"]),
4049
"price": price,
4150
"volume": volume,
4251
"notional": notional,
43-
"tickRule": 1 if update["side"] == "BUY" else -1,
52+
"tickRule": 1 if update["side"] == BUY else -1,
4453
}
45-
await self.callback(TRADES, trade, ts)
54+
await self.callback(TRADES, t, timestamp)
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from decimal import Decimal
2+
from typing import Tuple
23

3-
from cryptofeed.defines import TRADES
4-
from cryptofeed.exchanges import Bitmex
4+
from cryptofeed.defines import BUY, TRADES
5+
from cryptofeed.exchanges import Bitmex as BaseBitmex
56

6-
from ..exchange import Exchange
7+
from ..feed import Feed
78

89

9-
class BitmexExchange(Exchange, Bitmex):
10-
async def _trade(self, msg: dict, timestamp: float):
10+
class Bitmex(Feed, BaseBitmex):
11+
async def _trade(self, msg: dict, timestamp: float) -> Tuple[str, dict, float]:
1112
"""
1213
trade msg example
1314
{
@@ -27,15 +28,14 @@ async def _trade(self, msg: dict, timestamp: float):
2728
price = Decimal(data["price"])
2829
volume = Decimal(data["foreignNotional"])
2930
notional = volume / price
30-
ts = self.timestamp_normalize(data["timestamp"])
31-
trade = {
32-
"exchange": self.id,
31+
t = {
32+
"exchange": self.id.lower(),
3333
"uid": data["trdMatchID"],
34-
"symbol": data["symbol"], # Do not normalize
35-
"timestamp": ts,
34+
"symbol": data["symbol"],
35+
"timestamp": self.parse_datetime(data["timestamp"]),
3636
"price": price,
3737
"volume": volume,
3838
"notional": notional,
39-
"tickRule": 1 if data["side"] == "Buy" else -1,
39+
"tickRule": 1 if data["side"].lower() == BUY else -1,
4040
}
41-
await self.callback(TRADES, trade, ts)
41+
await self.callback(TRADES, t, timestamp)

0 commit comments

Comments
 (0)