You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
467 lines
11 KiB
467 lines
11 KiB
import time |
|
from dataclasses import dataclass |
|
from datetime import datetime |
|
from enum import Enum, IntEnum |
|
from typing import Dict, List |
|
|
|
from cached_property import cached_property |
|
|
|
from .helpers import round_down, round_up |
|
|
|
|
|
@dataclass |
|
class Coin: |
|
exchange_name: str |
|
|
|
@property |
|
def name(self): |
|
return self.exchange_name.replace("1", "ONE") |
|
|
|
def __hash__(self): |
|
return self.name.__hash__() |
|
|
|
|
|
@dataclass |
|
class Pair: |
|
exchange_name: str |
|
price_precision: int |
|
quantity_precision: int |
|
|
|
@property |
|
def name(self): |
|
return self.exchange_name.replace("1", "ONE") |
|
|
|
@cached_property |
|
def base_coin(self) -> Coin: |
|
return Coin(self.name.split("_")[0]) |
|
|
|
@cached_property |
|
def quote_coin(self) -> Coin: |
|
return Coin(self.name.split("_")[1]) |
|
|
|
def round_price(self, price): |
|
return round_down(price, self.price_precision) |
|
|
|
def round_quantity(self, quantity): |
|
return round_down(quantity, self.quantity_precision) |
|
|
|
def __hash__(self): |
|
return self.name.__hash__() |
|
|
|
|
|
class DefaultPairDict(dict): |
|
"""Use default precision for old missing pairs.""" |
|
|
|
def __getitem__(self, name: str) -> Pair: |
|
try: |
|
return super().__getitem__(name) |
|
except KeyError: |
|
return Pair(name, 8, 8) |
|
|
|
|
|
@dataclass |
|
class MarketTicker: |
|
pair: Pair |
|
buy_price: float |
|
sell_price: float |
|
trade_price: float |
|
time: int |
|
volume: float |
|
high: float |
|
low: float |
|
change: float |
|
|
|
@classmethod |
|
def from_api(cls, pair, data): |
|
return cls( |
|
pair=pair, |
|
buy_price=pair.round_price(data["b"]), |
|
sell_price=pair.round_price(data["k"]), |
|
trade_price=pair.round_price(data["a"]), |
|
time=int(data["t"] / 1000), |
|
volume=pair.round_quantity(data["v"]), |
|
high=pair.round_price(data["h"]), |
|
low=pair.round_price(data["l"]), |
|
change=round_down(data["c"], 3), |
|
) |
|
|
|
|
|
class OrderSide(str, Enum): |
|
BUY = "BUY" |
|
SELL = "SELL" |
|
|
|
|
|
@dataclass |
|
class MarketTrade: |
|
id: int |
|
time: int |
|
price: float |
|
quantity: float |
|
side: OrderSide |
|
pair: Pair |
|
|
|
@classmethod |
|
def from_api(cls, pair: Pair, data: Dict): |
|
return cls( |
|
id=data["d"], |
|
time=int(data["t"] / 1000), |
|
price=pair.round_price(data["p"]), |
|
quantity=pair.round_quantity(data["q"]), |
|
side=OrderSide(data["s"].upper()), |
|
pair=pair, |
|
) |
|
|
|
|
|
class Period(str, Enum): |
|
MINS = "1m" |
|
MINS_5 = "5m" |
|
MINS_15 = "15m" |
|
MINS_30 = "30m" |
|
HOURS = "1h" |
|
HOURS_4 = "4h" |
|
HOURS_6 = "6h" |
|
HOURS_12 = "12h" |
|
DAY = "1D" |
|
WEEK = "7D" |
|
WEEK_2 = "14D" |
|
MONTH_1 = "1M" |
|
|
|
|
|
@dataclass |
|
class Candle: |
|
time: int |
|
open: float |
|
high: float |
|
low: float |
|
close: float |
|
volume: float |
|
pair: Pair |
|
|
|
@classmethod |
|
def from_api(cls, pair: Pair, data: Dict): |
|
return cls( |
|
time=int(data["t"] / 1000), |
|
open=pair.round_price(float(data["o"])), |
|
high=pair.round_price(float(data["h"])), |
|
low=pair.round_price(float(data["l"])), |
|
close=pair.round_price(float(data["c"])), |
|
volume=pair.round_quantity(float(data["v"])), |
|
pair=pair, |
|
) |
|
|
|
|
|
@dataclass |
|
class OrderInBook: |
|
price: float |
|
quantity: float |
|
count: int |
|
pair: Pair |
|
side: OrderSide |
|
|
|
@property |
|
def volume(self) -> float: |
|
return self.pair.round_quantity(self.price * self.quantity) |
|
|
|
|
|
@dataclass |
|
class OrderBook: |
|
buys: List[OrderInBook] |
|
sells: List[OrderInBook] |
|
pair: Pair |
|
|
|
@property |
|
def spread(self) -> float: |
|
return round_down(self.sells[-1].price / self.buys[0].price - 1, 6) |
|
|
|
|
|
@dataclass |
|
class Balance: |
|
total: float |
|
available: float |
|
in_orders: float |
|
in_stake: float |
|
coin: Coin |
|
|
|
@classmethod |
|
def from_api(cls, data): |
|
return cls( |
|
total=data["balance"], |
|
available=data["available"], |
|
in_orders=data["order"], |
|
in_stake=data["stake"], |
|
coin=Coin(data["currency"]), |
|
) |
|
|
|
|
|
class OrderType(str, Enum): |
|
LIMIT = "LIMIT" |
|
MARKET = "MARKET" |
|
STOP_LOSS = "STOP_LOSS" |
|
STOP_LIMIT = "STOP_LIMIT" |
|
TAKE_PROFIT = "TAKE_PROFIT" |
|
TAKE_PROFIT_LIMIT = "TAKE_PROFIT_LIMIT" |
|
|
|
|
|
class OrderStatus(str, Enum): |
|
ACTIVE = "ACTIVE" |
|
FILLED = "FILLED" |
|
CANCELED = "CANCELED" |
|
REJECTED = "REJECTED" |
|
EXPIRED = "EXPIRED" |
|
PENDING = "PENDING" |
|
|
|
|
|
class OrderExecType(str, Enum): |
|
MARKET = "" |
|
POST_ONLY = "POST_ONLY" |
|
|
|
|
|
class OrderForceType(str, Enum): |
|
GOOD_TILL_CANCEL = "GOOD_TILL_CANCEL" |
|
FILL_OR_KILL = "FILL_OR_KILL" |
|
IMMEDIATE_OR_CANCEL = "IMMEDIATE_OR_CANCEL" |
|
|
|
|
|
@dataclass |
|
class PrivateTrade: |
|
id: int |
|
side: OrderSide |
|
pair: Pair |
|
fees: float |
|
fees_coin: Coin |
|
created_at: int |
|
filled_price: float |
|
filled_quantity: float |
|
order_id: int |
|
|
|
@cached_property |
|
def is_buy(self): |
|
return self.side == OrderSide.BUY |
|
|
|
@cached_property |
|
def is_sell(self): |
|
return self.side == OrderSide.SELL |
|
|
|
@classmethod |
|
def create_from_api(cls, pair: Pair, data: Dict) -> "PrivateTrade": |
|
return cls( |
|
id=int(data["trade_id"]), |
|
side=OrderSide(data["side"]), |
|
pair=pair, |
|
fees=round_up(data["fee"], 8), |
|
fees_coin=Coin(data["fee_currency"]), |
|
created_at=int(data["create_time"] / 1000), |
|
filled_price=pair.round_price(data["traded_price"]), |
|
filled_quantity=pair.round_quantity(data["traded_quantity"]), |
|
order_id=int(data["order_id"]), |
|
) |
|
|
|
|
|
@dataclass |
|
class Order: |
|
id: int |
|
status: OrderStatus |
|
side: OrderSide |
|
price: float |
|
quantity: float |
|
client_id: str |
|
created_at: int |
|
updated_at: int |
|
type: OrderType |
|
pair: Pair |
|
filled_quantity: float |
|
filled_price: float |
|
fees_coin: Coin |
|
force_type: OrderForceType |
|
trigger_price: float |
|
trades: List[PrivateTrade] |
|
|
|
@cached_property |
|
def is_buy(self): |
|
return self.side == OrderSide.BUY |
|
|
|
@cached_property |
|
def is_sell(self): |
|
return self.side == OrderSide.SELL |
|
|
|
@cached_property |
|
def is_active(self): |
|
return self.status == OrderStatus.ACTIVE |
|
|
|
@cached_property |
|
def is_canceled(self): |
|
return self.status == OrderStatus.CANCELED |
|
|
|
@cached_property |
|
def is_rejected(self): |
|
return self.status == OrderStatus.REJECTED |
|
|
|
@cached_property |
|
def is_expired(self): |
|
return self.status == OrderStatus.EXPIRED |
|
|
|
@cached_property |
|
def is_filled(self): |
|
return self.status == OrderStatus.FILLED |
|
|
|
@cached_property |
|
def is_pending(self): |
|
return self.status == OrderStatus.PENDING |
|
|
|
@cached_property |
|
def volume(self): |
|
return self.pair.round_quantity(self.price * self.quantity) |
|
|
|
@cached_property |
|
def filled_volume(self): |
|
return self.pair.round_quantity( |
|
self.filled_price * self.filled_quantity |
|
) |
|
|
|
@cached_property |
|
def remain_volume(self): |
|
return self.pair.round_quantity(self.filled_volume - self.volume) |
|
|
|
@cached_property |
|
def remain_quantity(self): |
|
return self.pair.round_quantity(self.quantity - self.filled_quantity) |
|
|
|
@classmethod |
|
def create_from_api( |
|
cls, pair: Pair, data: Dict, trades: List[Dict] = None |
|
) -> "Order": |
|
fees_coin, trigger_price = None, None |
|
if data["fee_currency"]: |
|
fees_coin = Coin(data["fee_currency"]) |
|
if data.get("trigger_price") is not None: |
|
trigger_price = pair.round_price(data["trigger_price"]) |
|
|
|
trades = [ |
|
PrivateTrade.create_from_api(pair, trade) for trade in trades or [] |
|
] |
|
|
|
return cls( |
|
id=int(data["order_id"]), |
|
status=OrderStatus(data["status"]), |
|
side=OrderSide(data["side"]), |
|
price=pair.round_price(data["avg_price"] or data["price"]), |
|
quantity=pair.round_quantity(data["quantity"]), |
|
client_id=data["client_oid"], |
|
created_at=int(data["create_time"] / 1000), |
|
updated_at=int(data["update_time"] / 1000), |
|
type=OrderType(data["type"]), |
|
pair=pair, |
|
filled_price=pair.round_price(data["avg_price"]), |
|
filled_quantity=pair.round_quantity(data["cumulative_quantity"]), |
|
fees_coin=fees_coin, |
|
force_type=OrderForceType(data["time_in_force"]), |
|
trigger_price=trigger_price, |
|
trades=trades, |
|
) |
|
|
|
|
|
@dataclass |
|
class Interest: |
|
loan_id: int |
|
coin: Coin |
|
interest: float |
|
stake_amount: float |
|
interest_rate: float |
|
|
|
@classmethod |
|
def create_from_api(cls, data: Dict) -> "Interest": |
|
return cls( |
|
loan_id=int(data["loan_id"]), |
|
coin=Coin(data["currency"]), |
|
interest=float(data["interest"]), |
|
stake_amount=float(data["stake_amount"]), |
|
interest_rate=float(data["interest_rate"]), |
|
) |
|
|
|
|
|
class WithdrawalStatus(str, Enum): |
|
PENDING = "0" |
|
PROCESSING = "1" |
|
REJECTED = "2" |
|
PAYMENT_IN_PROGRESS = "3" |
|
PAYMENT_FAILED = "4" |
|
COMPLETED = "5" |
|
CANCELLED = "6" |
|
|
|
|
|
class DepositStatus(str, Enum): |
|
NOT_ARRIVED = "0" |
|
ARRIVED = "1" |
|
FAILED = "2" |
|
PENDING = "3" |
|
|
|
|
|
class TransactionType(IntEnum): |
|
WITHDRAWAL = 0 |
|
DEPOSIT = 1 |
|
|
|
|
|
@dataclass |
|
class Transaction: |
|
coin: Coin |
|
fee: float |
|
create_time: int |
|
id: str |
|
update_time: int |
|
amount: float |
|
address: str |
|
|
|
@staticmethod |
|
def _prepare(data): |
|
return dict( |
|
id=data["id"], |
|
coin=Coin(data["currency"]), |
|
fee=float(data["fee"]), |
|
create_time=datetime.fromtimestamp( |
|
int(data["create_time"]) / 1000 |
|
), |
|
update_time=datetime.fromtimestamp( |
|
int(data["update_time"]) / 1000 |
|
), |
|
amount=float(data["amount"]), |
|
address=data["address"], |
|
) |
|
|
|
|
|
@dataclass |
|
class Deposit(Transaction): |
|
status: DepositStatus |
|
|
|
@classmethod |
|
def create_from_api(cls, data: Dict) -> "Deposit": |
|
params = cls._prepare(data) |
|
params["status"] = DepositStatus(data["status"]) |
|
return cls(**params) |
|
|
|
|
|
@dataclass |
|
class Withdrawal(Transaction): |
|
client_wid: str |
|
status: WithdrawalStatus |
|
txid: str |
|
|
|
@classmethod |
|
def create_from_api(cls, data: Dict) -> "Withdrawal": |
|
params = cls._prepare(data) |
|
params["client_wid"] = data.get("client_wid", "") |
|
params["status"] = WithdrawalStatus(data["status"]) |
|
params["txid"] = data["txid"] |
|
return cls(**params) |
|
|
|
|
|
class Timeframe(IntEnum): |
|
NOW = 0 |
|
MINUTES = 60 |
|
HOURS = 60 * MINUTES |
|
DAYS = 24 * HOURS |
|
WEEKS = 7 * DAYS |
|
MONTHS = 30 * DAYS |
|
|
|
@classmethod |
|
def resolve(cls, seconds: int) -> int: |
|
return seconds + int(time.time())
|
|
|