import json import logging import os import time import config logger = logging.getLogger(__name__) def load_positions() -> dict: """Load positions from positions.json.""" if not os.path.exists(config.POSITIONS_FILE): return _empty_portfolio() try: with open(config.POSITIONS_FILE) as f: data = json.load(f) return data except (json.JSONDecodeError, IOError) as e: logger.error("Failed to load positions: %s", e) return _empty_portfolio() def save_positions(portfolio: dict): """Save portfolio state to positions.json.""" with open(config.POSITIONS_FILE, "w") as f: json.dump(portfolio, f, indent=2) def _empty_portfolio() -> dict: return { "total_balance_usdt": 0, "available_usdt": 0, "positions": {}, "trade_history": [], "last_updated": time.time(), } def update_position(portfolio: dict, symbol: str, action: str, amount: float, price: float, amount_usdt: float) -> dict: """Update portfolio after a trade execution.""" positions = portfolio.setdefault("positions", {}) if action == "BUY": existing = positions.get(symbol, {"amount": 0, "entry_price": 0, "value_usdt": 0}) old_amount = existing.get("amount", 0) old_entry = existing.get("entry_price", 0) new_amount = old_amount + amount # Weighted average entry price if new_amount > 0: new_entry = (old_amount * old_entry + amount * price) / new_amount else: new_entry = price positions[symbol] = { "amount": new_amount, "entry_price": new_entry, "value_usdt": new_amount * price, "last_update": time.time(), } portfolio["available_usdt"] = portfolio.get("available_usdt", 0) - amount_usdt elif action == "SELL": existing = positions.get(symbol, {}) old_amount = existing.get("amount", 0) sell_amount = min(amount, old_amount) remaining = old_amount - sell_amount if remaining <= 0.000001: positions.pop(symbol, None) else: positions[symbol] = { "amount": remaining, "entry_price": existing.get("entry_price", price), "value_usdt": remaining * price, "last_update": time.time(), } portfolio["available_usdt"] = portfolio.get("available_usdt", 0) + sell_amount * price # Record trade history = portfolio.setdefault("trade_history", []) history.append({ "symbol": symbol, "action": action, "amount": amount, "price": price, "amount_usdt": amount_usdt, "timestamp": time.time(), }) if len(history) > 500: portfolio["trade_history"] = history[-500:] portfolio["last_updated"] = time.time() return portfolio def calculate_pnl(portfolio: dict, current_prices: dict) -> dict: """Calculate unrealized P&L for all positions.""" pnl = {} for sym, pos in portfolio.get("positions", {}).items(): amount = pos.get("amount", 0) entry = pos.get("entry_price", 0) current = current_prices.get(sym, 0) if amount > 0 and entry > 0 and current > 0: unrealized = (current - entry) * amount unrealized_pct = (current - entry) / entry pnl[sym] = { "amount": amount, "entry_price": entry, "current_price": current, "unrealized_usdt": unrealized, "unrealized_pct": unrealized_pct, "value_usdt": amount * current, } return pnl def get_portfolio_summary(portfolio: dict, current_prices: dict) -> str: """Generate a human-readable portfolio summary.""" pnl = calculate_pnl(portfolio, current_prices) total_value = portfolio.get("available_usdt", 0) total_unrealized = 0 lines = ["📊 *Portfolio Summary*"] lines.append(f"Available USDT: {portfolio.get('available_usdt', 0):.2f}") lines.append("") if pnl: lines.append("*Open Positions:*") for sym, p in pnl.items(): name = config.SYMBOL_NAMES.get(sym, sym) emoji = "🟢" if p["unrealized_pct"] >= 0 else "🔴" lines.append( f"{emoji} {name}: {p['amount']:.6f} @ {p['entry_price']:.6g} → " f"{p['current_price']:.6g} ({p['unrealized_pct']:+.2%}) " f"= {p['unrealized_usdt']:+.2f} USDT" ) total_value += p["value_usdt"] total_unrealized += p["unrealized_usdt"] lines.append("") lines.append(f"*Total Portfolio Value:* {total_value:.2f} USDT") cost_basis = total_value - total_unrealized if cost_basis > 0: lines.append(f"*Total Unrealized P&L:* {total_unrealized:+.2f} USDT ({total_unrealized / cost_basis * 100:+.2f}%)") else: lines.append(f"*Total Unrealized P&L:* {total_unrealized:+.2f} USDT") initial_capital = portfolio.get("initial_capital", 0) if initial_capital > 0: total_return = total_value - initial_capital total_return_pct = (total_value / initial_capital - 1) * 100 sign = "+" if total_return >= 0 else "" lines.append(f"*Total Return:* {sign}{total_return:.2f} USDT ({sign}{total_return_pct:.2f}%)") history = portfolio.get("trade_history", []) if history: lines.append(f"\n*Trades today:* {len(history)}") return "\n".join(lines) def sync_with_exchange(portfolio: dict, account_status: dict) -> dict: """Sync portfolio with exchange account status.""" wallets = account_status.get("wallets", []) for w in wallets: if w.get("type") == "exchange" and w.get("currency") in ("UST", "USDT"): portfolio["total_balance_usdt"] = w.get("balance", 0) portfolio["available_usdt"] = w.get("available", 0) or w.get("balance", 0) break # Sync exchange positions for p in account_status.get("positions", []): sym = p.get("symbol", "") if sym and p.get("amount", 0) != 0: portfolio.setdefault("positions", {})[sym] = { "amount": abs(p["amount"]), "entry_price": p.get("base_price", 0), "value_usdt": abs(p["amount"]) * p.get("base_price", 0), "last_update": time.time(), "source": "exchange", } portfolio["last_updated"] = time.time() return portfolio