{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "f9127175",
   "metadata": {},
   "source": [
    "<div style=\"background-color:#000;\"><img src=\"pqn.png\"></img></div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "235dd464",
   "metadata": {},
   "source": [
    "This code uses the OpenBB Terminal SDK to load historical forex data and apply signal processing techniques to detect trading signals. It computes the Hilbert Transform to derive the dominant cycle phase and period, applies a Butterworth bandpass filter, and calculates amplitude and exponential moving average of amplitude. With these transformations, it identifies buy and sell positions based on signal thresholds and plots the results. This approach is useful for analyzing market cycles and testing trading strategies."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d9c52ae1",
   "metadata": {},
   "outputs": [],
   "source": [
    "from math import pi"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6d48ef61",
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.signal import butter, filtfilt\n",
    "import numpy as np\n",
    "import talib"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b79d1ca",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5fe3441",
   "metadata": {},
   "outputs": [],
   "source": [
    "from openbb_terminal.sdk import openbb"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a5cf8458",
   "metadata": {},
   "source": [
    "Load historical forex data for the EUR/USD pair between the specified dates"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "668e2456",
   "metadata": {},
   "outputs": [],
   "source": [
    "data = openbb.forex.load(\n",
    "    from_symbol=\"EUR\", \n",
    "    to_symbol=\"USD\", \n",
    "    start_date=\"2016-01-01\", \n",
    "    end_date=\"2021-12-31\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e6f3093",
   "metadata": {},
   "source": [
    "Load and preprocess forex data, converting adjusted close prices to a DataFrame"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "31dc063d",
   "metadata": {},
   "outputs": [],
   "source": [
    "data = openbb.forex.load(\n",
    "    from_symbol=\"EUR\", \n",
    "    to_symbol=\"USD\", \n",
    "    start_date=\"2016-01-01\", \n",
    "    end_date=\"2021-12-31\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "01ddec47",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices = (\n",
    "    data[\"Adj Close\"]\n",
    "    .to_frame()\n",
    "    .rename(\n",
    "        columns={\n",
    "            \"Adj Close\": \"close\"\n",
    "        }\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "06940c12",
   "metadata": {},
   "source": [
    "Calculate log returns from the closing prices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15166b77",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices[\"log_return\"] = (\n",
    "    prices.close\n",
    "    .apply(np.log)\n",
    "    .diff(1)\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9dea8d31",
   "metadata": {},
   "source": [
    "Compute the Hilbert Transform - Dominant Cycle Phase"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dc8300e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices[\"phase\"] = talib.HT_DCPHASE(prices.close)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59eb8282",
   "metadata": {},
   "source": [
    "Convert the phase into a sinusoidal signal"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef233ac9",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices[\"signal\"] = np.sin(prices.phase + pi / 4)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "408eef48",
   "metadata": {},
   "source": [
    "Compute the Hilbert Transform - Dominant Cycle Period"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5a4a8a74",
   "metadata": {
    "lines_to_next_cell": 1
   },
   "outputs": [],
   "source": [
    "prices[\"period\"] = talib.HT_DCPERIOD(prices.close)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0c0544a6",
   "metadata": {
    "lines_to_next_cell": 1
   },
   "outputs": [],
   "source": [
    "def butter_bandpass(data, period, delta=0.5, fs=5):\n",
    "    \"\"\"Applies Butterworth bandpass filter to data\n",
    "    \n",
    "    Parameters\n",
    "    ----------\n",
    "    data : np.ndarray\n",
    "        The data to be filtered\n",
    "    period : float\n",
    "        Dominant cycle period\n",
    "    delta : float, optional\n",
    "        Delta value to adjust cutoff frequencies, by default 0.5\n",
    "    fs : int, optional\n",
    "        Sampling frequency, by default 5\n",
    "    \n",
    "    Returns\n",
    "    -------\n",
    "    np.ndarray\n",
    "        Filtered data\n",
    "    \n",
    "    Notes\n",
    "    -----\n",
    "    This function applies a bandpass filter to the data using \n",
    "    the specified period and delta to determine cutoff frequencies.\n",
    "    \"\"\"\n",
    "    \n",
    "    nyq = 0.5 * fs\n",
    "\n",
    "    # Low cutoff frequency\n",
    "    low = 1.0 / (period * (1 + delta))\n",
    "    low /= nyq\n",
    "\n",
    "    # High cutoff frequency\n",
    "    high = 1.0 / (period * (1 - delta))\n",
    "    high /= nyq\n",
    "\n",
    "    b, a = butter(2, [low, high], btype=\"band\")\n",
    "\n",
    "    return filtfilt(b, a, data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af3a5919",
   "metadata": {
    "lines_to_next_cell": 1
   },
   "outputs": [],
   "source": [
    "def roll_apply(e):\n",
    "    \"\"\"Applies Butterworth bandpass filter to rolling window data\n",
    "    \n",
    "    Parameters\n",
    "    ----------\n",
    "    e : pd.Series\n",
    "        Rolling window of data\n",
    "    \n",
    "    Returns\n",
    "    -------\n",
    "    float\n",
    "        Filtered value for the last data point in the window\n",
    "    \n",
    "    Notes\n",
    "    -----\n",
    "    This function takes a rolling window of data, applies the \n",
    "    Butterworth bandpass filter, and returns the filtered value \n",
    "    for the last data point in the window.\n",
    "    \"\"\"\n",
    "    \n",
    "    close = prices.close.loc[e.index]\n",
    "    period = prices.period.loc[e.index][-1]\n",
    "    out = butter_bandpass(close, period)\n",
    "    return out[-1]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f52ba8f3",
   "metadata": {},
   "source": [
    "Apply rolling window to compute filtered signal using Butterworth bandpass filter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b36efc4a",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices[\"filtered\"] = (\n",
    "    prices.dropna()\n",
    "    .rolling(window=30)\n",
    "    .apply(lambda series: roll_apply(series), raw=False)\n",
    "    .iloc[:, 0]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "afb2c00d",
   "metadata": {},
   "source": [
    "Calculate amplitude from the filtered signal over a rolling window"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2ee4a444",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices[\"amplitude\"] = (\n",
    "    prices.\n",
    "    filtered\n",
    "    .rolling(window=30)\n",
    "    .apply(\n",
    "        lambda series: series.max() - series.min()\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c1a6a39b",
   "metadata": {},
   "source": [
    "Compute exponential moving average of amplitude"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1503d9a8",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices[\"ema_amplitude\"] = (\n",
    "    talib\n",
    "    .EMA(\n",
    "        prices.amplitude,\n",
    "        timeperiod=30\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "64229e8f",
   "metadata": {},
   "source": [
    "Define signal and amplitude thresholds for position determination"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c639f06a",
   "metadata": {},
   "outputs": [],
   "source": [
    "signal_thresh = 0.75\n",
    "amp_thresh = 0.004  # 40 pips"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f6a7f8a0",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices[\"position\"] = 0"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f799c701",
   "metadata": {},
   "source": [
    "Determine short positions based on signal and amplitude thresholds"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "753b389e",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices.loc[\n",
    "    (prices.signal >= signal_thresh) & \n",
    "    (prices.amplitude > amp_thresh), \"position\"\n",
    "] = -1"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3fd33d2e",
   "metadata": {},
   "source": [
    "Determine long positions based on signal and amplitude thresholds"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3df30906",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices.loc[\n",
    "    (prices.signal <= -signal_thresh) & \n",
    "    (prices.amplitude > amp_thresh), \"position\"\n",
    "] = 1"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d1756024",
   "metadata": {},
   "source": [
    "Plot the amplitude, signal, and position time series"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "91667313",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(\n",
    "    nrows=3,\n",
    "    figsize=(15, 10),\n",
    "    sharex=True\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "014c83b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices.ema_amplitude.plot(\n",
    "    ax=axes[0],\n",
    "    title=\"amp\"\n",
    ")\n",
    "axes[0].axhline(\n",
    "    amp_thresh,\n",
    "    lw=1,\n",
    "    c=\"r\"\n",
    ")\n",
    "prices.signal.plot(\n",
    "    ax=axes[1],\n",
    "    title=\"signal\"\n",
    ")\n",
    "axes[1].axhline(\n",
    "    signal_thresh,\n",
    "    c=\"r\"\n",
    ")\n",
    "axes[1].axhline(\n",
    "    -signal_thresh,\n",
    "    c=\"r\"\n",
    ")\n",
    "prices.position.plot(\n",
    "    ax=axes[2],\n",
    "    title=\"position\"\n",
    ")\n",
    "fig.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb3226c7",
   "metadata": {},
   "source": [
    "Calculate strategy returns and cumulative returns based on positions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e5d72bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices[\"strategy_return\"] = prices.position.shift(1) * prices.log_return\n",
    "prices[\"strategy_cum_return\"] = prices.strategy_return.cumsum().apply(np.exp)\n",
    "prices[\"bh_rtn_cum\"] = prices[\"log_return\"].cumsum().apply(np.exp)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "23ca420d",
   "metadata": {},
   "source": [
    "Plot cumulative returns of the buy-and-hold strategy and the trading strategy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3b32ee59",
   "metadata": {},
   "outputs": [],
   "source": [
    "(\n",
    "    prices[[\"bh_rtn_cum\", \"strategy_cum_return\"]]\n",
    "    .plot(title=\"Cumulative returns\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "660eb7bd",
   "metadata": {},
   "source": [
    "Create a copy of the prices DataFrame for further analysis"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7b745dfb",
   "metadata": {},
   "outputs": [],
   "source": [
    "df = prices.copy()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df18bb2f",
   "metadata": {},
   "source": [
    "Identify local minima and maxima in the signal for visualization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "340c66b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "n = 5  # number of points to be checked before and after"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "92aa121d",
   "metadata": {},
   "outputs": [],
   "source": [
    "df[\"min\"] = df.iloc[argrelextrema(df.signal.values, np.less_equal, order=n)[0]][\"signal\"]\n",
    "df[\"max\"] = df.iloc[argrelextrema(df.signal.values, np.greater_equal, order=n)[0]][\n",
    "    \"signal\"\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7e63625",
   "metadata": {},
   "source": [
    "Plot the identified local minima and maxima along with the signal"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a921b68f",
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.scatter(df.index, df[\"min\"], c=\"r\")\n",
    "plt.scatter(df.index, df[\"max\"], c=\"g\")\n",
    "plt.plot(df.index, df[\"signal\"])\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "831233d1",
   "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"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
