Skip to content

Commit

Permalink
Simplify database operations
Browse files Browse the repository at this point in the history
By creating simpler and atomic db operations as a separate layer, we reduce complexity for testing (duplicated code), decouple dependencies of database and services and also reduce circular imports.
  • Loading branch information
carkod committed Dec 8, 2024
1 parent ad80ade commit 756c481
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 100 deletions.
43 changes: 22 additions & 21 deletions api/bots/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session
from api.database.bot_crud import BotTableCrud
from api.deals.controllers import CreateDealController
from bots.bot_table_controller import BotTableController
from database.models.bot_table import BotTable
from database.utils import get_session
from tools.handle_error import json_response, json_response_error, json_response_message
from bots.controllers import Bot
from bots.schemas import BotSchema, BotListResponse, ErrorsRequestBody
Expand Down Expand Up @@ -69,29 +75,29 @@ def delete(id: List[str]):


@bot_blueprint.get("/bot/activate/{id}", tags=["bots"])
async def activate_by_id(id: str):
async def activate_by_id(id: str, session: Session = Depends(get_session)):
"""
Activate bot
- Creates deal
- If changes were made, it will override DB data
- Because botId is received from endpoint, it will be a str not a PyObjectId
"""
bot_instance = Bot(collection_name="bots")
bot = bot_instance.get_one(id)
if bot:
try:
bot_instance.activate(bot)
return json_response_message("Successfully activated bot!")
except BinbotErrors as error:
bot_instance.post_errors_by_id(id, error.message)
return json_response_error(error.message)
except BinanceErrors as error:
bot_instance.post_errors_by_id(id, error.message)
return json_response_error(error.message)
bot = BotTableCrud(session=session).get_one(bot_id=id)
if not bot:
return json_response_message("Successfully activated bot!")

else:
return json_response_error("Bot not found.")
bot_instance = CreateDealController(bot, db_table=BotTable)

try:
bot_instance.activate(bot)
return json_response_message("Successfully activated bot!")
except BinbotErrors as error:
bot_instance.update_logs(bot_id=id, log_message=error.message)
return json_response_error(error.message)
except BinanceErrors as error:
bot_instance.update_logs(bot_id=id, log_message=error.message)
return json_response_error(error.message)


@bot_blueprint.delete("/bot/deactivate/{id}", tags=["bots"])
Expand All @@ -115,11 +121,6 @@ def deactivation(id: str):
return json_response_error("Error deactivating bot.")


@bot_blueprint.put("/bot/archive/{id}", tags=["bots"])
def archive(id: str):
return Bot(collection_name="bots").put_archive(id)


@bot_blueprint.post("/bot/errors/{bot_id}", tags=["bots"])
def bot_errors(bot_id: str, bot_errors: ErrorsRequestBody):
"""
Expand Down
14 changes: 11 additions & 3 deletions api/bots/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class BotSchema(BaseModel):
fiat: str = "USDC"
balance_to_use: str = "USDC"
base_order_size: float | int = 15 # Min Binance 0.0001 BNB
candlestick_interval: BinanceKlineIntervals = Field(default=BinanceKlineIntervals.fifteen_minutes)
candlestick_interval: BinanceKlineIntervals = Field(
default=BinanceKlineIntervals.fifteen_minutes
)
close_condition: CloseConditions = Field(default=CloseConditions.dynamic_trailling)
# cooldown period in minutes before opening next bot with same pair
cooldown: int = 0
Expand Down Expand Up @@ -91,7 +93,9 @@ def check_names_not_empty(cls, v):
assert v != "", "Empty pair field."
return v

@field_validator("balance_size_to_use", "base_order_size", "base_order_size", mode="before")
@field_validator(
"balance_size_to_use", "base_order_size", "base_order_size", mode="before"
)
@classmethod
def countables(cls, v):
if isinstance(v, float):
Expand All @@ -104,7 +108,11 @@ def countables(cls, v):
raise ValueError(f"{v} must be a number (float, int or string)")

@field_validator(
"stop_loss", "take_profit", "trailling_deviation", "trailling_profit", mode="before"
"stop_loss",
"take_profit",
"trailling_deviation",
"trailling_profit",
mode="before",
)
@classmethod
def check_percentage(cls, v):
Expand Down
11 changes: 10 additions & 1 deletion api/charts/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,5 +248,14 @@ def top_gainers(self):
fiat = self.autotrade_db.get_fiat()
ticket_data = self.ticker_24()

fiat_market_data = sorted((item for item in ticket_data if item["symbol"].endswith(fiat) and float(item["priceChangePercent"]) > 0), key=lambda x: x["priceChangePercent"], reverse=True)
fiat_market_data = sorted(
(
item
for item in ticket_data
if item["symbol"].endswith(fiat)
and float(item["priceChangePercent"]) > 0
),
key=lambda x: x["priceChangePercent"],
reverse=True,
)
return fiat_market_data[:10]
71 changes: 30 additions & 41 deletions api/bots/bot_table_controller.py → api/database/bot_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
from fastapi import Query
from sqlmodel import Session, asc, desc, or_, select, case
from time import time
from base_producer import BaseProducer
from deals.controllers import CreateDealController
from bots.schemas import BotSchema
from database.models.bot_table import BotTable
from database.models.deal_table import DealTable
from database.utils import independent_session
from deals.controllers import CreateDealController
from deals.models import DealModel
from tools.enum_definitions import BinbotEnums, Status
from psycopg.types.json import Json, set_json_loads
from tools.enum_definitions import BinbotEnums, Status, Strategy
from psycopg.types.json import Json


class BotTableController:
class BotTableCrud:
"""
CRUD and database operations for the SQL API DB
bot_table table.
Use for lower level APIs that require a session
e.g.
client-side -> receive json -> bots.routes -> BotTableCrud
"""

def __init__(
Expand All @@ -29,11 +32,10 @@ def __init__(
if session is None:
session = independent_session()
self.session = session
self.base_producer = BaseProducer()
self.producer = self.base_producer.start_producer()
self.deal: CreateDealController | None = None

def update_logs(self, log_message: str, bot: BotSchema = None, bot_id: str | None = None):
def update_logs(
self, log_message: str, bot: BotSchema = None, bot_id: str | None = None
):
"""
Update logs for a bot
Expand Down Expand Up @@ -74,8 +76,10 @@ def get(
"""
Get all bots in the db except archived
Args:
- archive=false
- filter_by: string - last-week, last-month, all
- status: Status enum
- start_date and end_date are timestamps in milliseconds
- no_cooldown: bool - filter out bots that are in cooldown
- limit and offset for pagination
"""
statement = select(BotTable)

Expand Down Expand Up @@ -143,6 +147,9 @@ def get_one(self, bot_id: str | None = None, symbol: str | None = None):
def create(self, data: BotSchema):
"""
Create a new bot
It's crucial to reset fields, so bot can trigger base orders
and start trailling.
"""
bot = BotTable.model_validate(data)
# Ensure values are reset
Expand All @@ -152,26 +159,27 @@ def create(self, data: BotSchema):
bot.updated_at = time() * 1000
bot.status = Status.inactive
bot.deal = DealModel()

# db operations
self.session.add(bot)
self.session.commit()
self.session.close()
self.base_producer.update_required(self.producer, "CREATE_BOT")
return bot

def edit(self, bot_id: str, data: BotSchema):
def edit(self, id: str, data: BotSchema):
"""
Edit a bot
"""
bot = self.session.get(BotTable, bot_id)
bot = self.session.get(BotTable, id)
if not bot:
return bot

dumped_bot = bot.model_dump(exclude_unset=True)
# double check orders and deal are not overwritten
dumped_bot = data.model_dump(exclude_unset=True)
bot.sqlmodel_update(dumped_bot)
self.session.add(bot)
self.session.commit()
self.session.close()
self.base_producer.update_required(self.producer, "UPDATE_BOT")
return bot

def delete(self, bot_ids: List[str] = Query(...)):
Expand All @@ -185,38 +193,19 @@ def delete(self, bot_ids: List[str] = Query(...)):
bots = self.session.exec(statement).all()
self.session.commit()
self.session.close()
self.base_producer.update_required(self.producer, "DELETE_BOT")
return bots

def activate(self, bot_id: str):
"""
Activate a bot
"""
bot = self.session.get(BotTable, bot_id)
if not bot:
return bot

bot.status = Status.active
self.session.add(bot)
self.session.commit()
self.session.close()
self.base_producer.update_required(self.producer, "ACTIVATE_BOT")
return bot

def deactivate(self, bot_id: str):
def update_status(self, bot: BotTable, status: Status) -> BotTable:
"""
Deactivate a bot (panic sell)
Activate a bot by opening a deal
"""
bot = self.session.get(BotTable, bot_id)
if not bot:
return bot

bot.status = Status.completed

bot.status = status
# db operations
self.session.add(bot)
self.session.commit()
self.session.close()
self.base_producer.update_required(self.producer, "DEACTIVATE_BOT")
# do this after db operations in case there is rollback
# avoids sending unnecessary signals
return bot

def get_active_pairs(self):
Expand Down
77 changes: 77 additions & 0 deletions api/database/paper_trading_crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from time import time

from sqlmodel import Session
from database.models.paper_trading_table import PaperTradingTable
from database.utils import independent_session
from tools.enum_definitions import Status


class PaperTradingTableCrud:
def __init__(self, session: Session | None = None):
if session is None:
session = independent_session()
self.session = session
pass

def create(self, paper_trading: PaperTradingTable) -> PaperTradingTable:
"""
Create a new paper trading account
"""
paper_trading.created_at = time() * 1000
paper_trading.updated_at = time() * 1000

# db operations
self.session.add(paper_trading)
self.session.commit()
self.session.close()
return paper_trading

def edit(self, paper_trading: PaperTradingTable) -> PaperTradingTable:
"""
Edit a paper trading account
"""
self.session.add(paper_trading)
self.session.commit()
self.session.close()
return paper_trading

def delete(self, id: str) -> bool:
"""
Delete a paper trading account by id
"""
paper_trading = self.session.get(PaperTradingTable, id)
if not paper_trading:
return False

self.session.delete(paper_trading)
self.session.commit()
self.session.close()
return True

def get(self, id: str) -> PaperTradingTable:
"""
Get a paper trading account by id
"""
paper_trading = self.session.get(PaperTradingTable, id)
self.session.close()
return paper_trading

def activate(self, paper_trading: PaperTradingTable) -> PaperTradingTable:
"""
Activate a paper trading account
"""
paper_trading.status = Status.active
self.session.add(paper_trading)
self.session.commit()
self.session.close()
return paper_trading

def deactivate(self, paper_trading: PaperTradingTable) -> PaperTradingTable:
"""
Deactivate a paper trading account
"""
paper_trading.status = Status.inactive
self.session.add(paper_trading)
self.session.commit()
self.session.close()
return paper_trading
Loading

0 comments on commit 756c481

Please sign in to comment.