diff --git a/tutorials/20260622164035_Let's Build a Quant Trading Strategy, MemLabs/part3.ipynb b/tutorials/20260622164035_Let's Build a Quant Trading Strategy, MemLabs/part3.ipynb new file mode 100644 index 0000000..486f07f --- /dev/null +++ b/tutorials/20260622164035_Let's Build a Quant Trading Strategy, MemLabs/part3.ipynb @@ -0,0 +1,476 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 27, + "id": "87f54819", + "metadata": {}, + "outputs": [], + "source": [ + "# Data and analysis libraries\n", + "import polars as pl # Fast dataframes for financial data\n", + "import numpy as np # Numerical computing library\n", + "from datetime import datetime, timedelta # Date and time operations\n", + "import random\n", + "\n", + "# Machine learning libraries \n", + "import torch # PyTorch framework\n", + "import torch.nn as nn # Neural network modules\n", + "import torch.optim as optim # Optimization algorithms\n", + "import research # Model building and training utilities\n", + "\n", + "# Visualization and \n", + "import altair as alt # Interactive visualization library\n", + "\n", + "# data sources\n", + "import binance # Binance market data utilities" + ] + }, + { + "cell_type": "markdown", + "id": "8e8cc3fb", + "metadata": {}, + "source": [ + "# Part 3 - Implementation" + ] + }, + { + "cell_type": "markdown", + "id": "2718194d", + "metadata": {}, + "source": [ + "## Recap" + ] + }, + { + "cell_type": "markdown", + "id": "4fd78781", + "metadata": {}, + "source": [ + "### Part 1: Reasearch" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "ced8e0ab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearModel(\n", + " (linear): Linear(in_features=3, out_features=1, bias=True)\n", + ")" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import models\n", + "\n", + "model = models.LinearModel(3)\n", + "model.load_state_dict(torch.load('model_weights.pth', weights_only=True))\n", + "model.eval()" + ] + }, + { + "cell_type": "markdown", + "id": "0c43665f", + "metadata": {}, + "source": [ + "### AR(3) Model to predict future log return - 12h forecast horizon" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "d26b6d5a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "linear.weight:\n", + "[[-0.02849757 -0.08180149 -0.05941094]]\n", + "linear.bias:\n", + "[0.00058626]\n" + ] + } + ], + "source": [ + "research.print_model_params(model)" + ] + }, + { + "cell_type": "markdown", + "id": "d8250c71", + "metadata": {}, + "source": [ + "## Part 2: Strategy Recap" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "13a8bba8", + "metadata": {}, + "outputs": [], + "source": [ + "# ~14% without any optimization\n", + "# 1. Compounding Trade Sizing\n", + "# 2. Leverage\n", + "# ~14% to >40%" + ] + }, + { + "cell_type": "markdown", + "id": "4c0d6f13", + "metadata": {}, + "source": [ + "## Tick" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "8c0d69f4", + "metadata": {}, + "outputs": [], + "source": [ + "from abc import ABC, abstractmethod\n", + "from typing import Generic, TypeVar\n", + "\n", + "T = TypeVar('T') # input type\n", + "R = TypeVar('R') # output type\n", + "\n", + "class Tick(ABC, Generic[T, R]):\n", + " @abstractmethod\n", + " def on_tick(self, val: T) -> R:\n", + " \"\"\"Handle a new tick and optionally return a result.\"\"\"\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "4c684fbf", + "metadata": {}, + "source": [ + "## Sliding Window" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "9d331f96", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import deque\n", + "from typing import Deque, Optional\n", + "import numpy as np\n", + "\n", + "class DequeWindow(Tick[T, Optional[T]], Generic[T]):\n", + " def __init__(self, n: int):\n", + " self._data: Deque[T] = deque(maxlen=n)\n", + "\n", + " def on_tick(self, val: T) -> Optional[T]:\n", + " \"\"\"Append a value and return the oldest value dropped (if any).\"\"\"\n", + " dropped = None\n", + " if self.is_full():\n", + " dropped = self._data[0]\n", + " self._data.append(val)\n", + " return dropped\n", + " \n", + " def is_full(self) -> bool:\n", + " return self._data.maxlen == len(self._data)\n", + "\n", + " def append_left(self, val: T) -> Optional[T]:\n", + " dropped = None\n", + " if self.is_full():\n", + " dropped = self._data[-1]\n", + " self._data.appendleft(val)\n", + " return dropped\n", + " \n", + " def to_numpy(self) -> np.ndarray:\n", + " return np.array(self._data)\n", + " \n", + " def __repr__(self) -> str:\n", + " cls_name = self.__class__.__name__\n", + " return f\"{cls_name}(capacity={self._data.maxlen}, values={list(self._data)})\"" + ] + }, + { + "cell_type": "markdown", + "id": "6b1af130", + "metadata": {}, + "source": [ + "### Array based window" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "f4fc79a5", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from typing import Optional\n", + "\n", + "class NumpyWindow(Tick[T, Optional[T]]):\n", + " def __init__(self, n: int, dtype=np.float64):\n", + " if n <= 0:\n", + " raise ValueError(\"Capacity must be positive.\")\n", + " self._capacity = n\n", + " self._data = np.zeros(n, dtype=dtype)\n", + " self._size = 0\n", + "\n", + " def on_tick(self, val: float) -> Optional[float]:\n", + " dropped = None\n", + "\n", + " if self._size < self._capacity:\n", + " self._data[self._size] = val\n", + " self._size += 1\n", + " else:\n", + " dropped = self._data[0]\n", + " # shift left in-place\n", + " for i in range(1, self._capacity):\n", + " self._data[i - 1] = self._data[i]\n", + " self._data[-1] = val\n", + "\n", + " return dropped\n", + "\n", + "\n", + " def __getitem__(self, idx: int) -> float:\n", + " \"\"\"Index access (0 = oldest).\"\"\"\n", + " if not 0 <= idx < self._size:\n", + " raise IndexError(\"Index out of range.\")\n", + " return self._data[idx]\n", + "\n", + " def __len__(self) -> int:\n", + " return self._size\n", + "\n", + " def capacity(self) -> int:\n", + " return self._capacity\n", + "\n", + " def is_full(self) -> bool:\n", + " return self._size == self._capacity\n", + "\n", + " def values(self) -> np.ndarray:\n", + " return self._data[:self._size]\n", + "\n", + " def __repr__(self) -> str:\n", + " vals = self.values().tolist()\n", + " return f\"{self.__class__.__name__}(capacity={self._capacity}, size={self._size}, values={vals})\"" + ] + }, + { + "cell_type": "markdown", + "id": "c9213b19", + "metadata": {}, + "source": [ + "### Stream the Last Known Value" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "e3b6ae13", + "metadata": {}, + "outputs": [], + "source": [ + "class Last(Tick[T, T], Generic[T]):\n", + " def __init__(self):\n", + " self._value: Optional[T] = None\n", + "\n", + " def on_tick(self, val: T) -> Optional[T]:\n", + " self._value = val\n", + " return val\n", + "\n", + " def __repr__(self) -> str:\n", + " cls_name = self.__class__.__name__\n", + " return f\"{cls_name}(value={self._value})\"" + ] + }, + { + "cell_type": "markdown", + "id": "d93282e7", + "metadata": {}, + "source": [ + "## Streaming Log Returns" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "e14f546c", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "class LogReturn(Tick[float, Optional[float]], Generic[T]):\n", + " def __init__(self):\n", + " self._window = NumpyWindow(2)\n", + "\n", + " def on_tick(self, val: float) -> Optional[float]:\n", + " self._window.on_tick(val)\n", + " if self._window.is_full():\n", + " return np.log(self._window[1] / self._window[0])\n", + " else:\n", + " return None\n", + " \n", + " def __repr__(self) -> str:\n", + " cls_name = self.__class__.__name__\n", + " return f\"{cls_name}(window={self._window})\"" + ] + }, + { + "cell_type": "markdown", + "id": "8c2ef8a2", + "metadata": {}, + "source": [ + "## Streaming Auto-Regressive Log Returns Lags" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "cf955e23", + "metadata": {}, + "outputs": [], + "source": [ + "class LogReturnLags(Tick[float, torch.Tensor]):\n", + " def __init__(self, no_lags: int):\n", + " self._lags = DequeWindow(no_lags)\n", + " self._log_return = LogReturn()\n", + " \n", + " def on_tick(self, val: float) -> torch.Tensor | None:\n", + " log_ret = self._log_return.on_tick(val)\n", + " if log_ret is not None:\n", + " self._lags.append_left(log_ret)\n", + " return torch.tensor(self._lags.to_numpy(), dtype=torch.float32) if self._lags.is_full() else None\n", + " else:\n", + " return None\n", + " \n", + " def __repr__(self) -> str:\n", + " cls_name = self.__class__.__name__\n", + " return f\"{cls_name}(lags={self._lags}, log_return={self._log_return})\" " + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "7d62e078", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([ 0.3747, -0.3102, 0.4055])" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lags = LogReturnLags(3)\n", + "lags.on_tick(90)\n", + "lags.on_tick(100)\n", + "lags.on_tick(150)\n", + "lags.on_tick(110)\n", + "features = lags.on_tick(160)\n", + "features" + ] + }, + { + "cell_type": "markdown", + "id": "8ab400f2", + "metadata": {}, + "source": [ + "### Streaming features into our model" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "43b202c4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([-0.0088])" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X = features\n", + "with torch.no_grad():\n", + " y_hat = model(X)\n", + "y_hat" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "d57f4837", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(-0.0088)" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y_hat[0]" + ] + }, + { + "cell_type": "markdown", + "id": "a1f09327", + "metadata": {}, + "source": [ + "## Build the Trading System" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "study (3.14.5.final.0)", + "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.14.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}