Skip to content

Commit

Permalink
Fix all SQLModel vs Pydantic model compatibility issues
Browse files Browse the repository at this point in the history
  • Loading branch information
carkod committed Dec 25, 2024
1 parent 54d3b28 commit 1bc0443
Show file tree
Hide file tree
Showing 23 changed files with 821 additions and 341 deletions.
107 changes: 99 additions & 8 deletions api/bots/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,92 @@
from typing import List
from database.models.order_table import OrderModel
from typing import List, Optional
from uuid import uuid4, UUID
from tools.enum_definitions import (
BinanceKlineIntervals,
CloseConditions,
Status,
Strategy,
)
from deals.models import DealModel
from database.models.bot_table import BotBase, BotTable
from pydantic import BaseModel, Field, field_validator
from pydantic import (
BaseModel,
Field,
Json,
field_validator,
)
from database.utils import timestamp
from tools.handle_error import StandardResponse, IResponseBase
from tools.enum_definitions import DealType, OrderType

from tools.handle_error import StandardResponse

class OrderModel(BaseModel):
order_type: OrderType
time_in_force: str
timestamp: Optional[int]
order_id: int
order_side: str
pair: str
qty: float
status: str
price: float
deal_type: DealType


class BotBase(BaseModel):
id: Optional[UUID] = Field(default_factory=uuid4)
pair: str
fiat: str = Field(default="USDC")
base_order_size: float = Field(
default=15, description="Min Binance 0.0001 BNB approx 15USD"
)
candlestick_interval: BinanceKlineIntervals = Field(
default=BinanceKlineIntervals.fifteen_minutes,
)
close_condition: CloseConditions = Field(
default=CloseConditions.dynamic_trailling,
)
cooldown: int = Field(
default=0,
description="cooldown period in minutes before opening next bot with same pair",
)
created_at: float = Field(default_factory=timestamp)
updated_at: float = Field(default_factory=timestamp)
dynamic_trailling: bool = Field(default=False)
logs: list[Json[str]] = Field(default=[])
mode: str = Field(default="manual")
name: str = Field(default="Default bot")
status: Status = Field(default=Status.inactive)
stop_loss: float = Field(
default=0, description="If stop_loss > 0, allow for reversal"
)
margin_short_reversal: bool = Field(default=False)
take_profit: float = Field(default=0)
trailling: bool = Field(default=False)
trailling_deviation: float = Field(
default=0,
ge=-1,
le=101,
description="Trailling activation (first take profit hit)",
)
trailling_profit: float = Field(default=0)
strategy: Strategy = Field(default=Strategy.long)
total_commission: float = Field(
default=0, description="autoswitch to short_strategy"
)

@field_validator("id")
def deserialize_id(cls, v):
if isinstance(v, UUID):
return str(v)
return True


class BotModel(BotBase):
"""
The way SQLModel works causes a lot of errors
if we combine (with inheritance) both Pydantic models
and SQLModels. they are not compatible. Thus the duplication
"""

deal: DealModel = Field(default_factory=DealModel)
orders: List[OrderModel] = Field(default=[])

Expand Down Expand Up @@ -45,11 +124,23 @@ class BotModel(BotBase):


class BotResponse(StandardResponse):
data: BotModel
data: Optional[BotModel]


class ActivePairsResponse(IResponseBase):
data: list[str]


class BotListResponse(IResponseBase):
"""
Model exclusively used to serialize
list of bots.
Has to be converted to BotModel to be able to
serialize nested table objects (deal, orders)
"""

class BotListResponse(StandardResponse):
data: list[BotTable]
data: list[BotModel]


class ErrorsRequestBody(BaseModel):
Expand Down
120 changes: 76 additions & 44 deletions api/bots/routes.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
from fastapi import APIRouter, Depends
from pydantic import ValidationError
from pydantic import ValidationError, TypeAdapter
from sqlmodel import Session
from tools.enum_definitions import Status
from database.bot_crud import BotTableCrud
from deals.controllers import CreateDealController
from database.models.bot_table import BotBase, BotTable
from database.utils import get_session
from tools.handle_error import (
api_response,
from bots.models import (
BotModel,
BotResponse,
ErrorsRequestBody,
BotBase,
BotListResponse,
IResponseBase,
ActivePairsResponse,
)
from bots.models import BotModel, BotResponse, BotListResponse, ErrorsRequestBody
from typing import List
from tools.exceptions import BinanceErrors, BinbotErrors


bot_blueprint = APIRouter()


Expand All @@ -31,44 +34,64 @@ def get(
bots = BotTableCrud(session=session).get(
status, start_date, end_date, no_cooldown, limit, offset
)
return api_response(detail="Bots found", data=bots)
# Has to be converted to BotModel to
# be able to serialize nested objects
ta = TypeAdapter(List[BotModel])
data = ta.dump_python(bots)
return BotListResponse[List](message="Successfully found bots!", data=data)
except ValidationError as error:
return api_response(detail=error.json(), error=1)
return BotResponse(message="Failed to find bots!", data=error.json(), error=1)


@bot_blueprint.get("/bot/active-pairs", tags=["bots"])
@bot_blueprint.get(
"/bot/active-pairs", response_model=ActivePairsResponse, tags=["bots"]
)
def get_active_pairs(
session: Session = Depends(get_session),
):
try:
bot = BotTableCrud(session=session).get_active_pairs()
return api_response(detail="Active pairs found!", data=bot)
except ValueError as error:
return api_response(detail=f"Error retrieving active pairs: {error}", error=1)
if not bot:
return BotResponse(message="Bot not found.", error=1)
else:
ta = TypeAdapter(BotModel)
data = ta.dump_python(bot)
return ActivePairsResponse(
message="Successfully retrieved active pairs.", data=data
)

except ValidationError as error:
return BotResponse(
data=error.json(), error=1, message="Failed to find active pairs."
)


@bot_blueprint.get("/bot/{id}", tags=["bots"])
@bot_blueprint.get("/bot/{id}", response_model=BotResponse, tags=["bots"])
def get_one_by_id(id: str, session: Session = Depends(get_session)):
try:
bot = BotTableCrud(session=session).get_one(bot_id=id)
if not bot:
return api_response(detail="Bot not found.", error=1)
return BotResponse(message="Bot not found.", error=1)
else:
return api_response(detail="Bot found", data=bot)
ta = TypeAdapter(BotModel)
data = ta.dump_python(bot)
return BotResponse(message="Successfully found one bot.", data=data)
except ValidationError as error:
return api_response(error.json())
return BotResponse(message="Bot not found.", error=1, data=error.json())


@bot_blueprint.get("/bot/symbol/{symbol}", tags=["bots"])
def get_one_by_symbol(symbol: str, session: Session = Depends(get_session)):
try:
bot = BotTableCrud(session=session).get_one(bot_id=None, symbol=symbol)
if not bot:
return api_response(detail="Bot not found.", error=1)
return BotResponse(message="Bot not found.", error=1)
else:
return api_response(detail="Bot found", data=bot)
ta = TypeAdapter(BotModel)
data = ta.dump_python(bot)
return BotResponse(message="Successfully found one bot.", data=data)
except ValidationError as error:
return api_response(error.json())
return BotResponse(message="Bot not found.", error=1, data=error.json())


@bot_blueprint.post("/bot", tags=["bots"], response_model=BotResponse)
Expand All @@ -77,10 +100,14 @@ def create(
session: Session = Depends(get_session),
):
try:
data = BotTableCrud(session=session).create(bot_item)
return api_response(detail="Bot created", data=data)
bot = BotTableCrud(session=session).create(bot_item)
ta = TypeAdapter(BotModel)
data = ta.dump_python(bot)
return BotResponse(message="Successfully created one bot.", data=data)
except ValidationError as error:
return api_response(detail=error.json(), error=1)
return BotResponse(
message="Failed to create new bot", data=error.json(), error=1
)


@bot_blueprint.put("/bot/{id}", tags=["bots"])
Expand All @@ -90,10 +117,13 @@ def edit(
session: Session = Depends(get_session),
):
try:
bot_item.id = id
bot = BotTableCrud(session=session).save(bot_item)
return api_response(detail="Bot updated", data=bot)
ta = TypeAdapter(BotModel)
data = ta.dump_python(bot)
return BotResponse(message="Sucessfully edited bot", data=data)
except ValidationError as error:
return api_response(detail=error.json(), error=1)
return BotResponse(message="Failed to edit bot", data=error.json(), error=1)


@bot_blueprint.delete("/bot", tags=["bots"])
Expand All @@ -106,13 +136,13 @@ def delete(
"""
try:
BotTableCrud(session=session).delete(id)
return api_response(detail="Bots deleted successfully.")
return IResponseBase(message="Sucessfully deleted bot.")
except ValidationError as error:
return api_response(error.json())
return BotResponse(message="Failed to delete bot", data=error.json(), error=1)


@bot_blueprint.get("/bot/activate/{id}", tags=["bots"])
async def activate_by_id(id: str, session: Session = Depends(get_session)):
def activate_by_id(id: str, session: Session = Depends(get_session)):
"""
Activate bot
Expand All @@ -122,41 +152,42 @@ async def activate_by_id(id: str, session: Session = Depends(get_session)):
"""
bot = BotTableCrud(session=session).get_one(bot_id=id)
if not bot:
return api_response(detail="Bot not found.")
return BotResponse(message="Bot not found.")

bot_instance = CreateDealController(bot)
bot_model = BotModel.model_construct(**bot.model_dump())
bot_instance = CreateDealController(bot_model)

try:
data = bot_instance.open_deal()
return api_response(detail="Successfully activated bot!", data=data)
return BotResponse(message="Successfully activated bot!", data=data)
except BinbotErrors as error:
bot_instance.controller.update_logs(bot_id=id, log_message=error.message)
return api_response(detail=error.message, error=1)
return BotResponse(message=error.message, error=1)
except BinanceErrors as error:
bot_instance.controller.update_logs(bot_id=id, log_message=error.message)
return api_response(detail=error.message, error=1)
return BotResponse(message=error.message, error=1)


@bot_blueprint.delete("/bot/deactivate/{id}", tags=["bots"])
@bot_blueprint.delete("/bot/deactivate/{id}", response_model=BotResponse, tags=["bots"])
def deactivation(id: str, session: Session = Depends(get_session)):
"""
Deactivation means closing all deals and selling to
fiat. This is often used to prevent losses
"""
bot_model = BotTableCrud(session=session).get_one(bot_id=id)
if not bot_model:
return api_response(detail="No active bot found.")
bot_table = BotTableCrud(session=session).get_one(bot_id=id)
if not bot_table:
return BotResponse(message="No active bot found.")

bot_model = BotModel.model_construct(**bot_table.model_dump())
deal_instance = CreateDealController(bot_model)
try:
deal_instance.close_all()
return api_response(detail="Active orders closed, sold base asset, deactivated")
data = deal_instance.close_all()
return BotResponse(message="Active orders closed, sold base asset, deactivated", data=data)
except BinbotErrors as error:
deal_instance.controller.update_logs(bot_id=id, log_message=error.message)
return api_response(error.message)
return BotResponse(message=error.message, error=1)


@bot_blueprint.post("/bot/errors/{bot_id}", tags=["bots"])
@bot_blueprint.post("/bot/errors/{bot_id}", response_model=BotResponse, tags=["bots"])
def bot_errors(
bot_id: str, bot_errors: ErrorsRequestBody, session: Session = Depends(get_session)
):
Expand All @@ -172,6 +203,7 @@ def bot_errors(
bot = BotTableCrud(session=session).update_logs(
log_message=errors, bot_id=bot_id
)
return api_response(detail="Errors posted successfully.", data=bot)
except Exception as error:
return api_response(f"Error posting errors: {error}", error=1)
data = BotModel.model_construct(**bot.model_dump())
return BotResponse(message="Errors posted successfully.", data=data)
except ValidationError as error:
return BotResponse(message="Failed to post errors", data=error.json(), error=1)
Loading

0 comments on commit 1bc0443

Please sign in to comment.