AlgoQuant / manual_trading.py
saadrizvi09
Add Market feature with manual trading and P&L tracking, rename Live Trading to Trading Bot
a4a7fb9
"""
Manual Trading Service
Handles manual buy/sell operations for the Market page
Operates independently from automated trading bot strategies
"""
from typing import Optional, Tuple, List
from datetime import datetime
from sqlmodel import Session, select
from database import engine
from models import PortfolioAsset, Trade
import uuid
# Trading fee (0.1% as typical exchange fee)
TRADING_FEE = 0.001
# Supported trading pairs for manual trading
SUPPORTED_ASSETS = ["BTC", "ETH", "SOL", "LINK", "DOGE", "BNB"]
def get_current_price_from_binance(symbol: str, quote: str = "USDT") -> Optional[float]:
"""
Fetch current market price from Binance API
Args:
symbol: Base asset (e.g., 'BTC', 'ETH')
quote: Quote asset (e.g., 'USDT')
Returns:
Current price or None if error
"""
try:
from binance.client import Client
import os
from dotenv import load_dotenv
load_dotenv()
api_key = os.getenv("BINANCE_API_KEY", "")
api_secret = os.getenv("BINANCE_SECRET_KEY", "")
client = Client(api_key, api_secret, testnet=True)
trading_pair = f"{symbol}{quote}"
ticker = client.get_symbol_ticker(symbol=trading_pair)
return float(ticker['price'])
except Exception as e:
print(f"[ManualTrading] Binance fetch failed for {symbol}/{quote}: {e}")
# Fallback to Yahoo Finance
try:
import yfinance as yf
ticker_symbol = f"{symbol}-USD"
ticker = yf.Ticker(ticker_symbol)
price_data = ticker.history(period="1d", interval="1m")
if not price_data.empty:
return float(price_data['Close'].iloc[-1])
return None
except Exception as yf_error:
print(f"[ManualTrading] Yahoo Finance fallback failed: {yf_error}")
return None
def get_user_balance(symbol: str, user_email: str) -> float:
"""
Get current balance of an asset from user's portfolio
Args:
symbol: Asset symbol (e.g., 'USDT', 'BTC')
user_email: User identifier
Returns:
Current balance (0.0 if asset not found)
"""
with Session(engine) as session:
statement = select(PortfolioAsset).where(
PortfolioAsset.symbol == symbol,
PortfolioAsset.user_email == user_email
)
asset = session.exec(statement).first()
return asset.balance if asset else 0.0
def get_asset_cost_basis(symbol: str, user_email: str) -> dict:
"""
Get the average cost basis and investment info for an asset
Args:
symbol: Asset symbol (e.g., 'BTC', 'ETH')
user_email: User identifier
Returns:
Dict with avg_cost_basis, total_invested, and balance
"""
with Session(engine) as session:
statement = select(PortfolioAsset).where(
PortfolioAsset.symbol == symbol,
PortfolioAsset.user_email == user_email
)
asset = session.exec(statement).first()
if asset:
return {
'symbol': symbol,
'balance': asset.balance,
'avg_cost_basis': getattr(asset, 'avg_cost_basis', 0.0) or 0.0,
'total_invested': getattr(asset, 'total_invested', 0.0) or 0.0
}
return {
'symbol': symbol,
'balance': 0.0,
'avg_cost_basis': 0.0,
'total_invested': 0.0
}
def record_trade(
session: Session,
user_email: str,
symbol: str,
side: str,
price: float,
quantity: float,
total: float,
fee: float = 0.0,
pnl: Optional[float] = None
) -> Trade:
"""
Record a trade in the database
Args:
session: Database session
user_email: User identifier
symbol: Trading pair (e.g., 'BTCUSDT')
side: 'BUY' or 'SELL'
price: Execution price
quantity: Amount traded
total: Total value in quote currency
fee: Trading fee
pnl: Profit/Loss (for SELL trades)
Returns:
Created Trade object
"""
trade = Trade(
session_id=f"manual_{uuid.uuid4().hex[:8]}",
user_email=user_email,
symbol=symbol,
side=side,
price=price,
quantity=quantity,
total=total,
pnl=pnl,
order_id=f"MANUAL_{uuid.uuid4().hex[:12].upper()}",
executed_at=datetime.now()
)
session.add(trade)
return trade
def execute_manual_buy(
symbol: str,
usdt_amount: float,
user_email: str,
current_price: Optional[float] = None
) -> Tuple[bool, Optional[dict], Optional[str]]:
"""
Execute a manual BUY order
Args:
symbol: Asset to buy (e.g., 'BTC', 'ETH')
usdt_amount: Amount in USDT to spend
user_email: User identifier
current_price: Optional price (fetched if not provided)
Returns:
Tuple of (success: bool, trade_info: dict or None, error_message: str or None)
"""
# Validate symbol
if symbol.upper() not in SUPPORTED_ASSETS:
return False, None, f"Asset {symbol} is not supported for manual trading"
symbol = symbol.upper()
# Get current market price if not provided
price = current_price or get_current_price_from_binance(symbol, "USDT")
if price is None:
return False, None, f"Could not fetch price for {symbol}/USDT"
# Calculate quantity to buy and fees
fee = usdt_amount * TRADING_FEE
usdt_after_fee = usdt_amount - fee
quantity_to_buy = usdt_after_fee / price
# Check if user has enough USDT
usdt_balance = get_user_balance("USDT", user_email)
if usdt_balance < usdt_amount:
return False, None, f"Insufficient USDT balance. Required: {usdt_amount:.2f}, Available: {usdt_balance:.2f}"
# Execute trade in database transaction
try:
with Session(engine) as session:
# Deduct USDT
usdt_stmt = select(PortfolioAsset).where(
PortfolioAsset.symbol == "USDT",
PortfolioAsset.user_email == user_email
)
usdt_asset = session.exec(usdt_stmt).first()
usdt_asset.balance -= usdt_amount
session.add(usdt_asset)
# Add purchased asset and update cost basis
asset_stmt = select(PortfolioAsset).where(
PortfolioAsset.symbol == symbol,
PortfolioAsset.user_email == user_email
)
asset = session.exec(asset_stmt).first()
if asset:
# Calculate new weighted average cost basis
old_balance = asset.balance
old_total_invested = getattr(asset, 'total_invested', 0.0) or 0.0
new_total_invested = old_total_invested + usdt_amount
new_balance = old_balance + quantity_to_buy
# Weighted average: (old_invested + new_invested) / new_total_quantity
asset.avg_cost_basis = new_total_invested / new_balance if new_balance > 0 else 0.0
asset.total_invested = new_total_invested
asset.balance = new_balance
session.add(asset)
else:
new_asset = PortfolioAsset(
symbol=symbol,
balance=quantity_to_buy,
user_email=user_email,
avg_cost_basis=usdt_amount / quantity_to_buy if quantity_to_buy > 0 else 0.0,
total_invested=usdt_amount
)
session.add(new_asset)
asset = new_asset
# Record the trade
trade = record_trade(
session=session,
user_email=user_email,
symbol=f"{symbol}USDT",
side="BUY",
price=price,
quantity=quantity_to_buy,
total=usdt_amount,
fee=fee
)
session.commit()
trade_info = {
'order_id': trade.order_id,
'symbol': f"{symbol}USDT",
'side': 'BUY',
'price': price,
'quantity': quantity_to_buy,
'usdt_spent': usdt_amount,
'fee': fee,
'net_quantity': quantity_to_buy,
'executed_at': trade.executed_at.isoformat(),
'new_balance': {
'USDT': usdt_asset.balance,
symbol: asset.balance if asset else quantity_to_buy
}
}
print(f"[ManualTrading] ✅ BUY executed: {quantity_to_buy:.8f} {symbol} @ ${price:.2f}")
print(f" Spent: ${usdt_amount:.2f} USDT (Fee: ${fee:.4f})")
return True, trade_info, None
except Exception as e:
print(f"[ManualTrading] ❌ BUY transaction failed: {e}")
return False, None, f"Transaction failed: {str(e)}"
def execute_manual_sell(
symbol: str,
quantity: float,
user_email: str,
current_price: Optional[float] = None
) -> Tuple[bool, Optional[dict], Optional[str]]:
"""
Execute a manual SELL order
Args:
symbol: Asset to sell (e.g., 'BTC', 'ETH')
quantity: Amount of asset to sell
user_email: User identifier
current_price: Optional price (fetched if not provided)
Returns:
Tuple of (success: bool, trade_info: dict or None, error_message: str or None)
"""
# Validate symbol
if symbol.upper() not in SUPPORTED_ASSETS:
return False, None, f"Asset {symbol} is not supported for manual trading"
symbol = symbol.upper()
# Get current market price if not provided
price = current_price or get_current_price_from_binance(symbol, "USDT")
if price is None:
return False, None, f"Could not fetch price for {symbol}/USDT"
# Check if user has enough of the asset to sell
asset_balance = get_user_balance(symbol, user_email)
if asset_balance < quantity:
return False, None, f"Insufficient {symbol} balance. Required: {quantity:.8f}, Available: {asset_balance:.8f}"
# Calculate proceeds after fee
gross_proceeds = price * quantity
fee = gross_proceeds * TRADING_FEE
net_proceeds = gross_proceeds - fee
# Execute trade in database transaction
try:
with Session(engine) as session:
# Deduct sold asset and calculate PnL
asset_stmt = select(PortfolioAsset).where(
PortfolioAsset.symbol == symbol,
PortfolioAsset.user_email == user_email
)
asset = session.exec(asset_stmt).first()
# Calculate PnL based on cost basis
avg_cost_basis = getattr(asset, 'avg_cost_basis', 0.0) or 0.0
total_invested = getattr(asset, 'total_invested', 0.0) or 0.0
# Cost of the sold portion
cost_of_sold = avg_cost_basis * quantity
# PnL = What we received - What we paid (including fees)
pnl = net_proceeds - cost_of_sold
pnl_percent = ((net_proceeds / cost_of_sold) - 1) * 100 if cost_of_sold > 0 else 0.0
# Update asset balance and total invested
old_balance = asset.balance
new_balance = old_balance - quantity
# Proportionally reduce total invested
if old_balance > 0:
proportion_sold = quantity / old_balance
invested_sold = total_invested * proportion_sold
asset.total_invested = total_invested - invested_sold
else:
asset.total_invested = 0.0
asset.balance = new_balance
# Keep avg_cost_basis the same (it's still relevant for remaining holdings)
session.add(asset)
# Add USDT proceeds
usdt_stmt = select(PortfolioAsset).where(
PortfolioAsset.symbol == "USDT",
PortfolioAsset.user_email == user_email
)
usdt_asset = session.exec(usdt_stmt).first()
if usdt_asset:
usdt_asset.balance += net_proceeds
session.add(usdt_asset)
else:
new_usdt = PortfolioAsset(
symbol="USDT",
balance=net_proceeds,
user_email=user_email
)
session.add(new_usdt)
# Record the trade with PnL
trade = record_trade(
session=session,
user_email=user_email,
symbol=f"{symbol}USDT",
side="SELL",
price=price,
quantity=quantity,
total=net_proceeds,
fee=fee,
pnl=pnl
)
session.commit()
trade_info = {
'order_id': trade.order_id,
'symbol': f"{symbol}USDT",
'side': 'SELL',
'price': price,
'quantity': quantity,
'gross_proceeds': gross_proceeds,
'fee': fee,
'net_proceeds': net_proceeds,
'avg_cost_basis': avg_cost_basis,
'cost_of_sold': cost_of_sold,
'pnl': pnl,
'pnl_percent': pnl_percent,
'executed_at': trade.executed_at.isoformat(),
'new_balance': {
'USDT': usdt_asset.balance if usdt_asset else net_proceeds,
symbol: asset.balance
}
}
pnl_emoji = "📈" if pnl >= 0 else "📉"
print(f"[ManualTrading] ✅ SELL executed: {quantity:.8f} {symbol} @ ${price:.2f}")
print(f" Received: ${net_proceeds:.2f} USDT (Fee: ${fee:.4f})")
print(f" {pnl_emoji} PnL: ${pnl:.2f} ({pnl_percent:+.2f}%)")
return True, trade_info, None
except Exception as e:
print(f"[ManualTrading] ❌ SELL transaction failed: {e}")
return False, None, f"Transaction failed: {str(e)}"
def get_manual_trade_history(user_email: str, limit: int = 50) -> List[dict]:
"""
Get manual trade history for a user
Args:
user_email: User identifier
limit: Maximum number of trades to return
Returns:
List of trade dictionaries
"""
with Session(engine) as session:
statement = select(Trade).where(
Trade.user_email == user_email,
Trade.session_id.startswith("manual_")
).order_by(Trade.executed_at.desc()).limit(limit)
trades = session.exec(statement).all()
result = []
for trade in trades:
# Calculate pnl_percent for sell trades
pnl_percent = None
if trade.side == "SELL" and trade.pnl is not None:
# cost_basis = total - pnl (what we got minus profit = what we paid)
cost_basis = trade.total - trade.pnl
if cost_basis > 0:
pnl_percent = (trade.pnl / cost_basis) * 100
result.append({
'id': trade.id,
'order_id': trade.order_id,
'symbol': trade.symbol,
'side': trade.side,
'price': trade.price,
'quantity': trade.quantity,
'total': trade.total,
'pnl': trade.pnl,
'pnl_percent': pnl_percent,
'time': trade.executed_at.isoformat() if trade.executed_at else None
})
return result
def get_prices_for_assets(assets: List[str] = None) -> dict:
"""
Get current prices for multiple assets
Args:
assets: List of asset symbols (defaults to SUPPORTED_ASSETS)
Returns:
Dictionary mapping symbol to price data
"""
if assets is None:
assets = SUPPORTED_ASSETS
prices = {}
for asset in assets:
price = get_current_price_from_binance(asset, "USDT")
if price:
prices[asset] = {
'symbol': asset,
'price': price,
'pair': f"{asset}USDT"
}
return prices
# ============================================
# PLACEHOLDER: Real Binance Order Execution
# ============================================
def execute_binance_order(
symbol: str,
side: str, # 'BUY' or 'SELL'
order_type: str, # 'MARKET', 'LIMIT'
quantity: float,
price: Optional[float] = None, # Required for LIMIT orders
api_key: Optional[str] = None,
api_secret: Optional[str] = None
) -> Tuple[bool, Optional[dict], Optional[str]]:
"""
PLACEHOLDER: Execute a real order on Binance exchange
This function is a placeholder for when you want to execute
real orders on Binance. Uncomment and configure the code below.
Args:
symbol: Trading pair (e.g., 'BTCUSDT')
side: 'BUY' or 'SELL'
order_type: 'MARKET' or 'LIMIT'
quantity: Amount to trade
price: Limit price (required for LIMIT orders)
api_key: Binance API key
api_secret: Binance API secret
Returns:
Tuple of (success: bool, order_info: dict or None, error_message: str or None)
"""
# ===== UNCOMMENT THIS CODE TO ENABLE REAL TRADING =====
#
# from binance.client import Client
# from binance.exceptions import BinanceAPIException
# import os
# from dotenv import load_dotenv
#
# load_dotenv()
#
# # Use provided keys or fall back to environment variables
# key = api_key or os.getenv("BINANCE_API_KEY")
# secret = api_secret or os.getenv("BINANCE_SECRET_KEY")
#
# if not key or not secret:
# return False, None, "Binance API credentials not configured"
#
# try:
# client = Client(key, secret)
#
# if order_type == 'MARKET':
# if side == 'BUY':
# order = client.create_order(
# symbol=symbol,
# side=Client.SIDE_BUY,
# type=Client.ORDER_TYPE_MARKET,
# quantity=quantity
# )
# else:
# order = client.create_order(
# symbol=symbol,
# side=Client.SIDE_SELL,
# type=Client.ORDER_TYPE_MARKET,
# quantity=quantity
# )
# elif order_type == 'LIMIT':
# if price is None:
# return False, None, "Price required for LIMIT orders"
#
# if side == 'BUY':
# order = client.create_order(
# symbol=symbol,
# side=Client.SIDE_BUY,
# type=Client.ORDER_TYPE_LIMIT,
# timeInForce=Client.TIME_IN_FORCE_GTC,
# quantity=quantity,
# price=str(price)
# )
# else:
# order = client.create_order(
# symbol=symbol,
# side=Client.SIDE_SELL,
# type=Client.ORDER_TYPE_LIMIT,
# timeInForce=Client.TIME_IN_FORCE_GTC,
# quantity=quantity,
# price=str(price)
# )
#
# return True, order, None
#
# except BinanceAPIException as e:
# return False, None, f"Binance API Error: {e.message}"
# except Exception as e:
# return False, None, f"Order execution failed: {str(e)}"
#
# ===== END OF REAL TRADING CODE =====
# For now, return a message indicating this is a placeholder
return False, None, "Real Binance trading not enabled. Using simulated trading."