{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Session 5: Event-Driven Trading Bot\n", "\n", "## Notebook 3: Portfolio Management & Risk Controls\n", "\n", "**Learning Objectives:**\n", "- Build a real-time Portfolio Manager (the \"Keeper\")\n", "- Implement comprehensive Risk Controls\n", "- Track positions, balances, and P&L in real-time\n", "- Apply risk management rules (position sizing, daily limits)\n", "- Integrate portfolio and risk systems with OMS\n", "\n", "**Why This Matters:**\n", "The Portfolio Manager is your \"financial brain\" - it knows exactly what you own, what it's worth, and how much you've made or lost. Risk Controls are your \"safety net\" - they prevent catastrophic losses by enforcing rules. Together, they keep your trading bot profitable and safe.\n", "\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Quick Setup: Import Previous Systems\n", "\n", "Let's import the event system and OMS from our previous notebooks:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Import our systems from previous notebooks\n", "from datetime import datetime, timedelta\n", "from typing import Dict, Any, List, Callable, Optional\n", "from dataclasses import dataclass, field\n", "from collections import deque, defaultdict\n", "from threading import Lock\n", "from enum import Enum\n", "import json\n", "import uuid\n", "import time\n", "\n", "# Event System (from Notebook 1)\n", "@dataclass\n", "class Event:\n", " event_type: str\n", " data: Dict[str, Any]\n", " timestamp: datetime = None\n", " source: str = \"unknown\"\n", " \n", " def __post_init__(self):\n", " if self.timestamp is None:\n", " self.timestamp = datetime.now()\n", "\n", "class EventQueue:\n", " def __init__(self):\n", " self.events = deque()\n", " self.subscribers = {}\n", " self.lock = Lock()\n", " self.event_history = []\n", " \n", " def subscribe(self, event_type: str, callback: Callable[[Event], None]):\n", " with self.lock:\n", " if event_type not in self.subscribers:\n", " self.subscribers[event_type] = []\n", " self.subscribers[event_type].append(callback)\n", " \n", " def publish(self, event: Event):\n", " with self.lock:\n", " self.events.append(event)\n", " self.event_history.append(event)\n", " \n", " if event.event_type in self.subscribers:\n", " for callback in self.subscribers[event.event_type]:\n", " try:\n", " callback(event)\n", " except Exception as e:\n", " print(f\"โŒ Error in callback for {event.event_type}: {e}\")\n", "\n", "# Order System (from Notebook 2) - simplified\n", "class OrderState(Enum):\n", " PENDING = \"PENDING\"\n", " FILLED = \"FILLED\"\n", " CANCELLED = \"CANCELLED\"\n", "\n", "class OrderSide(Enum):\n", " BUY = \"BUY\"\n", " SELL = \"SELL\"\n", "\n", "@dataclass\n", "class Order:\n", " symbol: str\n", " side: OrderSide\n", " quantity: float\n", " price: float\n", " order_id: str = field(default_factory=lambda: str(uuid.uuid4()))\n", " state: OrderState = OrderState.PENDING\n", " filled_quantity: float = 0.0\n", " average_fill_price: float = 0.0\n", " fees_paid: float = 0.0\n", " created_at: datetime = field(default_factory=datetime.now)\n", "\n", "# Create our event queue\n", "event_queue = EventQueue()\n", "print(\"๐Ÿ—๏ธ Event system and Order classes ready for Portfolio & Risk integration!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 1: Position and Portfolio Data Structures\n", "\n", "Let's create the core data structures to track positions and portfolio state:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@dataclass\n", "class Position:\n", " \"\"\"Represents a position in a specific symbol\"\"\"\n", " symbol: str\n", " quantity: float = 0.0 # Current position size (+ for long, - for short)\n", " average_price: float = 0.0 # Average cost basis\n", " market_price: float = 0.0 # Current market price\n", " total_cost: float = 0.0 # Total amount invested\n", " realized_pnl: float = 0.0 # Profit/Loss from closed positions\n", " fees_paid: float = 0.0 # Total fees paid\n", " last_updated: datetime = field(default_factory=datetime.now)\n", " \n", " @property\n", " def market_value(self) -> float:\n", " \"\"\"Current market value of position\"\"\"\n", " return abs(self.quantity) * self.market_price\n", " \n", " @property\n", " def unrealized_pnl(self) -> float:\n", " \"\"\"Unrealized profit/loss\"\"\"\n", " if self.quantity == 0:\n", " return 0.0\n", " \n", " if self.quantity > 0: # Long position\n", " return (self.market_price - self.average_price) * self.quantity\n", " else: # Short position\n", " return (self.average_price - self.market_price) * abs(self.quantity)\n", " \n", " @property\n", " def total_pnl(self) -> float:\n", " \"\"\"Total P&L (realized + unrealized)\"\"\"\n", " return self.realized_pnl + self.unrealized_pnl\n", " \n", " @property\n", " def pnl_percentage(self) -> float:\n", " \"\"\"P&L as percentage of invested capital\"\"\"\n", " if self.total_cost == 0:\n", " return 0.0\n", " return (self.total_pnl / self.total_cost) * 100\n", " \n", " def update_market_price(self, new_price: float):\n", " \"\"\"Update current market price\"\"\"\n", " self.market_price = new_price\n", " self.last_updated = datetime.now()\n", " \n", " def add_trade(self, quantity: float, price: float, fees: float = 0.0):\n", " \"\"\"Add a trade to this position\"\"\"\n", " trade_value = quantity * price\n", " \n", " if self.quantity == 0:\n", " # Opening new position\n", " self.quantity = quantity\n", " self.average_price = price\n", " self.total_cost = abs(trade_value)\n", " else:\n", " # Modifying existing position\n", " if (self.quantity > 0 and quantity > 0) or (self.quantity < 0 and quantity < 0):\n", " # Adding to position\n", " total_value = (self.quantity * self.average_price) + trade_value\n", " self.quantity += quantity\n", " self.average_price = total_value / self.quantity if self.quantity != 0 else 0\n", " self.total_cost += abs(trade_value)\n", " else:\n", " # Reducing or closing position\n", " if abs(quantity) >= abs(self.quantity):\n", " # Closing position completely\n", " self.realized_pnl += (price - self.average_price) * self.quantity\n", " self.quantity = 0\n", " self.average_price = 0\n", " else:\n", " # Partial close\n", " close_pnl = (price - self.average_price) * abs(quantity)\n", " self.realized_pnl += close_pnl\n", " self.quantity += quantity # quantity is negative for close\n", " \n", " self.fees_paid += fees\n", " self.last_updated = datetime.now()\n", " \n", " def to_dict(self) -> Dict[str, Any]:\n", " return {\n", " 'symbol': self.symbol,\n", " 'quantity': self.quantity,\n", " 'average_price': self.average_price,\n", " 'market_price': self.market_price,\n", " 'market_value': self.market_value,\n", " 'total_cost': self.total_cost,\n", " 'unrealized_pnl': self.unrealized_pnl,\n", " 'realized_pnl': self.realized_pnl,\n", " 'total_pnl': self.total_pnl,\n", " 'pnl_percentage': f\"{self.pnl_percentage:.2f}%\",\n", " 'fees_paid': self.fees_paid\n", " }\n", " \n", " def __str__(self):\n", " direction = \"LONG\" if self.quantity > 0 else \"SHORT\" if self.quantity < 0 else \"FLAT\"\n", " return f\"{direction} {abs(self.quantity):.4f} {self.symbol} @ ${self.average_price:.2f} (P&L: ${self.total_pnl:.2f})\"\n", "\n", "\n", "# Test Position class\n", "print(\"๐Ÿงช Testing Position class:\\n\")\n", "\n", "btc_position = Position(\"BTC/USDT\")\n", "print(f\"New position: {btc_position}\")\n", "\n", "# Add a buy trade\n", "btc_position.add_trade(0.1, 43000, 4.30) # Buy 0.1 BTC at $43,000\n", "btc_position.update_market_price(43500) # Market moves up\n", "print(f\"After buy: {btc_position}\")\n", "print(f\"Position details: {json.dumps(btc_position.to_dict(), indent=2, default=str)}\")\n", "\n", "# Partial sell\n", "btc_position.add_trade(-0.03, 43600, 1.31) # Sell 0.03 BTC at $43,600\n", "print(f\"\\nAfter partial sell: {btc_position}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 2: Portfolio Manager (The \"Keeper\")\n", "\n", "Now let's build the Portfolio Manager that tracks all positions and overall account health:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class PortfolioManager:\n", " \"\"\"The Keeper - manages all positions and portfolio state\"\"\"\n", " \n", " def __init__(self, event_queue: EventQueue, initial_balance: float = 100000.0):\n", " self.event_queue = event_queue\n", " self.name = \"portfolio_manager\"\n", " \n", " # Portfolio state\n", " self.positions: Dict[str, Position] = {} # symbol -> Position\n", " self.cash_balance = initial_balance\n", " self.initial_balance = initial_balance\n", " \n", " # Portfolio tracking\n", " self.total_trades = 0\n", " self.total_fees_paid = 0.0\n", " self.daily_pnl = 0.0\n", " self.max_drawdown = 0.0\n", " self.peak_value = initial_balance\n", " \n", " # Market prices (for position valuation)\n", " self.market_prices: Dict[str, float] = {}\n", " \n", " # Subscribe to relevant events\n", " self.event_queue.subscribe(\"order_fill\", self.on_order_fill)\n", " self.event_queue.subscribe(\"market_data\", self.on_market_data)\n", " \n", " print(f\"๐Ÿ’ผ {self.name}: Portfolio initialized with ${initial_balance:,.2f}\")\n", " \n", " def on_market_data(self, event: Event):\n", " \"\"\"Update market prices for position valuation\"\"\"\n", " symbol = event.data[\"symbol\"]\n", " price = event.data[\"price\"]\n", " \n", " self.market_prices[symbol] = price\n", " \n", " # Update position market prices\n", " if symbol in self.positions:\n", " old_pnl = self.positions[symbol].total_pnl\n", " self.positions[symbol].update_market_price(price)\n", " new_pnl = self.positions[symbol].total_pnl\n", " \n", " # Update daily P&L\n", " self.daily_pnl += (new_pnl - old_pnl)\n", " \n", " # Check for new peak/drawdown\n", " current_value = self.total_portfolio_value\n", " if current_value > self.peak_value:\n", " self.peak_value = current_value\n", " else:\n", " drawdown = (self.peak_value - current_value) / self.peak_value * 100\n", " self.max_drawdown = max(self.max_drawdown, drawdown)\n", " \n", " # Publish portfolio update event\n", " self._publish_portfolio_update(\"price_update\", f\"{symbol} price updated to ${price:,.2f}\")\n", " \n", " def on_order_fill(self, event: Event):\n", " \"\"\"Process order fills and update positions\"\"\"\n", " symbol = event.data[\"symbol\"]\n", " side = event.data[\"side\"]\n", " fill_quantity = event.data[\"fill_quantity\"]\n", " fill_price = event.data[\"fill_price\"]\n", " fees = event.data.get(\"fees\", 0.0)\n", " \n", " print(f\"๐Ÿ’ผ {self.name}: Processing fill - {side} {fill_quantity} {symbol} @ ${fill_price:,.2f}\")\n", " \n", " # Calculate trade quantity (negative for sells)\n", " trade_quantity = fill_quantity if side == \"BUY\" else -fill_quantity\n", " trade_value = fill_quantity * fill_price\n", " \n", " # Update cash balance\n", " if side == \"BUY\":\n", " self.cash_balance -= (trade_value + fees)\n", " else:\n", " self.cash_balance += (trade_value - fees)\n", " \n", " # Update or create position\n", " if symbol not in self.positions:\n", " self.positions[symbol] = Position(symbol)\n", " # Set initial market price if we have it\n", " if symbol in self.market_prices:\n", " self.positions[symbol].update_market_price(self.market_prices[symbol])\n", " \n", " # Add trade to position\n", " self.positions[symbol].add_trade(trade_quantity, fill_price, fees)\n", " \n", " # Update statistics\n", " self.total_trades += 1\n", " self.total_fees_paid += fees\n", " \n", " # Publish portfolio update\n", " message = f\"Position updated: {self.positions[symbol]}\"\n", " self._publish_portfolio_update(\"trade_executed\", message)\n", " \n", " print(f\" ๐Ÿ’ฐ Cash balance: ${self.cash_balance:,.2f}\")\n", " print(f\" ๐Ÿ“Š Position: {self.positions[symbol]}\")\n", " \n", " def _publish_portfolio_update(self, update_type: str, message: str):\n", " \"\"\"Publish portfolio update event\"\"\"\n", " portfolio_event = Event(\n", " event_type=\"portfolio_update\",\n", " data={\n", " \"update_type\": update_type,\n", " \"message\": message,\n", " \"portfolio_value\": self.total_portfolio_value,\n", " \"cash_balance\": self.cash_balance,\n", " \"total_pnl\": self.total_pnl,\n", " \"daily_pnl\": self.daily_pnl,\n", " \"positions\": {symbol: pos.to_dict() for symbol, pos in self.positions.items()}\n", " },\n", " source=self.name\n", " )\n", " self.event_queue.publish(portfolio_event)\n", " \n", " @property\n", " def total_portfolio_value(self) -> float:\n", " \"\"\"Total portfolio value (cash + positions)\"\"\"\n", " positions_value = sum(pos.market_value for pos in self.positions.values())\n", " return self.cash_balance + positions_value\n", " \n", " @property\n", " def total_pnl(self) -> float:\n", " \"\"\"Total profit/loss\"\"\"\n", " return self.total_portfolio_value - self.initial_balance\n", " \n", " @property\n", " def total_pnl_percentage(self) -> float:\n", " \"\"\"Total P&L as percentage\"\"\"\n", " return (self.total_pnl / self.initial_balance) * 100\n", " \n", " def get_position(self, symbol: str) -> Optional[Position]:\n", " \"\"\"Get position for a specific symbol\"\"\"\n", " return self.positions.get(symbol)\n", " \n", " def get_portfolio_summary(self) -> Dict[str, Any]:\n", " \"\"\"Get comprehensive portfolio summary\"\"\"\n", " return {\n", " \"cash_balance\": self.cash_balance,\n", " \"total_portfolio_value\": self.total_portfolio_value,\n", " \"total_pnl\": self.total_pnl,\n", " \"total_pnl_percentage\": f\"{self.total_pnl_percentage:.2f}%\",\n", " \"daily_pnl\": self.daily_pnl,\n", " \"max_drawdown\": f\"{self.max_drawdown:.2f}%\",\n", " \"total_trades\": self.total_trades,\n", " \"total_fees_paid\": self.total_fees_paid,\n", " \"open_positions\": len([p for p in self.positions.values() if p.quantity != 0]),\n", " \"positions\": {symbol: pos.to_dict() for symbol, pos in self.positions.items() if pos.quantity != 0}\n", " }\n", "\n", "# Create portfolio manager\n", "portfolio = PortfolioManager(event_queue, initial_balance=100000.0)\n", "print(f\"\\n๐Ÿ“Š Initial Portfolio Summary:\")\n", "print(json.dumps(portfolio.get_portfolio_summary(), indent=2, default=str))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 3: Risk Controller\n", "\n", "Now let's build the Risk Controller that enforces trading rules and protects our capital:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class RiskController:\n", " \"\"\"Risk management system - the safety net for our trading bot\"\"\"\n", " \n", " def __init__(self, event_queue: EventQueue, portfolio: PortfolioManager):\n", " self.event_queue = event_queue\n", " self.portfolio = portfolio\n", " self.name = \"risk_controller\"\n", " \n", " # Risk limits\n", " self.max_position_size_pct = 25.0 # Max 25% of portfolio in one position\n", " self.max_daily_loss_pct = 5.0 # Max 5% daily loss\n", " self.max_total_drawdown_pct = 15.0 # Max 15% total drawdown\n", " self.min_cash_reserve_pct = 10.0 # Keep at least 10% in cash\n", " self.max_leverage = 1.0 # No leverage for now\n", " \n", " # Risk tracking\n", " self.daily_start_value = portfolio.total_portfolio_value\n", " self.risk_violations = []\n", " self.blocked_orders = 0\n", " self.risk_warnings = 0\n", " \n", " # Subscribe to events that need risk checking\n", " self.event_queue.subscribe(\"order_request\", self.on_order_request)\n", " self.event_queue.subscribe(\"portfolio_update\", self.on_portfolio_update)\n", " \n", " print(f\"๐Ÿ›ก๏ธ {self.name}: Risk management initialized\")\n", " print(f\" Max position size: {self.max_position_size_pct}%\")\n", " print(f\" Max daily loss: {self.max_daily_loss_pct}%\")\n", " print(f\" Max drawdown: {self.max_total_drawdown_pct}%\")\n", " \n", " def on_order_request(self, event: Event):\n", " \"\"\"Check order requests against risk rules\"\"\"\n", " order = event.data[\"order\"]\n", " \n", " print(f\"๐Ÿ›ก๏ธ {self.name}: Evaluating order risk - {order}\")\n", " \n", " # Run all risk checks\n", " risk_checks = [\n", " self._check_position_size_limit(order),\n", " self._check_cash_availability(order),\n", " self._check_daily_loss_limit(),\n", " self._check_total_drawdown_limit(),\n", " self._check_minimum_cash_reserve(order)\n", " ]\n", " \n", " # Evaluate results\n", " violations = [check for check in risk_checks if not check[\"approved\"]]\n", " warnings = [check for check in risk_checks if check[\"approved\"] and check.get(\"warning\")]\n", " \n", " if violations:\n", " # Block the order\n", " self.blocked_orders += 1\n", " violation_messages = [v[\"message\"] for v in violations]\n", " \n", " risk_event = Event(\n", " event_type=\"risk_violation\",\n", " data={\n", " \"order_id\": order.order_id,\n", " \"violations\": violation_messages,\n", " \"severity\": \"CRITICAL\",\n", " \"action\": \"ORDER_BLOCKED\"\n", " },\n", " source=self.name\n", " )\n", " self.event_queue.publish(risk_event)\n", " \n", " print(f\" โŒ ORDER BLOCKED - Risk violations:\")\n", " for msg in violation_messages:\n", " print(f\" โ€ข {msg}\")\n", " \n", " self.risk_violations.extend(violation_messages)\n", " \n", " elif warnings:\n", " # Allow order but issue warnings\n", " self.risk_warnings += len(warnings)\n", " warning_messages = [w[\"message\"] for w in warnings]\n", " \n", " risk_event = Event(\n", " event_type=\"risk_warning\",\n", " data={\n", " \"order_id\": order.order_id,\n", " \"warnings\": warning_messages,\n", " \"severity\": \"WARNING\",\n", " \"action\": \"ORDER_APPROVED_WITH_WARNINGS\"\n", " },\n", " source=self.name\n", " )\n", " self.event_queue.publish(risk_event)\n", " \n", " print(f\" โš ๏ธ ORDER APPROVED (with warnings):\")\n", " for msg in warning_messages:\n", " print(f\" โ€ข {msg}\")\n", " \n", " else:\n", " # Approve order\n", " risk_event = Event(\n", " event_type=\"risk_approved\",\n", " data={\n", " \"order_id\": order.order_id,\n", " \"severity\": \"INFO\",\n", " \"action\": \"ORDER_APPROVED\"\n", " },\n", " source=self.name\n", " )\n", " self.event_queue.publish(risk_event)\n", " \n", " print(f\" โœ… ORDER APPROVED - All risk checks passed\")\n", " \n", " def on_portfolio_update(self, event: Event):\n", " \"\"\"Monitor portfolio for ongoing risk issues\"\"\"\n", " update_type = event.data[\"update_type\"]\n", " \n", " if update_type == \"price_update\":\n", " # Check if we're approaching risk limits\n", " current_drawdown = self.portfolio.max_drawdown\n", " daily_loss_pct = self._calculate_daily_loss_percentage()\n", " \n", " if current_drawdown > self.max_total_drawdown_pct * 0.8: # 80% of limit\n", " print(f\"โš ๏ธ {self.name}: Approaching drawdown limit ({current_drawdown:.1f}% / {self.max_total_drawdown_pct}%)\")\n", " \n", " if daily_loss_pct > self.max_daily_loss_pct * 0.8: # 80% of limit\n", " print(f\"โš ๏ธ {self.name}: Approaching daily loss limit ({daily_loss_pct:.1f}% / {self.max_daily_loss_pct}%)\")\n", " \n", " def _check_position_size_limit(self, order: Order) -> Dict[str, Any]:\n", " \"\"\"Check if order would exceed position size limits\"\"\"\n", " symbol = order.symbol\n", " order_value = order.quantity * order.price\n", " portfolio_value = self.portfolio.total_portfolio_value\n", " \n", " # Calculate position value after this order\n", " current_position = self.portfolio.get_position(symbol)\n", " current_value = current_position.market_value if current_position else 0\n", " \n", " if order.side == OrderSide.BUY:\n", " new_position_value = current_value + order_value\n", " else:\n", " new_position_value = max(0, current_value - order_value)\n", " \n", " position_pct = (new_position_value / portfolio_value) * 100\n", " \n", " if position_pct > self.max_position_size_pct:\n", " return {\n", " \"approved\": False,\n", " \"message\": f\"Position size limit exceeded: {position_pct:.1f}% > {self.max_position_size_pct}%\"\n", " }\n", " elif position_pct > self.max_position_size_pct * 0.8:\n", " return {\n", " \"approved\": True,\n", " \"warning\": True,\n", " \"message\": f\"Large position warning: {position_pct:.1f}% of portfolio\"\n", " }\n", " \n", " return {\"approved\": True, \"message\": f\"Position size OK: {position_pct:.1f}%\"}\n", " \n", " def _check_cash_availability(self, order: Order) -> Dict[str, Any]:\n", " \"\"\"Check if we have enough cash for the order\"\"\"\n", " if order.side == OrderSide.SELL:\n", " return {\"approved\": True, \"message\": \"Sell order - no cash required\"}\n", " \n", " order_value = order.quantity * order.price\n", " estimated_fees = order_value * 0.001 # Estimate 0.1% fees\n", " total_needed = order_value + estimated_fees\n", " \n", " if total_needed > self.portfolio.cash_balance:\n", " return {\n", " \"approved\": False,\n", " \"message\": f\"Insufficient cash: need ${total_needed:,.2f}, have ${self.portfolio.cash_balance:,.2f}\"\n", " }\n", " \n", " return {\"approved\": True, \"message\": \"Cash availability OK\"}\n", " \n", " def _check_daily_loss_limit(self) -> Dict[str, Any]:\n", " \"\"\"Check daily loss limits\"\"\"\n", " daily_loss_pct = self._calculate_daily_loss_percentage()\n", " \n", " if daily_loss_pct > self.max_daily_loss_pct:\n", " return {\n", " \"approved\": False,\n", " \"message\": f\"Daily loss limit exceeded: {daily_loss_pct:.1f}% > {self.max_daily_loss_pct}%\"\n", " }\n", " elif daily_loss_pct > self.max_daily_loss_pct * 0.8:\n", " return {\n", " \"approved\": True,\n", " \"warning\": True,\n", " \"message\": f\"Approaching daily loss limit: {daily_loss_pct:.1f}%\"\n", " }\n", " \n", " return {\"approved\": True, \"message\": \"Daily loss within limits\"}\n", " \n", " def _check_total_drawdown_limit(self) -> Dict[str, Any]:\n", " \"\"\"Check total drawdown limits\"\"\"\n", " current_drawdown = self.portfolio.max_drawdown\n", " \n", " if current_drawdown > self.max_total_drawdown_pct:\n", " return {\n", " \"approved\": False,\n", " \"message\": f\"Maximum drawdown exceeded: {current_drawdown:.1f}% > {self.max_total_drawdown_pct}%\"\n", " }\n", " \n", " return {\"approved\": True, \"message\": \"Drawdown within limits\"}\n", " \n", " def _check_minimum_cash_reserve(self, order: Order) -> Dict[str, Any]:\n", " \"\"\"Check minimum cash reserve requirements\"\"\"\n", " if order.side == OrderSide.SELL:\n", " return {\"approved\": True, \"message\": \"Sell order - increases cash\"}\n", " \n", " order_value = order.quantity * order.price\n", " remaining_cash = self.portfolio.cash_balance - order_value\n", " portfolio_value = self.portfolio.total_portfolio_value\n", " cash_pct = (remaining_cash / portfolio_value) * 100\n", " \n", " if cash_pct < self.min_cash_reserve_pct:\n", " return {\n", " \"approved\": False,\n", " \"message\": f\"Minimum cash reserve violated: {cash_pct:.1f}% < {self.min_cash_reserve_pct}%\"\n", " }\n", " \n", " return {\"approved\": True, \"message\": \"Cash reserve OK\"}\n", " \n", " def _calculate_daily_loss_percentage(self) -> float:\n", " \"\"\"Calculate daily loss as percentage\"\"\"\n", " current_value = self.portfolio.total_portfolio_value\n", " daily_change = current_value - self.daily_start_value\n", " \n", " if daily_change >= 0:\n", " return 0.0 # No loss\n", " \n", " return abs(daily_change / self.daily_start_value) * 100\n", " \n", " def get_risk_summary(self) -> Dict[str, Any]:\n", " \"\"\"Get risk management summary\"\"\"\n", " return {\n", " \"risk_limits\": {\n", " \"max_position_size_pct\": self.max_position_size_pct,\n", " \"max_daily_loss_pct\": self.max_daily_loss_pct,\n", " \"max_total_drawdown_pct\": self.max_total_drawdown_pct,\n", " \"min_cash_reserve_pct\": self.min_cash_reserve_pct\n", " },\n", " \"current_status\": {\n", " \"daily_loss_pct\": self._calculate_daily_loss_percentage(),\n", " \"total_drawdown_pct\": self.portfolio.max_drawdown,\n", " \"cash_reserve_pct\": (self.portfolio.cash_balance / self.portfolio.total_portfolio_value) * 100\n", " },\n", " \"statistics\": {\n", " \"blocked_orders\": self.blocked_orders,\n", " \"risk_warnings\": self.risk_warnings,\n", " \"total_violations\": len(self.risk_violations)\n", " }\n", " }\n", "\n", "# Create risk controller\n", "risk_controller = RiskController(event_queue, portfolio)\n", "print(f\"\\n๐Ÿ›ก๏ธ Risk Controller Summary:\")\n", "print(json.dumps(risk_controller.get_risk_summary(), indent=2, default=str))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 4: Market Data Simulator\n", "\n", "Let's create a simple market data simulator to test our portfolio and risk systems:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MarketDataSimulator:\n", " \"\"\"Simple market data simulator for testing\"\"\"\n", " \n", " def __init__(self, event_queue: EventQueue):\n", " self.event_queue = event_queue\n", " self.name = \"market_simulator\"\n", " \n", " # Initial prices\n", " self.prices = {\n", " \"BTC/USDT\": 43000.0,\n", " \"ETH/USDT\": 2650.0,\n", " \"BNB/USDT\": 310.0\n", " }\n", " \n", " def update_price(self, symbol: str, new_price: float):\n", " \"\"\"Update price and publish market data event\"\"\"\n", " old_price = self.prices.get(symbol, 0)\n", " self.prices[symbol] = new_price\n", " change_pct = ((new_price - old_price) / old_price * 100) if old_price > 0 else 0\n", " \n", " market_event = Event(\n", " event_type=\"market_data\",\n", " data={\n", " \"symbol\": symbol,\n", " \"price\": new_price,\n", " \"volume\": 100.0, # Mock volume\n", " \"change_pct\": change_pct\n", " },\n", " source=self.name\n", " )\n", " \n", " self.event_queue.publish(market_event)\n", " print(f\"๐Ÿ“ˆ {symbol}: ${old_price:,.2f} โ†’ ${new_price:,.2f} ({change_pct:+.2f}%)\")\n", " \n", " def simulate_order_fill(self, symbol: str, side: str, quantity: float, price: float, fees: float = None):\n", " \"\"\"Simulate an order fill\"\"\"\n", " if fees is None:\n", " fees = quantity * price * 0.001 # 0.1% default fee\n", " \n", " fill_event = Event(\n", " event_type=\"order_fill\",\n", " data={\n", " \"order_id\": str(uuid.uuid4()),\n", " \"symbol\": symbol,\n", " \"side\": side,\n", " \"fill_quantity\": quantity,\n", " \"fill_price\": price,\n", " \"fees\": fees\n", " },\n", " source=self.name\n", " )\n", " \n", " self.event_queue.publish(fill_event)\n", "\n", "# Create market simulator\n", "market_sim = MarketDataSimulator(event_queue)\n", "print(f\"๐Ÿ“Š Market simulator ready with initial prices: {market_sim.prices}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 5: Complete Demo - Portfolio & Risk in Action\n", "\n", "Let's test our complete system with various trading scenarios!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"๐ŸŽฌ DEMO: Portfolio & Risk Management System\\n\")\n", "print(\"=\" * 70)\n", "\n", "# Initial state\n", "print(\"\\n๐Ÿ“Š Initial State:\")\n", "print(f\"Portfolio Value: ${portfolio.total_portfolio_value:,.2f}\")\n", "print(f\"Cash Balance: ${portfolio.cash_balance:,.2f}\")\n", "\n", "# Test 1: Normal trade (should pass all risk checks)\n", "print(\"\\n๐Ÿ“Š Test 1: Normal BTC Purchase\")\n", "print(\"-\" * 40)\n", "btc_order = Order(\"BTC/USDT\", OrderSide.BUY, 0.5, 43000) # $21,500 order (21.5% of portfolio)\n", "request_event = Event(\n", " event_type=\"order_request\",\n", " data={\"order\": btc_order},\n", " source=\"demo\"\n", ")\n", "event_queue.publish(request_event)\n", "\n", "# Simulate the fill\n", "time.sleep(0.5)\n", "market_sim.simulate_order_fill(\"BTC/USDT\", \"BUY\", 0.5, 43000)\n", "\n", "time.sleep(0.5)\n", "\n", "# Test 2: Price update (profit scenario)\n", "print(\"\\n๐Ÿ“Š Test 2: BTC Price Increase\")\n", "print(\"-\" * 30)\n", "market_sim.update_price(\"BTC/USDT\", 44500) # +3.49% increase\n", "\n", "time.sleep(0.5)\n", "\n", "# Test 3: Large order (should trigger position size warning)\n", "print(\"\\n๐Ÿ“Š Test 3: Large ETH Order (Position Size Warning)\")\n", "print(\"-\" * 50)\n", "eth_order = Order(\"ETH/USDT\", OrderSide.BUY, 8.0, 2650) # $21,200 order\n", "request_event = Event(\n", " event_type=\"order_request\",\n", " data={\"order\": eth_order},\n", " source=\"demo\"\n", ")\n", "event_queue.publish(request_event)\n", "\n", "time.sleep(0.5)\n", "market_sim.simulate_order_fill(\"ETH/USDT\", \"BUY\", 8.0, 2650)\n", "\n", "time.sleep(0.5)\n", "\n", "# Test 4: Excessive order (should be blocked)\n", "print(\"\\n๐Ÿ“Š Test 4: Excessive Order (Should be BLOCKED)\")\n", "print(\"-\" * 45)\n", "huge_order = Order(\"BTC/USDT\", OrderSide.BUY, 1.0, 44500) # $44,500 order (too large)\n", "request_event = Event(\n", " event_type=\"order_request\",\n", " data={\"order\": huge_order},\n", " source=\"demo\"\n", ")\n", "event_queue.publish(request_event)\n", "\n", "time.sleep(0.5)\n", "\n", "# Test 5: Market crash (trigger risk warnings)\n", "print(\"\\n๐Ÿ“Š Test 5: Market Crash Simulation\")\n", "print(\"-\" * 35)\n", "market_sim.update_price(\"BTC/USDT\", 39000) # -12.4% crash\n", "market_sim.update_price(\"ETH/USDT\", 2300) # -13.2% crash\n", "\n", "time.sleep(0.5)\n", "\n", "# Test 6: Try to trade during high losses (should be blocked)\n", "print(\"\\n๐Ÿ“Š Test 6: Order During High Losses (Should be BLOCKED)\")\n", "print(\"-\" * 55)\n", "risky_order = Order(\"BNB/USDT\", OrderSide.BUY, 50, 310) # $15,500 order\n", "request_event = Event(\n", " event_type=\"order_request\",\n", " data={\"order\": risky_order},\n", " source=\"demo\"\n", ")\n", "event_queue.publish(request_event)\n", "\n", "time.sleep(0.5)\n", "\n", "print(\"\\n\" + \"=\" * 70)\n", "print(\"๐Ÿ“ˆ Final Portfolio Summary:\")\n", "print(json.dumps(portfolio.get_portfolio_summary(), indent=2, default=str))\n", "\n", "print(\"\\n๐Ÿ›ก๏ธ Final Risk Summary:\")\n", "print(json.dumps(risk_controller.get_risk_summary(), indent=2, default=str))\n", "\n", "print(f\"\\n๐Ÿ“Š Total Events Processed: {len(event_queue.event_history)}\")\n", "print(\"\\n๐ŸŽฏ Key Achievements:\")\n", "print(f\" โ€ข Tracked {len(portfolio.positions)} positions in real-time\")\n", "print(f\" โ€ข Processed {portfolio.total_trades} trades\")\n", "print(f\" โ€ข Blocked {risk_controller.blocked_orders} risky orders\")\n", "print(f\" โ€ข Issued {risk_controller.risk_warnings} risk warnings\")\n", "print(f\" โ€ข Current P&L: ${portfolio.total_pnl:,.2f} ({portfolio.total_pnl_percentage:+.2f}%)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 6: Your Turn - Advanced Portfolio Features\n", "\n", "**Challenge:** Extend the system with advanced portfolio and risk features!\n", "\n", "Choose one or more of these challenges:\n", "\n", "1. **Performance Metrics**: Add Sharpe ratio, Sortino ratio, maximum drawdown duration\n", "2. **Advanced Risk Controls**: Position correlation limits, sector exposure limits\n", "3. **Dynamic Risk Adjustment**: Adjust risk limits based on market volatility\n", "4. **Portfolio Rebalancing**: Automatic rebalancing when positions drift too far\n", "5. **Risk Reports**: Generate detailed risk reports with recommendations" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Challenge 1: Performance Metrics\n", "def calculate_sharpe_ratio(portfolio: PortfolioManager, risk_free_rate: float = 0.02) -> float:\n", " \"\"\"Calculate Sharpe ratio for the portfolio\"\"\"\n", " # YOUR CODE HERE\n", " # Hint: You'll need to track daily returns over time\n", " pass\n", "\n", "# Challenge 2: Correlation-based Risk Control\n", "def check_position_correlation(portfolio: PortfolioManager, new_symbol: str, max_correlation: float = 0.7) -> bool:\n", " \"\"\"Check if new position would create excessive correlation risk\"\"\"\n", " # YOUR CODE HERE\n", " # Hint: You could use simple sector groupings (BTC/ETH = crypto, etc.)\n", " pass\n", "\n", "# Challenge 3: Dynamic Risk Adjustment\n", "def adjust_risk_limits_for_volatility(risk_controller: RiskController, market_volatility: float):\n", " \"\"\"Adjust risk limits based on market conditions\"\"\"\n", " # YOUR CODE HERE\n", " # Hint: Reduce position limits when volatility is high\n", " pass\n", "\n", "# Challenge 4: Portfolio Rebalancing\n", "def suggest_rebalancing_trades(portfolio: PortfolioManager, target_weights: Dict[str, float]) -> List[Order]:\n", " \"\"\"Suggest trades to rebalance portfolio to target weights\"\"\"\n", " # YOUR CODE HERE\n", " pass\n", "\n", "# Challenge 5: Risk Report Generation\n", "def generate_risk_report(portfolio: PortfolioManager, risk_controller: RiskController) -> Dict[str, Any]:\n", " \"\"\"Generate comprehensive risk report\"\"\"\n", " # YOUR CODE HERE\n", " pass\n", "\n", "# Test your implementations here\n", "print(\"๐Ÿงช Test your advanced features here!\")\n", "\n", "# Example tests:\n", "# sharpe = calculate_sharpe_ratio(portfolio)\n", "# rebalance_trades = suggest_rebalancing_trades(portfolio, {\"BTC/USDT\": 0.6, \"ETH/USDT\": 0.3, \"CASH\": 0.1})\n", "# risk_report = generate_risk_report(portfolio, risk_controller)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Congratulations!\n", "\n", "You've built a professional-grade Portfolio Management and Risk Control system! Here's what you accomplished:\n", "\n", "โœ… **Real-time Portfolio Tracking**: Positions, P&L, balances updated instantly \n", "โœ… **Comprehensive Risk Controls**: Position size, daily loss, drawdown, cash reserve limits \n", "โœ… **Event-driven Integration**: Seamless communication with order management system \n", "โœ… **Professional Risk Management**: Order blocking, warnings, violation tracking \n", "โœ… **Market Simulation**: Realistic testing environment with price movements \n", "โœ… **Complete Statistics**: Detailed reporting and monitoring capabilities \n", "\n", "## Key Professional Features:\n", "\n", "1. **Position Management**: Real-time tracking with average cost basis and P&L calculation\n", "2. **Risk Controls**: Multi-layered protection against excessive losses\n", "3. **Event Integration**: Loose coupling through event system\n", "4. **Real-time Updates**: Instant portfolio updates as market prices change\n", "5. **Comprehensive Monitoring**: Statistics, warnings, and violation tracking\n", "\n", "## Next Steps:\n", "\n", "In the next notebook, we'll add the **Market Data Handler** and **Algorithm Engine** to complete our MARKET architecture and create a fully functional trading bot!\n", "\n", "---\n", "*\"Risk management is not about avoiding risk - it's about taking the right risks at the right size.\"* ๐Ÿ›ก๏ธ๐Ÿ’ฐ" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.3" } }, "nbformat": 4, "nbformat_minor": 4 }