{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fd999901",
   "metadata": {},
   "source": [
    "<div style=\"background-color:#000;\"><img src=\"pqn.png\"></img></div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "986105e8",
   "metadata": {},
   "outputs": [],
   "source": [
    "import threading\n",
    "import time\n",
    "from typing import Dict, Optional"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b1b21d3d",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "from ibapi.client import EClient\n",
    "from ibapi.common import BarData\n",
    "from ibapi.contract import Contract\n",
    "from ibapi.order import Order\n",
    "from ibapi.wrapper import EWrapper"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "655e0e34",
   "metadata": {},
   "source": [
    "### Calculate Donchian Channels for price data"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "712afa36",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "This section defines a function to calculate Donchian Channels for given price data over a specified period. It calculates the upper and lower bands and optionally the middle line."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c9b4cbb0",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "def donchian_channel(df: pd.DataFrame, period: int = 30) -> pd.DataFrame:\n",
    "\n",
    "    df[\"upper\"] = df[\"high\"].rolling(window=period).max()\n",
    "\n",
    "    df[\"lower\"] = df[\"low\"].rolling(window=period).min()\n",
    "\n",
    "    df[\"mid\"] = (df[\"upper\"] + df[\"lower\"]) / 2\n",
    "\n",
    "    return df"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2467633a",
   "metadata": {},
   "source": [
    "This function takes a DataFrame containing price data and a period for the calculation. It computes the upper band as the highest high over the period and the lower band as the lowest low. The middle line is calculated as the average of the upper and lower bands. Finally, the function returns the DataFrame with the new columns added."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "193cbda0",
   "metadata": {},
   "source": [
    "### Create a class to interact with Interactive Brokers API"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59e6e825",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "This section defines a TradingApp class that interacts with the Interactive Brokers (IB) API. This class handles connections, data retrieval, and order placement."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef93770e",
   "metadata": {},
   "outputs": [],
   "source": [
    "class TradingApp(EClient, EWrapper):\n",
    "\n",
    "    def __init__(self) -> None:\n",
    "\n",
    "        EClient.__init__(self, self)\n",
    "        self.data: Dict[int, pd.DataFrame] = {}\n",
    "        self.nextOrderId: Optional[int] = None\n",
    "\n",
    "    def error(\n",
    "        self, reqId: int, errorCode: int, errorString: str, advanced: any\n",
    "    ) -> None:\n",
    "\n",
    "        print(f\"Error: {reqId}, {errorCode}, {errorString}\")\n",
    "\n",
    "    def nextValidId(self, orderId: int) -> None:\n",
    "\n",
    "        super().nextValidId(orderId)\n",
    "        self.nextOrderId = orderId\n",
    "\n",
    "    def get_historical_data(self, reqId: int, contract: Contract) -> pd.DataFrame:\n",
    "\n",
    "        self.data[reqId] = pd.DataFrame(columns=[\"time\", \"high\", \"low\", \"close\"])\n",
    "        self.data[reqId].set_index(\"time\", inplace=True)\n",
    "        self.reqHistoricalData(\n",
    "            reqId=reqId,\n",
    "            contract=contract,\n",
    "            endDateTime=\"\",\n",
    "            durationStr=\"1 D\",\n",
    "            barSizeSetting=\"1 min\",\n",
    "            whatToShow=\"MIDPOINT\",\n",
    "            useRTH=0,\n",
    "            formatDate=2,\n",
    "            keepUpToDate=False,\n",
    "            chartOptions=[],\n",
    "        )\n",
    "        time.sleep(5)\n",
    "        return self.data[reqId]\n",
    "\n",
    "    def historicalData(self, reqId: int, bar: BarData) -> None:\n",
    "\n",
    "        df = self.data[reqId]\n",
    "\n",
    "        df.loc[pd.to_datetime(bar.date, unit=\"s\"), [\"high\", \"low\", \"close\"]] = [\n",
    "            bar.high,\n",
    "            bar.low,\n",
    "            bar.close,\n",
    "        ]\n",
    "\n",
    "        df = df.astype(float)\n",
    "\n",
    "        self.data[reqId] = df\n",
    "\n",
    "    @staticmethod\n",
    "    def get_contract(symbol: str) -> Contract:\n",
    "\n",
    "        contract = Contract()\n",
    "        contract.symbol = symbol\n",
    "        contract.secType = \"STK\"\n",
    "        contract.exchange = \"SMART\"\n",
    "        contract.currency = \"USD\"\n",
    "        return contract\n",
    "\n",
    "    def place_order(\n",
    "        self, contract: Contract, action: str, order_type: str, quantity: int\n",
    "    ) -> None:\n",
    "\n",
    "        order = Order()\n",
    "        order.action = action\n",
    "        order.orderType = order_type\n",
    "        order.totalQuantity = quantity\n",
    "\n",
    "        self.placeOrder(self.nextOrderId, contract, order)\n",
    "        self.nextOrderId += 1\n",
    "        print(\"Order placed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ac4d04c",
   "metadata": {},
   "source": [
    "The TradingApp class extends EClient and EWrapper to interact with the IB API. It initializes the client and wrapper components and sets up data storage. The error method handles API errors, while nextValidId sets the next valid order ID. The get_historical_data method requests historical market data for a given contract, storing it in a DataFrame. The historicalData method processes and stores the received data. The get_contract method creates a stock contract, and the place_order method places trades using the provided contract, action, order type, and quantity."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd46c3c3",
   "metadata": {},
   "source": [
    "### Connect the trading app and request data"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4f05869",
   "metadata": {},
   "source": [
    "This section connects the TradingApp to the IB API and requests historical data for a specified stock. It also calculates Donchian Channels for the acquired data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6916d705",
   "metadata": {},
   "outputs": [],
   "source": [
    "app = TradingApp()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "46aa8c0a",
   "metadata": {},
   "outputs": [],
   "source": [
    "app.connect(\"127.0.0.1\", 7497, clientId=5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e4c3e56e",
   "metadata": {},
   "outputs": [],
   "source": [
    "threading.Thread(target=app.run, daemon=True).start()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e7ceb258",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "while True:\n",
    "    if isinstance(app.nextOrderId, int):\n",
    "        print(\"connected\")\n",
    "        break\n",
    "    else:\n",
    "        print(\"waiting for connection\")\n",
    "        time.sleep(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "57f829af",
   "metadata": {},
   "outputs": [],
   "source": [
    "nvda = TradingApp.get_contract(\"NVDA\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "815602fb",
   "metadata": {},
   "outputs": [],
   "source": [
    "data = app.get_historical_data(99, nvda)\n",
    "data.tail()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "75f32b38",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "donchian = donchian_channel(data, period=30)\n",
    "donchian.tail()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09e50c35",
   "metadata": {},
   "source": [
    "The code creates an instance of the TradingApp class and connects it to the IB API using the specified IP address, port, and client ID. It then starts the app on a separate thread to allow code execution to continue. A loop checks for a successful connection by verifying the nextOrderId. Once connected, it defines a contract for the stock symbol NVDA and requests historical data for the last trading day using a specified request ID. The Donchian Channels are then calculated for the acquired data."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b65adae",
   "metadata": {},
   "source": [
    "### Implement trading logic based on Donchian Channels"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3061633c",
   "metadata": {},
   "source": [
    "This section implements trading logic based on the Donchian Channels. It checks for breakouts and places buy or sell orders accordingly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f4d5dd6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "period = 30"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "848bb8d0",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "while True:\n",
    "\n",
    "    print(\"Getting data for contract...\")\n",
    "    data = app.get_historical_data(99, nvda)\n",
    "\n",
    "    if len(data) < period:\n",
    "        print(f\"There are only {len(data)} bars of data, skipping...\")\n",
    "        continue\n",
    "\n",
    "    print(\"Computing the Donchian Channel...\")\n",
    "    donchian = donchian_channel(data, period=period)\n",
    "\n",
    "    last_price = data.iloc[-1].close\n",
    "\n",
    "    upper, lower = donchian[[\"upper\", \"lower\"]].iloc[-1]\n",
    "\n",
    "    print(\n",
    "        f\"Check if last price {last_price} is outside the channels {upper} and {lower}\"\n",
    "    )\n",
    "\n",
    "    if last_price >= upper:\n",
    "        print(\"Breakout detected, going long...\")\n",
    "        app.place_order(nvda, \"BUY\", \"MKT\", 10)\n",
    "\n",
    "    elif last_price <= lower:\n",
    "        print(\"Breakout detected, going short...\")\n",
    "        app.place_order(nvda, \"SELL\", \"MKT\", 10)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31f5b652",
   "metadata": {},
   "source": [
    "The code sets the period for the Donchian Channels to 30. It enters an infinite loop to continuously request data and check for trading opportunities. It retrieves historical data for the NVDA contract and skips further processing if there is insufficient data. It then calculates the Donchian Channels and gets the last traded price. The code compares the last price with the upper and lower channels to detect breakouts. If a breakout to the upside is detected, it places a buy market order for 10 shares. If a breakout to the downside is detected, it places a sell market order for 10 shares."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "89402165",
   "metadata": {},
   "source": [
    "### Disconnect the trading app"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "92b4fc23",
   "metadata": {},
   "source": [
    "This section disconnects the TradingApp from the IB API once the trading logic is complete."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c771f63a",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "app.disconnect()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4d25159",
   "metadata": {},
   "source": [
    "The code disconnects the TradingApp from the IB API. This ensures that the connection is properly closed once the trading logic is complete. It is important to disconnect to avoid leaving open connections, which can lead to issues with the API or unwanted data usage."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0733028",
   "metadata": {},
   "source": [
    "### Your next steps"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2221da63",
   "metadata": {},
   "source": [
    "Try changing the stock symbol to a different one to see how the Donchian Channels and trading logic perform with other stocks. Experiment with different periods for the Donchian Channels to observe how the results change. Consider modifying the order quantity to suit your trading strategy better."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9961ff12",
   "metadata": {},
   "source": [
    "<a href=\"https://pyquantnews.com/\">PyQuant News</a> is where finance practitioners level up with Python for quant finance, algorithmic trading, and market data analysis. Looking to get started? Check out the fastest growing, top-selling course to <a href=\"https://gettingstartedwithpythonforquantfinance.com/\">get started with Python for quant finance</a>. For educational purposes. Not investment advise. Use at your own risk."
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "cell_metadata_filter": "-all",
   "main_language": "python",
   "notebook_metadata_filter": "-all"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
