Skip to content
Merged
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
20 changes: 13 additions & 7 deletions api/account/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from tools.handle_error import json_response, json_response_error, json_response_message
from tools.round_numbers import round_numbers
from tools.exceptions import BinanceErrors, LowBalanceCleanupError
from tools.enum_definitions import Status, Strategy
from tools.enum_definitions import Strategy
from database.bot_crud import BotTableCrud


Expand Down Expand Up @@ -333,28 +333,34 @@ def disable_isolated_accounts(self):

return json_response_message(msg)

def one_click_liquidation(self, pair: str) -> BotTable:
def one_click_liquidation(self, pair: str, market: str = "margin") -> BotTable:
"""
Emulate Binance Dashboard
One click liquidation function

This endpoint is different than the margin_liquidation function
in that it contains some clean up functionality in the cases
where there are are still funds in the isolated pair
where there are are still funds in the isolated pair.
Therefore, it should clean all bots with provided pairs without filtering
by status.

market arg is required, because there can be repeated
pairs in both MARGIN and SPOT markets.
"""

bot = self.bot_controller.get_one(symbol=pair, status=Status.active)
strategy = Strategy.margin_short if market == "margin" else Strategy.long

bot = self.bot_controller.get_one(symbol=pair, strategy=strategy)

if not bot:
return bot

active_bot = BotModel.model_validate(bot.model_dump())
deal = DealAbstract(active_bot, db_table=BotTable)

if active_bot.strategy == Strategy.margin_short:
if market == "margin":
deal.margin_liquidation(pair)

if active_bot.strategy == Strategy.long:
else:
deal.spot_liquidation(pair)

return bot
12 changes: 8 additions & 4 deletions api/account/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
GainersLosersResponse,
BalanceSeriesResponse,
)
from tools.exceptions import BinanceErrors, LowBalanceCleanupError, MarginLoanNotFound
from tools.exceptions import BinanceErrors, BinbotErrors, LowBalanceCleanupError, MarginLoanNotFound
from tools.handle_error import json_response, json_response_error, json_response_message
from sqlmodel import Session
from database.utils import get_session
Expand Down Expand Up @@ -159,10 +159,10 @@ def check_isolated_symbol(symbol: str, session: Session = Depends(get_session)):
return isolated_account


@account_blueprint.get("/one-click-liquidation/{asset}", tags=["assets"])
def one_click_liquidation(asset: str, session: Session = Depends(get_session)):
@account_blueprint.get("/one-click-liquidation/{market}/{asset}", tags=["assets"])
def one_click_liquidation(asset: str, market: str = "margin", session: Session = Depends(get_session)):
try:
liquidated = Assets(session=session).one_click_liquidation(asset)
liquidated = Assets(session=session).one_click_liquidation(asset, market)
if not liquidated:
raise HTTPException(
status_code=404, detail="Could not liquidate asset that doesn't exist."
Expand All @@ -174,3 +174,7 @@ def one_click_liquidation(asset: str, session: Session = Depends(get_session)):
)
except BinanceErrors as error:
return json_response_error(f"Error liquidating {asset}: {error.message}")

except BinbotErrors as error:
return json_response_error(f"Error liquidating {asset}: {error.message}")

52 changes: 51 additions & 1 deletion api/database/api_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from database.models.order_table import ExchangeOrderTable
from database.models.user_table import UserTable
from database.models.bot_table import BotTable, PaperTradingTable
from database.models.account_balances import BalancesTable, ConsolidatedBalancesTable
from database.models.blacklist import Blacklist
from sqlmodel import Session, SQLModel, select
from tools.enum_definitions import (
AutotradeSettingsDocument,
Expand All @@ -17,7 +19,7 @@
)
from alembic.config import Config
from alembic import command
from database.utils import engine
from database.utils import engine, timestamp


class ApiDb:
Expand All @@ -31,6 +33,7 @@ def init_db(self):
self.init_users()
self.create_dummy_bot()
self.init_autotrade_settings()
self.create_dummy_balance()
self.session.close()
logging.info("Finishing db operations")

Expand Down Expand Up @@ -226,3 +229,50 @@ def delete_bot(self, pair):
self.session.commit()
self.session.close()
return results.first()

def create_dummy_balance(self):
statement = select(ConsolidatedBalancesTable)
results = self.session.exec(statement)
if results.first():
return

id = timestamp() / 1000
balances = [
BalancesTable(asset="BTC", free=0.3, locked=0),
BalancesTable(asset="BNB", free=0.00096915, locked=0),
BalancesTable(asset="ZEC", free=0.191808, locked=0),
BalancesTable(asset="KMD", free=37.81, locked=0),
BalancesTable(asset="DUSK", free=294, locked=0),
BalancesTable(asset="GBP", free=9.87392004, locked=0),
BalancesTable(asset="NFT", free=26387.61, locked=0),
]

consolidated = ConsolidatedBalancesTable(
id=id,
balances=balances,
estimated_total_fiat=0.0088,
)
self.session.add(consolidated)
self.session.commit()
self.session.refresh(consolidated)
return consolidated

def select_balance(self):
statement = select(ConsolidatedBalancesTable)
results = self.session.exec(statement)
balance = results.first()
return balance

def create_dummy_blacklist(self):
statement = select(Blacklist)
results = self.session.exec(statement)
if results.first():
return
btc_eth = Blacklist(
id="BTCETH",
reason="Test",
)
self.session.add(btc_eth)
self.session.commit()
self.session.refresh(btc_eth)
return btc_eth
9 changes: 8 additions & 1 deletion api/database/bot_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from database.models.deal_table import DealTable
from database.models.order_table import ExchangeOrderTable
from database.utils import independent_session
from tools.enum_definitions import BinbotEnums, Status
from tools.enum_definitions import BinbotEnums, Status, Strategy
from bots.models import BotBase
from collections.abc import Sequence
from sqlalchemy.orm.attributes import flag_modified
Expand Down Expand Up @@ -130,6 +130,7 @@ def get_one(
bot_id: Optional[str] = None,
symbol: Optional[str] = None,
status: Status | None = None,
strategy: Optional[Strategy] = None,
) -> BotTable:
"""
Get one bot by id or symbol
Expand All @@ -150,6 +151,12 @@ def get_one(
BotTable.pair == symbol, BotTable.status == status
)
).first()
elif strategy:
bot = self.session.exec(
select(BotTable).where(
BotTable.pair == symbol, BotTable.strategy == strategy
)
).first()
else:
bot = self.session.exec(
select(BotTable).where(BotTable.pair == symbol)
Expand Down
88 changes: 50 additions & 38 deletions api/database/models/account_balances.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,76 @@
from typing import Optional
from pydantic import field_validator
from sqlalchemy import JSON, Column
from uuid import UUID
import uuid
from sqlalchemy import BigInteger, Column
from database.utils import timestamp
from sqlmodel import SQLModel, Field
from sqlmodel import Relationship, SQLModel, Field


class ExchangeInfoTable(SQLModel, table=True):
class BalancesTable(SQLModel, table=True):
"""
Temporary design of new consolidated table for all balance data this includes:

- Old MongoDB balance data
- New plans to include exchange info data
- Old blacklist data then would become redundant
- Single source of truth for all exchanges, so an field indicating exchange must be included
Balances table to store user balances
"""

__tablename__ = "exchange_info"
__tablename__ = "balances"

symbol: str = Field(index=True, primary_key=True, nullable=False, unique=True)
base_asset: str = Field(
index=True, description="Base asset of the symbol e.g. BTCUSDC -> BTC"
id: UUID = Field(
default_factory=uuid.uuid4,
primary_key=True,
unique=True,
index=True,
)
binance_exchange_info: dict = Field(
sa_column=Column(JSON),
description="For now, this is the full binance exchange info to make things easier",
timestamp: int = Field(
default_factory=timestamp, sa_column=Column(BigInteger(), index=True)
)
created_at: float = Field(default_factory=timestamp)
updated_at: float = Field(default_factory=timestamp)

model_config = {
"from_attributes": True,
"use_enum_values": True,
}
asset: str = Field(index=True, nullable=False)
quantity: Optional[float] = Field(default=0, description="local quantity (asset)")

@field_validator("logs", mode="before")
def validate_logs(cls, v, info):
return v
# Relationships
consolidated_balances_id: Optional[int] = Field(
default=None, foreign_key="consolidated_balances.id", ondelete="CASCADE"
)
consolidated_balances: Optional["ConsolidatedBalancesTable"] = Relationship(
back_populates="balances"
)


class ConsolidatedBalancesTable(SQLModel, table=True):
"""
Replacement of old MongoDB balances collection
future plans include consolidation with all connected exchanges
Replacement of old MongoDB balances collection
future plans include consolidation with all connected exchanges.

- id is a unique timestamp to support series. This should be ingested as before, once per day
Should return the same as previous balances collection

{
_id: ObjectId('60eb374383f341ddc54b9411'),
time: '2021-07-11',
balances: [
{ asset: 'BTC', free: 0.00333733, locked: 0 },
{ asset: 'BNB', free: 0.00096915, locked: 0 },
{ asset: 'ZEC', free: 0.191808, locked: 0 },
{ asset: 'KMD', free: 37.81, locked: 0 },
{ asset: 'DUSK', free: 294, locked: 0 },
{ asset: 'GBP', free: 9.87392004, locked: 0 },
{ asset: 'NFT', free: 26387.614932, locked: 0 }
],
estimated_total_btc: 0.008809462952797817,
estimated_total_gbp: 203.25007925973117
}

- id is a unique timestamp to support series. This should be ingested as before, once per day

"""

__tablename__ = "consolidated_balances"

id: float = Field(
id: int = Field(
default_factory=timestamp,
primary_key=True,
unique=True,
index=True,
nullable=False,
sa_column=Column(BigInteger(), primary_key=True, unique=True, index=True),
)
asset: str = Field(index=True, nullable=False)
free: Optional[float] = Field(default=0)
estimated_total_fiat: Optional[float] = Field(
balances: list[BalancesTable] = Relationship(
sa_relationship_kwargs={"lazy": "joined", "single_parent": True},
)
estimated_total_fiat: float = Field(
default=0,
description="This is derived from free * price of fiat, which is determined in autotrade",
)
11 changes: 11 additions & 0 deletions api/database/models/blacklist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlmodel import SQLModel, Field
from database.utils import timestamp


class Blacklist(SQLModel, table=True):
__tablename__ = "blacklist"

id: int = Field(primary_key=True, description="Symbol")
reason: str = Field(nullable=False)
created_at: int = Field(default_factory=timestamp)
updated_at: int = Field(default_factory=timestamp)
2 changes: 1 addition & 1 deletion api/database/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def independent_session() -> Session:
return Session(engine)


def timestamp() -> float:
def timestamp() -> int:
return int(round(time() * 1000))


Expand Down
2 changes: 1 addition & 1 deletion api/deals/margin.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def init_margin_short(self, initial_price: float) -> BotModel:
# transfer back and left client know (raise exception again)
if error.code == -3045:
self.terminate_failed_transactions()
raise BinanceErrors(error)
raise BinanceErrors(error.message)

self.active_bot.deal.margin_loan_id = int(loan_created["tranId"])
# in this new data system there is only one field for qty
Expand Down
3 changes: 0 additions & 3 deletions api/deals/spot.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,6 @@ def streaming_updates(self, close_price, open_price):
self.base_producer.update_required(
self.producer, "EXECUTE_SWITCH_MARGIN_SHORT"
)
self.controller.update_logs(
"Completed switch to margin short bot", self.active_bot
)

return

Expand Down
Loading