diff --git a/examples/2021-01-11-backtest-multicore.ipynb b/examples/2021-01-11-backtest-multicore.ipynb
new file mode 100644
index 00000000..9b19bfc1
--- /dev/null
+++ b/examples/2021-01-11-backtest-multicore.ipynb
@@ -0,0 +1,2539 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# \"backtesting multiprocessing\"\n",
+ "> \"How to fetch and backtest crypto data using fastquant and use multiple cores\"\n",
+ "\n",
+ "- toc: true\n",
+ "- branch: master\n",
+ "- badges: true\n",
+ "- comments: true\n",
+ "- author: Mikee Jazmines\n",
+ "- categories: [crypto, backtest, grid search]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# uncomment to install in colab\n",
+ "# !pip3 install fastquant --update\n",
+ "# or pip install git+https://www.github.com/enzoampil/fastquant.git@history"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## fetch data from binance"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from fastquant import get_crypto_data\n",
+ "\n",
+ "crypto = get_crypto_data(\"BTC/USDT\", \n",
+ " \"2018-12-01\", \n",
+ " \"2019-12-31\",\n",
+ " time_resolution='1d'\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " open | \n",
+ " high | \n",
+ " low | \n",
+ " close | \n",
+ " volume | \n",
+ "
\n",
+ " \n",
+ " dt | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 2019-12-27 | \n",
+ " 7202.00 | \n",
+ " 7275.86 | \n",
+ " 7076.42 | \n",
+ " 7254.74 | \n",
+ " 33642.701861 | \n",
+ "
\n",
+ " \n",
+ " 2019-12-28 | \n",
+ " 7254.77 | \n",
+ " 7365.01 | \n",
+ " 7238.67 | \n",
+ " 7316.14 | \n",
+ " 26848.982199 | \n",
+ "
\n",
+ " \n",
+ " 2019-12-29 | \n",
+ " 7315.36 | \n",
+ " 7528.45 | \n",
+ " 7288.00 | \n",
+ " 7388.24 | \n",
+ " 31387.106085 | \n",
+ "
\n",
+ " \n",
+ " 2019-12-30 | \n",
+ " 7388.43 | \n",
+ " 7408.24 | \n",
+ " 7220.00 | \n",
+ " 7246.00 | \n",
+ " 29605.911782 | \n",
+ "
\n",
+ " \n",
+ " 2019-12-31 | \n",
+ " 7246.00 | \n",
+ " 7320.00 | \n",
+ " 7145.01 | \n",
+ " 7195.23 | \n",
+ " 25954.453533 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " open high low close volume\n",
+ "dt \n",
+ "2019-12-27 7202.00 7275.86 7076.42 7254.74 33642.701861\n",
+ "2019-12-28 7254.77 7365.01 7238.67 7316.14 26848.982199\n",
+ "2019-12-29 7315.36 7528.45 7288.00 7388.24 31387.106085\n",
+ "2019-12-30 7388.43 7408.24 7220.00 7246.00 29605.911782\n",
+ "2019-12-31 7246.00 7320.00 7145.01 7195.23 25954.453533"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "crypto.tail()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "x = crypto.reset_index()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import datetime"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "TypeError",
+ "evalue": "an integer is required (got type Timestamp)",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)",
+ "\u001b[0;32m/var/folders/js/_vkhp5md1h30xkynyqkbw15c0000gn/T/ipykernel_11316/4240321943.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mx\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'dt'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mapply\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0ms\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mdatetime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatetime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mutcfromtimestamp\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+ "\u001b[0;32m~/hawksight/fastquant/venv/lib/python3.7/site-packages/pandas/core/series.py\u001b[0m in \u001b[0;36mapply\u001b[0;34m(self, func, convert_dtype, args, **kwds)\u001b[0m\n\u001b[1;32m 4211\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4212\u001b[0m \u001b[0mvalues\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mastype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobject\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_values\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 4213\u001b[0;31m \u001b[0mmapped\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmap_infer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconvert\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mconvert_dtype\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4214\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4215\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmapped\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmapped\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mSeries\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;32mpandas/_libs/lib.pyx\u001b[0m in \u001b[0;36mpandas._libs.lib.map_infer\u001b[0;34m()\u001b[0m\n",
+ "\u001b[0;32m/var/folders/js/_vkhp5md1h30xkynyqkbw15c0000gn/T/ipykernel_11316/4240321943.py\u001b[0m in \u001b[0;36m\u001b[0;34m(s)\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mx\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'dt'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mapply\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0ms\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mdatetime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatetime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mutcfromtimestamp\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+ "\u001b[0;31mTypeError\u001b[0m: an integer is required (got type Timestamp)"
+ ]
+ }
+ ],
+ "source": [
+ "x['dt'].apply(lambda s: datetime.datetime.utcfromtimestamp(s))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Timestamp('2018-12-01 00:00:00')"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "x.iloc[0,0]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## run backtest with a grid of values"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "AttributeError",
+ "evalue": "Can't pickle local object 'initalize_data..CustomData'",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)",
+ "\u001b[0;32m/var/folders/js/_vkhp5md1h30xkynyqkbw15c0000gn/T/ipykernel_11316/2990413032.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mplot\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mverbose\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 9\u001b[0;31m \u001b[0mmax_cpus\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 10\u001b[0m )\n",
+ "\u001b[0;32m~/hawksight/fastquant/venv/lib/python3.7/site-packages/fastquant/backtest/backtest.py\u001b[0m in \u001b[0;36mbacktest\u001b[0;34m(strategy, data, commission, init_cash, plot, fractional, slippage, single_position, verbose, sort_by, sentiments, strats, return_history, return_plot, channel, symbol, max_cpus, allow_short, short_max, figsize, multi_line_indicators, data_class, data_kwargs, plot_kwargs, fig, **kwargs)\u001b[0m\n\u001b[1;32m 214\u001b[0m \u001b[0;31m# clock the start of the process\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 215\u001b[0m \u001b[0mtstart\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 216\u001b[0;31m \u001b[0mstratruns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcerebro\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 217\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 218\u001b[0m \u001b[0;31m# clock the end of the process\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;32m~/hawksight/fastquant/venv/lib/python3.7/site-packages/backtrader/cerebro.py\u001b[0m in \u001b[0;36mrun\u001b[0;34m(self, **kwargs)\u001b[0m\n\u001b[1;32m 1141\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1142\u001b[0m \u001b[0mpool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmultiprocessing\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPool\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmaxcpus\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1143\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mr\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mimap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterstrats\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1144\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrunstrats\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mr\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1145\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mcb\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moptcbs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;32m~/.pyenv/versions/3.7.7/lib/python3.7/multiprocessing/pool.py\u001b[0m in \u001b[0;36mnext\u001b[0;34m(self, timeout)\u001b[0m\n\u001b[1;32m 746\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0msuccess\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 747\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 748\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 749\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 750\u001b[0m \u001b[0m__next__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnext\u001b[0m \u001b[0;31m# XXX\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;32m~/.pyenv/versions/3.7.7/lib/python3.7/multiprocessing/pool.py\u001b[0m in \u001b[0;36m_handle_tasks\u001b[0;34m(taskqueue, put, outqueue, pool, cache)\u001b[0m\n\u001b[1;32m 429\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 430\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 431\u001b[0;31m \u001b[0mput\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtask\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 432\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 433\u001b[0m \u001b[0mjob\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0midx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtask\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;32m~/.pyenv/versions/3.7.7/lib/python3.7/multiprocessing/connection.py\u001b[0m in \u001b[0;36msend\u001b[0;34m(self, obj)\u001b[0m\n\u001b[1;32m 204\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_check_closed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 205\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_check_writable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 206\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_send_bytes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_ForkingPickler\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdumps\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 207\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 208\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mrecv_bytes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmaxlength\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;32m~/.pyenv/versions/3.7.7/lib/python3.7/multiprocessing/reduction.py\u001b[0m in \u001b[0;36mdumps\u001b[0;34m(cls, obj, protocol)\u001b[0m\n\u001b[1;32m 49\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mdumps\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcls\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mprotocol\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 50\u001b[0m \u001b[0mbuf\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mio\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mBytesIO\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 51\u001b[0;31m \u001b[0mcls\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbuf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mprotocol\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdump\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 52\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mbuf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgetbuffer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 53\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;31mAttributeError\u001b[0m: Can't pickle local object 'initalize_data..CustomData'"
+ ]
+ }
+ ],
+ "source": [
+ "from fastquant import backtest\n",
+ "\n",
+ "results = backtest('smac', \n",
+ " crypto, \n",
+ " fast_period=[7,14], \n",
+ " slow_period=[30,45],\n",
+ " plot=False,\n",
+ " verbose=False,\n",
+ " max_cpus=3\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " strat_id | \n",
+ " init_cash | \n",
+ " buy_prop | \n",
+ " sell_prop | \n",
+ " commission | \n",
+ " execution_type | \n",
+ " channel | \n",
+ " symbol | \n",
+ " fast_period | \n",
+ " slow_period | \n",
+ " rtot | \n",
+ " ravg | \n",
+ " rnorm | \n",
+ " rnorm100 | \n",
+ " sharperatio | \n",
+ " pnl | \n",
+ " final_value | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 3 | \n",
+ " 100000 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 0.0075 | \n",
+ " close | \n",
+ " None | \n",
+ " None | \n",
+ " 7 | \n",
+ " 75 | \n",
+ " 0.949014 | \n",
+ " 0.002397 | \n",
+ " 0.829272 | \n",
+ " 82.927229 | \n",
+ " 0.987367 | \n",
+ " 158316.23 | \n",
+ " 258316.234050 | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 14 | \n",
+ " 100000 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 0.0075 | \n",
+ " close | \n",
+ " None | \n",
+ " None | \n",
+ " 28 | \n",
+ " 60 | \n",
+ " 0.931504 | \n",
+ " 0.002352 | \n",
+ " 0.809002 | \n",
+ " 80.900205 | \n",
+ " 0.986999 | \n",
+ " 153832.42 | \n",
+ " 253832.420400 | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 7 | \n",
+ " 100000 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 0.0075 | \n",
+ " close | \n",
+ " None | \n",
+ " None | \n",
+ " 14 | \n",
+ " 75 | \n",
+ " 0.830975 | \n",
+ " 0.002098 | \n",
+ " 0.696898 | \n",
+ " 69.689847 | \n",
+ " 0.984563 | \n",
+ " 129555.54 | \n",
+ " 229555.539175 | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 11 | \n",
+ " 100000 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 0.0075 | \n",
+ " close | \n",
+ " None | \n",
+ " None | \n",
+ " 21 | \n",
+ " 75 | \n",
+ " 0.782243 | \n",
+ " 0.001975 | \n",
+ " 0.645083 | \n",
+ " 64.508323 | \n",
+ " 0.983142 | \n",
+ " 118637.07 | \n",
+ " 218637.072700 | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 15 | \n",
+ " 100000 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 0.0075 | \n",
+ " close | \n",
+ " None | \n",
+ " None | \n",
+ " 28 | \n",
+ " 75 | \n",
+ " 0.769574 | \n",
+ " 0.001943 | \n",
+ " 0.631874 | \n",
+ " 63.187426 | \n",
+ " 0.982741 | \n",
+ " 115884.74 | \n",
+ " 215884.739100 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " strat_id init_cash buy_prop sell_prop commission execution_type \\\n",
+ "0 3 100000 1 1 0.0075 close \n",
+ "1 14 100000 1 1 0.0075 close \n",
+ "2 7 100000 1 1 0.0075 close \n",
+ "3 11 100000 1 1 0.0075 close \n",
+ "4 15 100000 1 1 0.0075 close \n",
+ "\n",
+ " channel symbol fast_period slow_period rtot ravg rnorm \\\n",
+ "0 None None 7 75 0.949014 0.002397 0.829272 \n",
+ "1 None None 28 60 0.931504 0.002352 0.809002 \n",
+ "2 None None 14 75 0.830975 0.002098 0.696898 \n",
+ "3 None None 21 75 0.782243 0.001975 0.645083 \n",
+ "4 None None 28 75 0.769574 0.001943 0.631874 \n",
+ "\n",
+ " rnorm100 sharperatio pnl final_value \n",
+ "0 82.927229 0.987367 158316.23 258316.234050 \n",
+ "1 80.900205 0.986999 153832.42 253832.420400 \n",
+ "2 69.689847 0.984563 129555.54 229555.539175 \n",
+ "3 64.508323 0.983142 118637.07 218637.072700 \n",
+ "4 63.187426 0.982741 115884.74 215884.739100 "
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "results.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "That's a 258% maximum profit using only SMAC because bitcoin was bullish all time long!"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(7, 75)"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "#get best parameters on top row \n",
+ "fast_best, slow_best = results.iloc[0][[\"fast_period\",\"slow_period\"]]\n",
+ "fast_best, slow_best"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## run backtest using optimum values"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import matplotlib as pl\n",
+ "pl.style.use(\"default\")\n",
+ "pl.rcParams[\"figure.figsize\"] = (9,5)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 75\n",
+ "Final Portfolio Value: 258316.23405000006\n",
+ "Final PnL: 158316.23\n",
+ "Time used (seconds): 0.10248279571533203\n",
+ "Optimal parameters: {'init_cash': 100000, 'buy_prop': 1, 'sell_prop': 1, 'commission': 0.0075, 'execution_type': 'close', 'channel': None, 'symbol': None, 'fast_period': 7, 'slow_period': 75}\n",
+ "Optimal metrics: {'rtot': 0.9490143617322465, 'ravg': 0.002396500913465269, 'rnorm': 0.8292722866407841, 'rnorm100': 82.92722866407841, 'sharperatio': 0.9873670567519415, 'pnl': 158316.23, 'final_value': 258316.23405000006}\n"
+ ]
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "/* Put everything inside the global mpl namespace */\n",
+ "window.mpl = {};\n",
+ "\n",
+ "\n",
+ "mpl.get_websocket_type = function() {\n",
+ " if (typeof(WebSocket) !== 'undefined') {\n",
+ " return WebSocket;\n",
+ " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
+ " return MozWebSocket;\n",
+ " } else {\n",
+ " alert('Your browser does not have WebSocket support. ' +\n",
+ " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+ " 'Firefox 4 and 5 are also supported but you ' +\n",
+ " 'have to enable WebSockets in about:config.');\n",
+ " };\n",
+ "}\n",
+ "\n",
+ "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
+ " this.id = figure_id;\n",
+ "\n",
+ " this.ws = websocket;\n",
+ "\n",
+ " this.supports_binary = (this.ws.binaryType != undefined);\n",
+ "\n",
+ " if (!this.supports_binary) {\n",
+ " var warnings = document.getElementById(\"mpl-warnings\");\n",
+ " if (warnings) {\n",
+ " warnings.style.display = 'block';\n",
+ " warnings.textContent = (\n",
+ " \"This browser does not support binary websocket messages. \" +\n",
+ " \"Performance may be slow.\");\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.imageObj = new Image();\n",
+ "\n",
+ " this.context = undefined;\n",
+ " this.message = undefined;\n",
+ " this.canvas = undefined;\n",
+ " this.rubberband_canvas = undefined;\n",
+ " this.rubberband_context = undefined;\n",
+ " this.format_dropdown = undefined;\n",
+ "\n",
+ " this.image_mode = 'full';\n",
+ "\n",
+ " this.root = $('');\n",
+ " this._root_extra_style(this.root)\n",
+ " this.root.attr('style', 'display: inline-block');\n",
+ "\n",
+ " $(parent_element).append(this.root);\n",
+ "\n",
+ " this._init_header(this);\n",
+ " this._init_canvas(this);\n",
+ " this._init_toolbar(this);\n",
+ "\n",
+ " var fig = this;\n",
+ "\n",
+ " this.waiting = false;\n",
+ "\n",
+ " this.ws.onopen = function () {\n",
+ " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
+ " fig.send_message(\"send_image_mode\", {});\n",
+ " if (mpl.ratio != 1) {\n",
+ " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
+ " }\n",
+ " fig.send_message(\"refresh\", {});\n",
+ " }\n",
+ "\n",
+ " this.imageObj.onload = function() {\n",
+ " if (fig.image_mode == 'full') {\n",
+ " // Full images could contain transparency (where diff images\n",
+ " // almost always do), so we need to clear the canvas so that\n",
+ " // there is no ghosting.\n",
+ " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+ " }\n",
+ " fig.context.drawImage(fig.imageObj, 0, 0);\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onunload = function() {\n",
+ " fig.ws.close();\n",
+ " }\n",
+ "\n",
+ " this.ws.onmessage = this._make_on_message_function(this);\n",
+ "\n",
+ " this.ondownload = ondownload;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._init_header = function() {\n",
+ " var titlebar = $(\n",
+ " '');\n",
+ " var titletext = $(\n",
+ " '');\n",
+ " titlebar.append(titletext)\n",
+ " this.root.append(titlebar);\n",
+ " this.header = titletext[0];\n",
+ "}\n",
+ "\n",
+ "\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
+ "\n",
+ "}\n",
+ "\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
+ "\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._init_canvas = function() {\n",
+ " var fig = this;\n",
+ "\n",
+ " var canvas_div = $('');\n",
+ "\n",
+ " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
+ "\n",
+ " function canvas_keyboard_event(event) {\n",
+ " return fig.key_event(event, event['data']);\n",
+ " }\n",
+ "\n",
+ " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
+ " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
+ " this.canvas_div = canvas_div\n",
+ " this._canvas_extra_style(canvas_div)\n",
+ " this.root.append(canvas_div);\n",
+ "\n",
+ " var canvas = $('');\n",
+ " canvas.addClass('mpl-canvas');\n",
+ " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
+ "\n",
+ " this.canvas = canvas[0];\n",
+ " this.context = canvas[0].getContext(\"2d\");\n",
+ "\n",
+ " var backingStore = this.context.backingStorePixelRatio ||\n",
+ "\tthis.context.webkitBackingStorePixelRatio ||\n",
+ "\tthis.context.mozBackingStorePixelRatio ||\n",
+ "\tthis.context.msBackingStorePixelRatio ||\n",
+ "\tthis.context.oBackingStorePixelRatio ||\n",
+ "\tthis.context.backingStorePixelRatio || 1;\n",
+ "\n",
+ " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
+ "\n",
+ " var rubberband = $('');\n",
+ " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
+ "\n",
+ " var pass_mouse_events = true;\n",
+ "\n",
+ " canvas_div.resizable({\n",
+ " start: function(event, ui) {\n",
+ " pass_mouse_events = false;\n",
+ " },\n",
+ " resize: function(event, ui) {\n",
+ " fig.request_resize(ui.size.width, ui.size.height);\n",
+ " },\n",
+ " stop: function(event, ui) {\n",
+ " pass_mouse_events = true;\n",
+ " fig.request_resize(ui.size.width, ui.size.height);\n",
+ " },\n",
+ " });\n",
+ "\n",
+ " function mouse_event_fn(event) {\n",
+ " if (pass_mouse_events)\n",
+ " return fig.mouse_event(event, event['data']);\n",
+ " }\n",
+ "\n",
+ " rubberband.mousedown('button_press', mouse_event_fn);\n",
+ " rubberband.mouseup('button_release', mouse_event_fn);\n",
+ " // Throttle sequential mouse events to 1 every 20ms.\n",
+ " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
+ "\n",
+ " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
+ " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
+ "\n",
+ " canvas_div.on(\"wheel\", function (event) {\n",
+ " event = event.originalEvent;\n",
+ " event['data'] = 'scroll'\n",
+ " if (event.deltaY < 0) {\n",
+ " event.step = 1;\n",
+ " } else {\n",
+ " event.step = -1;\n",
+ " }\n",
+ " mouse_event_fn(event);\n",
+ " });\n",
+ "\n",
+ " canvas_div.append(canvas);\n",
+ " canvas_div.append(rubberband);\n",
+ "\n",
+ " this.rubberband = rubberband;\n",
+ " this.rubberband_canvas = rubberband[0];\n",
+ " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
+ " this.rubberband_context.strokeStyle = \"#000000\";\n",
+ "\n",
+ " this._resize_canvas = function(width, height) {\n",
+ " // Keep the size of the canvas, canvas container, and rubber band\n",
+ " // canvas in synch.\n",
+ " canvas_div.css('width', width)\n",
+ " canvas_div.css('height', height)\n",
+ "\n",
+ " canvas.attr('width', width * mpl.ratio);\n",
+ " canvas.attr('height', height * mpl.ratio);\n",
+ " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
+ "\n",
+ " rubberband.attr('width', width);\n",
+ " rubberband.attr('height', height);\n",
+ " }\n",
+ "\n",
+ " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
+ " // upon first draw.\n",
+ " this._resize_canvas(600, 600);\n",
+ "\n",
+ " // Disable right mouse context menu.\n",
+ " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
+ " return false;\n",
+ " });\n",
+ "\n",
+ " function set_focus () {\n",
+ " canvas.focus();\n",
+ " canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " window.setTimeout(set_focus, 100);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function() {\n",
+ " var fig = this;\n",
+ "\n",
+ " var nav_element = $('');\n",
+ " nav_element.attr('style', 'width: 100%');\n",
+ " this.root.append(nav_element);\n",
+ "\n",
+ " // Define a callback function for later on.\n",
+ " function toolbar_event(event) {\n",
+ " return fig.toolbar_button_onclick(event['data']);\n",
+ " }\n",
+ " function toolbar_mouse_event(event) {\n",
+ " return fig.toolbar_button_onmouseover(event['data']);\n",
+ " }\n",
+ "\n",
+ " for(var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " // put a spacer in here.\n",
+ " continue;\n",
+ " }\n",
+ " var button = $('');\n",
+ " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
+ " 'ui-button-icon-only');\n",
+ " button.attr('role', 'button');\n",
+ " button.attr('aria-disabled', 'false');\n",
+ " button.click(method_name, toolbar_event);\n",
+ " button.mouseover(tooltip, toolbar_mouse_event);\n",
+ "\n",
+ " var icon_img = $('');\n",
+ " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
+ " icon_img.addClass(image);\n",
+ " icon_img.addClass('ui-corner-all');\n",
+ "\n",
+ " var tooltip_span = $('');\n",
+ " tooltip_span.addClass('ui-button-text');\n",
+ " tooltip_span.html(tooltip);\n",
+ "\n",
+ " button.append(icon_img);\n",
+ " button.append(tooltip_span);\n",
+ "\n",
+ " nav_element.append(button);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker_span = $('');\n",
+ "\n",
+ " var fmt_picker = $('');\n",
+ " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
+ " fmt_picker_span.append(fmt_picker);\n",
+ " nav_element.append(fmt_picker_span);\n",
+ " this.format_dropdown = fmt_picker[0];\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = $(\n",
+ " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
+ " fmt_picker.append(option);\n",
+ " }\n",
+ "\n",
+ " // Add hover states to the ui-buttons\n",
+ " $( \".ui-button\" ).hover(\n",
+ " function() { $(this).addClass(\"ui-state-hover\");},\n",
+ " function() { $(this).removeClass(\"ui-state-hover\");}\n",
+ " );\n",
+ "\n",
+ " var status_bar = $('');\n",
+ " nav_element.append(status_bar);\n",
+ " this.message = status_bar[0];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function(type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function() {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "}\n",
+ "\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1]);\n",
+ " fig.send_message(\"refresh\", {});\n",
+ " };\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
+ " var x0 = msg['x0'] / mpl.ratio;\n",
+ " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
+ " var x1 = msg['x1'] / mpl.ratio;\n",
+ " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0, 0, fig.canvas.width / mpl.ratio, fig.canvas.height / mpl.ratio);\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
+ " var cursor = msg['cursor'];\n",
+ " switch(cursor)\n",
+ " {\n",
+ " case 0:\n",
+ " cursor = 'pointer';\n",
+ " break;\n",
+ " case 1:\n",
+ " cursor = 'default';\n",
+ " break;\n",
+ " case 2:\n",
+ " cursor = 'crosshair';\n",
+ " break;\n",
+ " case 3:\n",
+ " cursor = 'move';\n",
+ " break;\n",
+ " }\n",
+ " fig.rubberband_canvas.style.cursor = cursor;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function() {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message(\"ack\", {});\n",
+ "}\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " evt.data.type = \"image/png\";\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src);\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " evt.data);\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig[\"handle_\" + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "}\n",
+ "\n",
+ "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
+ "mpl.findpos = function(e) {\n",
+ " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
+ " var targ;\n",
+ " if (!e)\n",
+ " e = window.event;\n",
+ " if (e.target)\n",
+ " targ = e.target;\n",
+ " else if (e.srcElement)\n",
+ " targ = e.srcElement;\n",
+ " if (targ.nodeType == 3) // defeat Safari bug\n",
+ " targ = targ.parentNode;\n",
+ "\n",
+ " // jQuery normalizes the pageX and pageY\n",
+ " // pageX,Y are the mouse positions relative to the document\n",
+ " // offset() returns the position of the element relative to the document\n",
+ " var x = e.pageX - $(targ).offset().left;\n",
+ " var y = e.pageY - $(targ).offset().top;\n",
+ "\n",
+ " return {\"x\": x, \"y\": y};\n",
+ "};\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * http://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys (original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object')\n",
+ " obj[key] = original[key]\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function(event, name) {\n",
+ " var canvas_pos = mpl.findpos(event)\n",
+ "\n",
+ " if (name === 'button_press')\n",
+ " {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " var x = canvas_pos.x * mpl.ratio;\n",
+ " var y = canvas_pos.y * mpl.ratio;\n",
+ "\n",
+ " this.send_message(name, {x: x, y: y, button: event.button,\n",
+ " step: event.step,\n",
+ " guiEvent: simpleKeys(event)});\n",
+ "\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We want\n",
+ " * to control all of the cursor setting manually through the\n",
+ " * 'cursor' event from matplotlib */\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function(event, name) {\n",
+ "\n",
+ " // Prevent repeat events\n",
+ " if (name == 'key_press')\n",
+ " {\n",
+ " if (event.which === this._key)\n",
+ " return;\n",
+ " else\n",
+ " this._key = event.which;\n",
+ " }\n",
+ " if (name == 'key_release')\n",
+ " this._key = null;\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.which != 17)\n",
+ " value += \"ctrl+\";\n",
+ " if (event.altKey && event.which != 18)\n",
+ " value += \"alt+\";\n",
+ " if (event.shiftKey && event.which != 16)\n",
+ " value += \"shift+\";\n",
+ "\n",
+ " value += 'k';\n",
+ " value += event.which.toString();\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, {key: value,\n",
+ " guiEvent: simpleKeys(event)});\n",
+ " return false;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
+ " if (name == 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message(\"toolbar_button\", {name: name});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.close = function() {\n",
+ " comm.close()\n",
+ " };\n",
+ " ws.send = function(m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function(msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
+ " ws.onmessage(msg['content']['data'])\n",
+ " });\n",
+ " return ws;\n",
+ "}\n",
+ "\n",
+ "mpl.mpl_figure_comm = function(comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = $(\"#\" + id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm)\n",
+ "\n",
+ " function ondownload(figure, format) {\n",
+ " window.open(figure.imageObj.src);\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy,\n",
+ " ondownload,\n",
+ " element.get(0));\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element.get(0);\n",
+ " fig.cell_info = mpl.find_output_cell(\"\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error(\"Failed to find cell for figure\", id, fig);\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var output_index = fig.cell_info[2]\n",
+ " var cell = fig.cell_info[0];\n",
+ "\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
+ " var width = fig.canvas.width/mpl.ratio\n",
+ " fig.root.unbind('remove')\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable()\n",
+ " $(fig.parent_element).html('
');\n",
+ " fig.close_ws(fig, msg);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function(fig, msg){\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var width = this.canvas.width/mpl.ratio\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] = '
';\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function() {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message(\"ack\", {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () { fig.push_to_output() }, 1000);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function() {\n",
+ " var fig = this;\n",
+ "\n",
+ " var nav_element = $('');\n",
+ " nav_element.attr('style', 'width: 100%');\n",
+ " this.root.append(nav_element);\n",
+ "\n",
+ " // Define a callback function for later on.\n",
+ " function toolbar_event(event) {\n",
+ " return fig.toolbar_button_onclick(event['data']);\n",
+ " }\n",
+ " function toolbar_mouse_event(event) {\n",
+ " return fig.toolbar_button_onmouseover(event['data']);\n",
+ " }\n",
+ "\n",
+ " for(var toolbar_ind in mpl.toolbar_items){\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) { continue; };\n",
+ "\n",
+ " var button = $('');\n",
+ " button.click(method_name, toolbar_event);\n",
+ " button.mouseover(tooltip, toolbar_mouse_event);\n",
+ " nav_element.append(button);\n",
+ " }\n",
+ "\n",
+ " // Add the status bar.\n",
+ " var status_bar = $('');\n",
+ " nav_element.append(status_bar);\n",
+ " this.message = status_bar[0];\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = $('');\n",
+ " var button = $('');\n",
+ " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
+ " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
+ " buttongrp.append(button);\n",
+ " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
+ " titlebar.prepend(buttongrp);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function(el){\n",
+ " var fig = this\n",
+ " el.on(\"remove\", function(){\n",
+ "\tfig.close_ws(fig, {});\n",
+ " });\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function(el){\n",
+ " // this is important to make the div 'focusable\n",
+ " el.attr('tabindex', 0)\n",
+ " // reach out to IPython and tell the keyboard manager to turn it's self\n",
+ " // off when our div gets focus\n",
+ "\n",
+ " // location in version 3\n",
+ " if (IPython.notebook.keyboard_manager) {\n",
+ " IPython.notebook.keyboard_manager.register_events(el);\n",
+ " }\n",
+ " else {\n",
+ " // location in version 2\n",
+ " IPython.keyboard_manager.register_events(el);\n",
+ " }\n",
+ "\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
+ " var manager = IPython.notebook.keyboard_manager;\n",
+ " if (!manager)\n",
+ " manager = IPython.keyboard_manager;\n",
+ "\n",
+ " // Check for shift+enter\n",
+ " if (event.shiftKey && event.which == 13) {\n",
+ " this.canvas_div.blur();\n",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
+ " fig.ondownload(fig, null);\n",
+ "}\n",
+ "\n",
+ "\n",
+ "mpl.find_output_cell = function(html_output) {\n",
+ " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+ " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+ " // IPython event is triggered only after the cells have been serialised, which for\n",
+ " // our purposes (turning an active figure into a static one), is too late.\n",
+ " var cells = IPython.notebook.get_cells();\n",
+ " var ncells = cells.length;\n",
+ " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
+ " data = data.data;\n",
+ " }\n",
+ " if (data['text/html'] == html_output) {\n",
+ " return [cell, data, j];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// Register the function which deals with the matplotlib target/channel.\n",
+ "// The kernel may be null if the page has been refreshed.\n",
+ "if (IPython.notebook.kernel != null) {\n",
+ " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "results = backtest('smac', \n",
+ " crypto, \n",
+ " fast_period=fast_best, \n",
+ " slow_period=slow_best,\n",
+ " plot=True,\n",
+ " verbose=False\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## accessing history of transactions"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This is done by setting `return_history`=True."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### single strategy"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 75\n",
+ "Final Portfolio Value: 258316.23405000006\n",
+ "Final PnL: 158316.23\n",
+ "Time used (seconds): 0.10444140434265137\n",
+ "Optimal parameters: {'init_cash': 100000, 'buy_prop': 1, 'sell_prop': 1, 'commission': 0.0075, 'execution_type': 'close', 'channel': None, 'symbol': None, 'fast_period': 7, 'slow_period': 75}\n",
+ "Optimal metrics: {'rtot': 0.9490143617322465, 'ravg': 0.002396500913465269, 'rnorm': 0.8292722866407841, 'rnorm100': 82.92722866407841, 'sharperatio': 0.9873670567519415, 'pnl': 158316.23, 'final_value': 258316.23405000006}\n"
+ ]
+ }
+ ],
+ "source": [
+ "results, history = backtest('smac', \n",
+ " crypto, \n",
+ " fast_period=fast_best, \n",
+ " slow_period=slow_best,\n",
+ " plot=False,\n",
+ " verbose=False,\n",
+ " return_history=True\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " strat_id | \n",
+ " init_cash | \n",
+ " buy_prop | \n",
+ " sell_prop | \n",
+ " commission | \n",
+ " execution_type | \n",
+ " channel | \n",
+ " symbol | \n",
+ " fast_period | \n",
+ " slow_period | \n",
+ " rtot | \n",
+ " ravg | \n",
+ " rnorm | \n",
+ " rnorm100 | \n",
+ " sharperatio | \n",
+ " pnl | \n",
+ " final_value | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 100000 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 0.0075 | \n",
+ " close | \n",
+ " None | \n",
+ " None | \n",
+ " 7 | \n",
+ " 75 | \n",
+ " 0.949014 | \n",
+ " 0.002397 | \n",
+ " 0.829272 | \n",
+ " 82.927229 | \n",
+ " 0.987367 | \n",
+ " 158316.23 | \n",
+ " 258316.23405 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " strat_id init_cash buy_prop sell_prop commission execution_type \\\n",
+ "0 0 100000 1 1 0.0075 close \n",
+ "\n",
+ " channel symbol fast_period slow_period rtot ravg rnorm \\\n",
+ "0 None None 7 75 0.949014 0.002397 0.829272 \n",
+ "\n",
+ " rnorm100 sharperatio pnl final_value \n",
+ "0 82.927229 0.987367 158316.23 258316.23405 "
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "dict_keys(['orders', 'periodic'])"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "history.keys()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " strat_id | \n",
+ " strat_name | \n",
+ " dt | \n",
+ " type | \n",
+ " price | \n",
+ " size | \n",
+ " value | \n",
+ " commission | \n",
+ " pnl | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 0 | \n",
+ " fast_period7_slow_period75 | \n",
+ " 2019-02-15 | \n",
+ " buy | \n",
+ " 3590.56 | \n",
+ " 27 | \n",
+ " 96945.12 | \n",
+ " 727.088400 | \n",
+ " 0.00 | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 0 | \n",
+ " fast_period7_slow_period75 | \n",
+ " 2019-08-21 | \n",
+ " sell | \n",
+ " 10142.57 | \n",
+ " -27 | \n",
+ " 96945.12 | \n",
+ " 2053.870425 | \n",
+ " 176904.27 | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 0 | \n",
+ " fast_period7_slow_period75 | \n",
+ " 2019-11-02 | \n",
+ " buy | \n",
+ " 9231.61 | \n",
+ " 29 | \n",
+ " 267716.69 | \n",
+ " 2007.875175 | \n",
+ " 0.00 | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 0 | \n",
+ " fast_period7_slow_period75 | \n",
+ " 2019-11-12 | \n",
+ " sell | \n",
+ " 8821.94 | \n",
+ " -29 | \n",
+ " 267716.69 | \n",
+ " 1918.771950 | \n",
+ " -11880.43 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " strat_id strat_name dt type price size \\\n",
+ "0 0 fast_period7_slow_period75 2019-02-15 buy 3590.56 27 \n",
+ "1 0 fast_period7_slow_period75 2019-08-21 sell 10142.57 -27 \n",
+ "2 0 fast_period7_slow_period75 2019-11-02 buy 9231.61 29 \n",
+ "3 0 fast_period7_slow_period75 2019-11-12 sell 8821.94 -29 \n",
+ "\n",
+ " value commission pnl \n",
+ "0 96945.12 727.088400 0.00 \n",
+ "1 96945.12 2053.870425 176904.27 \n",
+ "2 267716.69 2007.875175 0.00 \n",
+ "3 267716.69 1918.771950 -11880.43 "
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "orders = history['orders']\n",
+ "orders"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The final value in `results` can be calculated from the `commission` and `pnl` (profit & loss) of all the closed (bought and sold) transactions in history:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "258316.23405000006"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "r = results.squeeze()\n",
+ "r.final_value"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "258316.23405000003"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "r.init_cash + orders.pnl.sum() - orders.commission.sum()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### single signal strategy with grid search"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 30\n",
+ "Final Portfolio Value: 167957.05730000004\n",
+ "Final PnL: 67957.06\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 45\n",
+ "Final Portfolio Value: 200109.894525\n",
+ "Final PnL: 100109.89\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 60\n",
+ "Final Portfolio Value: 189298.80590000006\n",
+ "Final PnL: 89298.81\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 30\n",
+ "Final Portfolio Value: 161429.22347500004\n",
+ "Final PnL: 61429.22\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 45\n",
+ "Final Portfolio Value: 166675.70495000004\n",
+ "Final PnL: 66675.7\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 60\n",
+ "Final Portfolio Value: 149527.12537499995\n",
+ "Final PnL: 49527.13\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 21\n",
+ "slow_period : 30\n",
+ "Final Portfolio Value: 119204.3985\n",
+ "Final PnL: 19204.4\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 21\n",
+ "slow_period : 45\n",
+ "Final Portfolio Value: 162617.28744999995\n",
+ "Final PnL: 62617.29\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 21\n",
+ "slow_period : 60\n",
+ "Final Portfolio Value: 185407.30802499995\n",
+ "Final PnL: 85407.31\n",
+ "Time used (seconds): 0.7071244716644287\n",
+ "Optimal parameters: {'init_cash': 100000, 'buy_prop': 1, 'sell_prop': 1, 'commission': 0.0075, 'execution_type': 'close', 'channel': None, 'symbol': None, 'fast_period': 7, 'slow_period': 45}\n",
+ "Optimal metrics: {'rtot': 0.6936965022801388, 'ravg': 0.0017517588441417647, 'rnorm': 0.5549497480208785, 'rnorm100': 55.494974802087846, 'sharperatio': 0.9800219547779011, 'pnl': 100109.89, 'final_value': 200109.894525}\n"
+ ]
+ }
+ ],
+ "source": [
+ "results, history = backtest('smac', \n",
+ " crypto, \n",
+ " fast_period=[7,14,21], \n",
+ " slow_period=[30,45,60],\n",
+ " plot=False,\n",
+ " verbose=False,\n",
+ " return_history=True\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(array([0, 1, 2, 3, 4, 5, 6, 7, 8]),\n",
+ " array(['fast_period7_slow_period30', 'fast_period7_slow_period45',\n",
+ " 'fast_period7_slow_period60', 'fast_period14_slow_period30',\n",
+ " 'fast_period14_slow_period45', 'fast_period14_slow_period60',\n",
+ " 'fast_period21_slow_period30', 'fast_period21_slow_period45',\n",
+ " 'fast_period21_slow_period60'], dtype=object))"
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "orders = history['orders']\n",
+ "orders.strat_id.unique(), orders.strat_name.unique()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "key = 'strat_id'\n",
+ "periodic = history['periodic']\n",
+ "g = periodic.set_index('dt').groupby(key)\n",
+ "axs = g.portfolio_value.plot(legend=key)\n",
+ "axs[0].set_ylabel('Returns')\n",
+ "axs[0].legend(g.groups, title=key, bbox_to_anchor=(1.01, 1), loc='upper left')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### multi signal strategy"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 96154.84370000003\n",
+ "Final PnL: -3845.16\n",
+ "Final Portfolio Value: 96154.84370000003\n",
+ "Final PnL: -3845.16\n",
+ "Time used (seconds): 0.12853026390075684\n",
+ "Optimal parameters: {'init_cash': 100000, 'buy_prop': 1, 'sell_prop': 1, 'smac.commission': 0.0075, 'execution_type': 'close', 'smac.channel': None, 'smac.symbol': None, 'smac.fast_period': 7, 'smac.slow_period': 60, 'rsi.commission': 0.0075, 'rsi.channel': None, 'rsi.symbol': None, 'rsi.rsi_period': 14, 'rsi.rsi_upper': 70, 'rsi.rsi_lower': 30}\n",
+ "Optimal metrics: {'rtot': -0.039210338727095576, 'ravg': -9.901600688660499e-05, 'rnorm': -0.024643304876387637, 'rnorm100': -2.464330487638764, 'sharperatio': -0.12300657942849803, 'pnl': -3845.16, 'final_value': 96154.84370000003}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from fastquant import backtest\n",
+ "\n",
+ "strats= {\n",
+ " 'smac': {\n",
+ " 'fast_period': 7, \n",
+ " 'slow_period': 60\n",
+ " },\n",
+ " 'rsi': {\n",
+ " 'rsi_upper': 70,\n",
+ " 'rsi_lower': 30 \n",
+ " }\n",
+ " }\n",
+ "results, history = backtest('multi', \n",
+ " crypto, \n",
+ " strats=strats,\n",
+ " plot=False,\n",
+ " verbose=False,\n",
+ " return_history=True\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " strat_id | \n",
+ " init_cash | \n",
+ " buy_prop | \n",
+ " sell_prop | \n",
+ " smac.commission | \n",
+ " execution_type | \n",
+ " smac.channel | \n",
+ " smac.symbol | \n",
+ " smac.fast_period | \n",
+ " smac.slow_period | \n",
+ " ... | \n",
+ " rsi.rsi_period | \n",
+ " rsi.rsi_upper | \n",
+ " rsi.rsi_lower | \n",
+ " rtot | \n",
+ " ravg | \n",
+ " rnorm | \n",
+ " rnorm100 | \n",
+ " sharperatio | \n",
+ " pnl | \n",
+ " final_value | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 100000 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 0.0075 | \n",
+ " close | \n",
+ " None | \n",
+ " None | \n",
+ " 7 | \n",
+ " 60 | \n",
+ " ... | \n",
+ " 14 | \n",
+ " 70 | \n",
+ " 30 | \n",
+ " -0.03921 | \n",
+ " -0.000099 | \n",
+ " -0.024643 | \n",
+ " -2.46433 | \n",
+ " -0.123007 | \n",
+ " -3845.16 | \n",
+ " 96154.8437 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
1 rows × 23 columns
\n",
+ "
"
+ ],
+ "text/plain": [
+ " strat_id init_cash buy_prop sell_prop smac.commission execution_type \\\n",
+ "0 0 100000 1 1 0.0075 close \n",
+ "\n",
+ " smac.channel smac.symbol smac.fast_period smac.slow_period ... \\\n",
+ "0 None None 7 60 ... \n",
+ "\n",
+ " rsi.rsi_period rsi.rsi_upper rsi.rsi_lower rtot ravg rnorm \\\n",
+ "0 14 70 30 -0.03921 -0.000099 -0.024643 \n",
+ "\n",
+ " rnorm100 sharperatio pnl final_value \n",
+ "0 -2.46433 -0.123007 -3845.16 96154.8437 \n",
+ "\n",
+ "[1 rows x 23 columns]"
+ ]
+ },
+ "execution_count": 19,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(array([0]),\n",
+ " array(['smac.fast_period7_slow_period60', 'rsi.rsi_upper70_rsi_lower30'],\n",
+ " dtype=object))"
+ ]
+ },
+ "execution_count": 20,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "orders = history['orders']\n",
+ "orders.strat_id.unique(), orders.strat_name.unique()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 30\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 20\n",
+ "Final Portfolio Value: 101479.475225\n",
+ "Final PnL: 1479.48\n",
+ "Final Portfolio Value: 101479.475225\n",
+ "Final PnL: 1479.48\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 30\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 92990.43297500002\n",
+ "Final PnL: -7009.57\n",
+ "Final Portfolio Value: 92990.43297500002\n",
+ "Final PnL: -7009.57\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 30\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 80\n",
+ "rsi_lower : 20\n",
+ "Final Portfolio Value: 165538.20930000002\n",
+ "Final PnL: 65538.21\n",
+ "Final Portfolio Value: 165538.20930000002\n",
+ "Final PnL: 65538.21\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 30\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 80\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 151316.80185000005\n",
+ "Final PnL: 51316.8\n",
+ "Final Portfolio Value: 151316.80185000005\n",
+ "Final PnL: 51316.8\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 20\n",
+ "Final Portfolio Value: 107714.30350000001\n",
+ "Final PnL: 7714.3\n",
+ "Final Portfolio Value: 107714.30350000001\n",
+ "Final PnL: 7714.3\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 96154.84370000003\n",
+ "Final PnL: -3845.16\n",
+ "Final Portfolio Value: 96154.84370000003\n",
+ "Final PnL: -3845.16\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 80\n",
+ "rsi_lower : 20\n",
+ "Final Portfolio Value: 134756.84680000003\n",
+ "Final PnL: 34756.85\n",
+ "Final Portfolio Value: 134756.84680000003\n",
+ "Final PnL: 34756.85\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 7\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 80\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 120430.34112500004\n",
+ "Final PnL: 20430.34\n",
+ "Final Portfolio Value: 120430.34112500004\n",
+ "Final PnL: 20430.34\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 30\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 20\n",
+ "Final Portfolio Value: 92345.06152500001\n",
+ "Final PnL: -7654.94\n",
+ "Final Portfolio Value: 92345.06152500001\n",
+ "Final PnL: -7654.94\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 30\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 83860.13070000001\n",
+ "Final PnL: -16139.87\n",
+ "Final Portfolio Value: 83860.13070000001\n",
+ "Final PnL: -16139.87\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 30\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 80\n",
+ "rsi_lower : 20\n",
+ "Final Portfolio Value: 127624.16555000003\n",
+ "Final PnL: 27624.17\n",
+ "Final Portfolio Value: 127624.16555000003\n",
+ "Final PnL: 27624.17\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 30\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 80\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 115264.11515000004\n",
+ "Final PnL: 15264.12\n",
+ "Final Portfolio Value: 115264.11515000004\n",
+ "Final PnL: 15264.12\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 20\n",
+ "Final Portfolio Value: 90231.50885000001\n",
+ "Final PnL: -9768.49\n",
+ "Final Portfolio Value: 90231.50885000001\n",
+ "Final PnL: -9768.49\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 70\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 84980.43325000003\n",
+ "Final PnL: -15019.57\n",
+ "Final Portfolio Value: 84980.43325000003\n",
+ "Final PnL: -15019.57\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 80\n",
+ "rsi_lower : 20\n",
+ "Final Portfolio Value: 111348.934\n",
+ "Final PnL: 11348.93\n",
+ "Final Portfolio Value: 111348.934\n",
+ "Final PnL: 11348.93\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "fast_period : 14\n",
+ "slow_period : 60\n",
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "rsi_period : 14\n",
+ "rsi_upper : 80\n",
+ "rsi_lower : 30\n",
+ "Final Portfolio Value: 104417.4315\n",
+ "Final PnL: 4417.43\n",
+ "Final Portfolio Value: 104417.4315\n",
+ "Final PnL: 4417.43\n",
+ "Time used (seconds): 2.0397427082061768\n",
+ "Optimal parameters: {'init_cash': 100000, 'buy_prop': 1, 'sell_prop': 1, 'smac.commission': 0.0075, 'execution_type': 'close', 'smac.channel': None, 'smac.symbol': None, 'smac.fast_period': 7, 'smac.slow_period': 30, 'rsi.commission': 0.0075, 'rsi.channel': None, 'rsi.symbol': None, 'rsi.rsi_period': 14, 'rsi.rsi_upper': 80, 'rsi.rsi_lower': 20}\n",
+ "Optimal metrics: {'rtot': 0.5040318540855331, 'ravg': 0.0012728077123372048, 'rnorm': 0.37815761213727767, 'rnorm100': 37.81576121372777, 'sharperatio': 1.834688207984273, 'pnl': 65538.21, 'final_value': 165538.20930000002}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from fastquant import backtest\n",
+ "\n",
+ "strats= {\n",
+ " 'smac': {\n",
+ " 'fast_period': [7,14], \n",
+ " 'slow_period': [30,60]\n",
+ " },\n",
+ " 'rsi': {\n",
+ " 'rsi_upper': [70,80],\n",
+ " 'rsi_lower': [20,30] \n",
+ " }\n",
+ " }\n",
+ "results, history = backtest('multi', \n",
+ " crypto, \n",
+ " strats=strats,\n",
+ " plot=False,\n",
+ " verbose=False,\n",
+ " return_history=True\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),\n",
+ " array(['smac.fast_period7_slow_period30', 'rsi.rsi_upper70_rsi_lower20',\n",
+ " 'rsi.rsi_upper70_rsi_lower30', 'rsi.rsi_upper80_rsi_lower20',\n",
+ " 'rsi.rsi_upper80_rsi_lower30', 'smac.fast_period7_slow_period60',\n",
+ " 'smac.fast_period14_slow_period30',\n",
+ " 'smac.fast_period14_slow_period60'], dtype=object))"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "orders = history['orders']\n",
+ "orders.strat_id.unique(), orders.strat_name.unique()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### custom strategy"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "\n",
+ "#add a column which is a proxy buy/sell indicator for custom strategy\n",
+ "crypto[\"custom\"] = crypto.close.pct_change()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "===Global level arguments===\n",
+ "init_cash : 100000\n",
+ "buy_prop : 1\n",
+ "sell_prop : 1\n",
+ "commission : 0.0075\n",
+ "===Strategy level arguments===\n",
+ "Upper limit: 0.05\n",
+ "Lower limit: -0.05\n",
+ "Final Portfolio Value: 165576.88775000008\n",
+ "Final PnL: 65576.89\n",
+ "Time used (seconds): 0.12016487121582031\n",
+ "Optimal parameters: {'init_cash': 100000, 'buy_prop': 1, 'sell_prop': 1, 'commission': 0.0075, 'execution_type': 'close', 'channel': None, 'symbol': None, 'upper_limit': 0.05, 'lower_limit': -0.05, 'custom_column': 'custom'}\n",
+ "Optimal metrics: {'rtot': 0.5042654794956734, 'ravg': 0.0012733976754941247, 'rnorm': 0.3783625190470573, 'rnorm100': 37.83625190470573, 'sharperatio': 0.7416913074113402, 'pnl': 65576.89, 'final_value': 165576.88775000008}\n"
+ ]
+ }
+ ],
+ "source": [
+ "results, history = backtest('custom', \n",
+ " crypto, \n",
+ " upper_limit=0.05, \n",
+ " lower_limit=-0.05,\n",
+ " plot=False,\n",
+ " verbose=False,\n",
+ " return_history=True\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(array([0]), array(['upper_limit0.05_lower_limit-0.05'], dtype=object))"
+ ]
+ },
+ "execution_count": 25,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "orders = history['orders']\n",
+ "orders.strat_id.unique(), orders.strat_name.unique()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "fq-multi",
+ "language": "python",
+ "name": "fq-multi"
+ },
+ "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.7.7"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {},
+ "toc_section_display": true,
+ "toc_window_display": false
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/python/fastquant/backtest/backtest.py b/python/fastquant/backtest/backtest.py
index ebbf7e8e..a6f82a7f 100644
--- a/python/fastquant/backtest/backtest.py
+++ b/python/fastquant/backtest/backtest.py
@@ -69,6 +69,7 @@ def backtest(
return_plot=False,
channel="",
symbol="",
+ max_cpus=1,
allow_short=False,
short_max=1.5,
figsize=(30, 15),
@@ -109,6 +110,8 @@ def backtest(
Channel to be used for notifications - e.g. "slack" (default=None)
symbol : str
Symbol to be referenced in the channel notification if not None (default=None)
+ max_cpus : int
+ Determines how many cores will be used. Value of None means that the max cores will be used. This is 1 by default
allow_short : bool
Whether to allow short selling, with max set as `short_max` times the portfolio value (default=False)
short_max : float
@@ -125,7 +128,7 @@ def backtest(
"""
# Setting initial support for 1 cpu
# Return the full strategy object to get all run information
- cerebro = bt.Cerebro(stdstats=False, maxcpus=1, optreturn=False)
+ cerebro = bt.Cerebro(stdstats=False, maxcpus=max_cpus, optreturn=False)
cerebro.addobserver(bt.observers.Broker)
cerebro.addobserver(bt.observers.Trades)
cerebro.addobserver(bt.observers.BuySell)
@@ -164,9 +167,7 @@ def backtest(
# Allow instance of BaseStrategy or from the predefined mapping
if not isinstance(strategy, str) and issubclass(strategy, bt.Strategy):
strat_name = (
- strategy.__name__
- if hasattr(strategy, "__name__")
- else str(strategy)
+ strategy.__name__ if hasattr(strategy, "__name__") else str(strategy)
)
else:
strat_name = strategy
@@ -255,9 +256,7 @@ def backtest(
**optim_params,
)
else:
- fig = plot_results(
- cerebro, data_format_dict, figsize, **plot_kwargs
- )
+ fig = plot_results(cerebro, data_format_dict, figsize, **plot_kwargs)
if return_history and return_plot:
return sorted_combined_df, history_dict, fig
diff --git a/python/fastquant/backtest/backtest_indicators.py b/python/fastquant/backtest/backtest_indicators.py
index 540b0d75..3b6af1f5 100644
--- a/python/fastquant/backtest/backtest_indicators.py
+++ b/python/fastquant/backtest/backtest_indicators.py
@@ -106,9 +106,7 @@ def get_indicators_as_dict(strat_run, multi_line_indicators):
indicators_dict = dict()
for i, ind in enumerate(indicators):
indicator_name = (
- ind.plotlabel()
- if hasattr(ind, "plotlabel")
- else "indicator{}".format(i)
+ ind.plotlabel() if hasattr(ind, "plotlabel") else "indicator{}".format(i)
)
# Check if indicator contains multiple lines
@@ -138,7 +136,5 @@ def rename_indicator(name, line_name=None):
# Changes the name to ___
tokens = indicator_regex.findall(name)
if line_name:
- tokens = [tokens[0], line_name] + (
- tokens[1:] if len(tokens) > 1 else []
- )
+ tokens = [tokens[0], line_name] + (tokens[1:] if len(tokens) > 1 else [])
return "_".join(tokens)
diff --git a/python/fastquant/backtest/data_prep.py b/python/fastquant/backtest/data_prep.py
index f37de36b..7c206507 100644
--- a/python/fastquant/backtest/data_prep.py
+++ b/python/fastquant/backtest/data_prep.py
@@ -107,9 +107,7 @@ def include_sentiment_score(data, sentiments):
senti_series = pd.Series(sentiments, name="sentiment_score", dtype=float)
# join and reset the index for dt to become the first column
- data = data.merge(
- senti_series, left_index=True, right_index=True, how="left"
- )
+ data = data.merge(senti_series, left_index=True, right_index=True, how="left")
data = data.reset_index()
return data
diff --git a/python/fastquant/backtest/post_backtest.py b/python/fastquant/backtest/post_backtest.py
index 56cfdae7..2797fa0c 100644
--- a/python/fastquant/backtest/post_backtest.py
+++ b/python/fastquant/backtest/post_backtest.py
@@ -49,12 +49,9 @@ def analyze_strategies(
for i, strat in enumerate(stratrun):
# Get indicator history
st_dtime = [
- bt.utils.date.num2date(num)
- for num in strat.lines.datetime.plot()
+ bt.utils.date.num2date(num) for num in strat.lines.datetime.plot()
]
- indicators_dict = get_indicators_as_dict(
- strat, multi_line_indicators
- )
+ indicators_dict = get_indicators_as_dict(strat, multi_line_indicators)
indicators_df = pd.DataFrame(indicators_dict)
indicators_df.insert(0, "dt", st_dtime)
@@ -106,9 +103,7 @@ def analyze_strategies(
order_history_dfs.append(order_history_df)
periodic_history_df = strat.periodic_history_df
- periodic_history_df["dt"] = pd.to_datetime(
- periodic_history_df.dt
- )
+ periodic_history_df["dt"] = pd.to_datetime(periodic_history_df.dt)
periodic_history_df.insert(0, "strat_name", history_key)
periodic_history_df.insert(0, "strat_id", strat_idx)
periodic_history_df[
@@ -143,18 +138,12 @@ def analyze_strategies(
total = np.nan
if "won" in tradeanalyzer.keys():
- win_rate = (
- tradeanalyzer["won"]["total"] / tradeanalyzer["total"]["total"]
- )
+ win_rate = tradeanalyzer["won"]["total"] / tradeanalyzer["total"]["total"]
won = tradeanalyzer["won"]["total"]
won_avg = tradeanalyzer["won"]["pnl"]["average"]
- won_avg_prcnt = (
- tradeanalyzer["won"]["pnl"]["average"] / init_cash * 100
- )
+ won_avg_prcnt = tradeanalyzer["won"]["pnl"]["average"] / init_cash * 100
won_max = tradeanalyzer["won"]["pnl"]["max"]
- won_max_prcnt = (
- tradeanalyzer["won"]["pnl"]["max"] / init_cash * 100
- )
+ won_max_prcnt = tradeanalyzer["won"]["pnl"]["max"] / init_cash * 100
else:
win_rate = np.nan
won = np.nan
@@ -166,13 +155,9 @@ def analyze_strategies(
if "lost" in tradeanalyzer.keys():
lost = tradeanalyzer["lost"]["total"]
lost_avg = tradeanalyzer["lost"]["pnl"]["average"]
- lost_avg_prcnt = (
- tradeanalyzer["lost"]["pnl"]["average"] / init_cash * 100
- )
+ lost_avg_prcnt = tradeanalyzer["lost"]["pnl"]["average"] / init_cash * 100
lost_max = tradeanalyzer["lost"]["pnl"]["max"]
- lost_max_prcnt = (
- tradeanalyzer["lost"]["pnl"]["max"] / init_cash * 100
- )
+ lost_max_prcnt = tradeanalyzer["lost"]["pnl"]["max"] / init_cash * 100
else:
lost = np.nan
lost_avg = np.nan
@@ -233,9 +218,7 @@ def analyze_strategies(
return sorted_combined_df, optim_params, history_dict
-def sort_metrics_params_and_strats(
- metrics_df, params_df, strat_ids, sort_by, verbose
-):
+def sort_metrics_params_and_strats(metrics_df, params_df, strat_ids, sort_by, verbose):
# Get indices based on `sort_by` metric
optim_idxs = np.argsort(metrics_df[sort_by].values)[::-1]
@@ -251,11 +234,7 @@ def sort_metrics_params_and_strats(
)
# drop extra columns #248
if (
- len(
- set(["channel" "symbol"]).intersection(
- sorted_combined_df.columns.values
- )
- )
+ len(set(["channel" "symbol"]).intersection(sorted_combined_df.columns.values))
== 2
):
sorted_combined_df.drop(["channel", "symbol"], axis=1, inplace=True)
diff --git a/python/fastquant/data/crypto/crypto.py b/python/fastquant/data/crypto/crypto.py
index fdcab4b6..52f537de 100644
--- a/python/fastquant/data/crypto/crypto.py
+++ b/python/fastquant/data/crypto/crypto.py
@@ -21,11 +21,7 @@ def unix_time_millis(date):
# epoch = datetime.utcfromtimestamp(0)
# value will only have : if the date passed is intraday
- dt_format = (
- DATETIME_FORMAT["intraday"]
- if ":" in date
- else DATETIME_FORMAT["daily"]
- )
+ dt_format = DATETIME_FORMAT["intraday"] if ":" in date else DATETIME_FORMAT["daily"]
dt = datetime.strptime(date, dt_format)
# return int((dt - epoch).total_seconds() * 1000)
return int(dt.timestamp() * 1000)
@@ -88,9 +84,9 @@ def get_crypto_data(
)
# Make sure we're at the start of that day
request_start_date_epoch = unix_time_millis(
- pd.to_datetime(
- request_start_date_epoch, unit="ms"
- ).strftime(dt_format)
+ pd.to_datetime(request_start_date_epoch, unit="ms").strftime(
+ dt_format
+ )
)
previous_request_end_date_epoch = request_start_date_epoch - 1
continue
@@ -107,10 +103,7 @@ def get_crypto_data(
# Get the last entry timestamp after we've retrieved (or attempted to) additional records
current_request_end_date_epoch = int(ohlcv_df.dt.max())
- if (
- current_request_end_date_epoch
- <= previous_request_end_date_epoch
- ):
+ if current_request_end_date_epoch <= previous_request_end_date_epoch:
# We haven't gained any additional records, so there's no point in further requests
# Let's mark this for the data end date, mostly so both end_date and end_date_epoch will be
# in sync in case someone in future uses them in code futher down and to ensure the loop bails
@@ -124,9 +117,7 @@ def get_crypto_data(
# The next request should start a millisecond after this one ended
request_start_date_epoch = current_request_end_date_epoch + 1
# This request's end date should now be set as current for the next loop
- previous_request_end_date_epoch = (
- current_request_end_date_epoch
- )
+ previous_request_end_date_epoch = current_request_end_date_epoch
if ohlcv_df is not None:
# Convert the unix timestampe to datetime
@@ -142,8 +133,6 @@ def get_crypto_data(
return ohlcv_df
else:
raise NotImplementedError(
- "The exchange "
- + exchange
- + " is not yet supported. Available exchanges: "
+ "The exchange " + exchange + " is not yet supported. Available exchanges: "
", ".join(CRYPTO_EXCHANGES)
)
diff --git a/python/fastquant/data/stocks/phisix.py b/python/fastquant/data/stocks/phisix.py
index 16d0a1a4..8bc57bb2 100644
--- a/python/fastquant/data/stocks/phisix.py
+++ b/python/fastquant/data/stocks/phisix.py
@@ -83,9 +83,7 @@ def get_phisix_data_by_date(symbol, date):
return None
-def get_phisix_data(
- symbol, start_date, end_date, save=False, max_straight_nones=10
-):
+def get_phisix_data(symbol, start_date, end_date, save=False, max_straight_nones=10):
"""Returns pricing data for a PHISIX stock symbol.
Parameters
@@ -103,10 +101,7 @@ def get_phisix_data(
Stock data (in CV format) for the specified company and date range
"""
date_range = (
- pd.period_range(start_date, end_date, freq="D")
- .to_series()
- .astype(str)
- .values
+ pd.period_range(start_date, end_date, freq="D").to_series().astype(str).values
)
max_straight_nones = min(max_straight_nones, len(date_range))
diff --git a/python/fastquant/data/stocks/pse.py b/python/fastquant/data/stocks/pse.py
index f9f62506..14e0c9b3 100644
--- a/python/fastquant/data/stocks/pse.py
+++ b/python/fastquant/data/stocks/pse.py
@@ -73,9 +73,7 @@ def get_stock_table(stock_table_fp=None):
pd.concat(
[
pd.read_html(r.text)[0],
- pd.DataFrame(
- {"attr": table.xpath("//tr/td/a/@onclick")[::2]}
- ),
+ pd.DataFrame({"attr": table.xpath("//tr/td/a/@onclick")[::2]}),
],
axis=1,
)
@@ -112,9 +110,7 @@ def get_pse_all_stocks():
return df
-def get_pse_data_old(
- symbol, start_date, end_date, stock_table_fp=None, verbose=True
-):
+def get_pse_data_old(symbol, start_date, end_date, stock_table_fp=None, verbose=True):
"""Returns pricing data for a specified stock.
Parameters
@@ -145,26 +141,18 @@ def get_pse_data_old(
data = {
"cmpy_id": int(
- stock_table["company_id"][
- stock_table["Stock Symbol"] == symbol
- ].values[0]
+ stock_table["company_id"][stock_table["Stock Symbol"] == symbol].values[0]
),
"security_id": int(
- stock_table["security_id"][
- stock_table["Stock Symbol"] == symbol
- ].values[0]
+ stock_table["security_id"][stock_table["Stock Symbol"] == symbol].values[0]
),
"startDate": datetime.strptime(start_date, CALENDAR_FORMAT).strftime(
"%m-%d-%Y"
),
- "endDate": datetime.strptime(end_date, CALENDAR_FORMAT).strftime(
- "%m-%d-%Y"
- ),
+ "endDate": datetime.strptime(end_date, CALENDAR_FORMAT).strftime("%m-%d-%Y"),
}
- r = requests.post(
- url="https://edge.pse.com.ph/common/DisclosureCht.ax", json=data
- )
+ r = requests.post(url="https://edge.pse.com.ph/common/DisclosureCht.ax", json=data)
df = pd.DataFrame(r.json()["chartData"])
rename_dict = {
"CHART_DATE": "dt",
@@ -181,9 +169,7 @@ def get_pse_data_old(
return df
-def get_pse_data_cache(
- symbol=None, cache_fp=None, update=False, verbose=False
-):
+def get_pse_data_cache(symbol=None, cache_fp=None, update=False, verbose=False):
"""
Loads cached historical data
Returns all if symbol is None
@@ -198,22 +184,14 @@ def get_pse_data_cache(
df.index = pd.to_datetime(df.index)
if verbose:
print("Loaded: ", cache_fp)
- return (
- df
- if symbol is None
- else df[symbol]
- if symbol in df.columns
- else None
- )
+ return df if symbol is None else df[symbol] if symbol in df.columns else None
else:
errmsg = "Cache does not exist! Try update=True"
print(errmsg)
return None
-def update_pse_data_cache(
- start_date="2010-01-01", verbose=True, cache_fp=None
-):
+def update_pse_data_cache(start_date="2010-01-01", verbose=True, cache_fp=None):
"""
Downloads DOHLC data of all PSE comapnies using get_pse_old
and saves as .zip in /data to be used as cache
@@ -229,9 +207,7 @@ def update_pse_data_cache(
data, unavailable = {}, []
for symbol in tqdm(names["Stock Symbol"].values):
try:
- df = get_pse_data_old(
- symbol, start_date, date_today, verbose=False
- )
+ df = get_pse_data_old(symbol, start_date, date_today, verbose=False)
data[symbol] = df
except Exception as e:
unavailable.append(symbol)
@@ -281,9 +257,7 @@ def get_pse_data(
start = datestring_to_datetime(start_date)
end = datestring_to_datetime(end_date)
- fp = Path(
- DATA_PATH, "{}_stock_{}_{}.csv".format(symbol, start_date, end_date)
- )
+ fp = Path(DATA_PATH, "{}_stock_{}_{}.csv".format(symbol, start_date, end_date))
if "v" in format:
if fp.exists():
@@ -309,9 +283,7 @@ def get_pse_data(
symbol, start_date, end_date, save=False, max_straight_nones=10
)
if not pse_data_df.empty:
- pse_data_df = pd.concat(
- [cache, pse_data_df], ignore_index=True
- )
+ pse_data_df = pd.concat([cache, pse_data_df], ignore_index=True)
else:
pse_data_df = cache.copy()
@@ -339,9 +311,7 @@ def pse_data_to_csv(symbol, start_date, end_date, pse_dir=DATA_PATH):
pse = get_pse_data(symbol, start_date, end_date)
fp = Path(
pse_dir,
- "{}_{}_{}_OHLCV.csCRYPTO_EXCHANGESv".format(
- symbol, start_date, end_date
- ),
+ "{}_{}_{}_OHLCV.csCRYPTO_EXCHANGESv".format(symbol, start_date, end_date),
)
if isinstance(pse, pd.DataFrame):
pse.to_csv(fp)
@@ -349,12 +319,8 @@ def pse_data_to_csv(symbol, start_date, end_date, pse_dir=DATA_PATH):
pse[0].to_csv(fp)
performance_dict = pse[1]
performance_dict["D"].to_csv(
- Path(
- pse_dir, "{}_{}_{}_D.csv".format(symbol, start_date, end_date)
- )
+ Path(pse_dir, "{}_{}_{}_D.csv".format(symbol, start_date, end_date))
)
performance_dict["E"].to_csv(
- Path(
- pse_dir, "{}_{}_{}_E.csv".format(symbol, start_date, end_date)
- )
+ Path(pse_dir, "{}_{}_{}_E.csv".format(symbol, start_date, end_date))
)
diff --git a/python/fastquant/data/web/twitter.py b/python/fastquant/data/web/twitter.py
index fb211227..f22f3412 100644
--- a/python/fastquant/data/web/twitter.py
+++ b/python/fastquant/data/web/twitter.py
@@ -21,9 +21,7 @@ def tweepy_api(consumer_key, consumer_secret, access_token, access_secret):
return api
-def get_twitter_sentiment(
- stock_code, twitter_auth, start_date, twitter_accounts=None
-):
+def get_twitter_sentiment(stock_code, twitter_auth, start_date, twitter_accounts=None):
"""
This function scrapes twitter data based on stock code and twitter accounts specified
Parameters
@@ -105,9 +103,9 @@ def get_twitter_sentiment(
lambda tweet: sia.polarity_scores(tweet)["compound"]
)
- tweet_avg_df = tweet_df.groupby(
- "tweet_created_at", as_index=False
- ).agg({"sentiment_score": "mean"})
+ tweet_avg_df = tweet_df.groupby("tweet_created_at", as_index=False).agg(
+ {"sentiment_score": "mean"}
+ )
date_sentiment = dict(
zip(
diff --git a/python/fastquant/disclosures/investagrams.py b/python/fastquant/disclosures/investagrams.py
index f68045b2..c160ef82 100644
--- a/python/fastquant/disclosures/investagrams.py
+++ b/python/fastquant/disclosures/investagrams.py
@@ -86,9 +86,7 @@ def get_disclosures_json(self):
params=params,
)
if hasattr(response, "text"):
- assert (
- len(response.text) > 10
- ), "Empty response from investagrams.com"
+ assert len(response.text) > 10, "Empty response from investagrams.com"
return response.json()
def disclosures_json_to_df(self):
@@ -109,9 +107,7 @@ def disclosures_json_to_df(self):
for ex in filtered_examples
]
)
- main_df = pd.DataFrame(filtered_examples)[
- ["id", "time", "color", "label"]
- ]
+ main_df = pd.DataFrame(filtered_examples)[["id", "time", "color", "label"]]
combined = pd.concat([main_df, additional_feats_df], axis=1)
combined["time"] = pd.to_datetime(combined.time, unit="s")
if "Total Revenue" in combined.columns.values:
@@ -135,9 +131,7 @@ def disclosures_json_to_df(self):
.apply(lambda x: x.split()[0])
.astype(float)
)
- combined["Net Income YoY Growth (%)"] = combined[
- "Net Income"
- ].apply(
+ combined["Net Income YoY Growth (%)"] = combined["Net Income"].apply(
lambda x: str(x)
.replace("(", "")
.replace(")", "")
diff --git a/python/fastquant/disclosures/pse.py b/python/fastquant/disclosures/pse.py
index dc8df35d..8cadd274 100644
--- a/python/fastquant/disclosures/pse.py
+++ b/python/fastquant/disclosures/pse.py
@@ -103,9 +103,7 @@ def __init__(
)
self.company_disclosures = self.get_company_disclosures()
self.disclosure_types = (
- self.company_disclosures["Template Name"]
- .apply(_remove_amend)
- .unique()
+ self.company_disclosures["Template Name"].apply(_remove_amend).unique()
)
if self.verbose:
print(
@@ -201,9 +199,7 @@ def get_company_disclosures_page(self, page=1):
data=data,
)
if hasattr(response, "text"):
- assert (
- len(response.text) > 10
- ), "Empty response from edge.pse.com.ph"
+ assert len(response.text) > 10, "Empty response from edge.pse.com.ph"
html = response.text
# Indicating the parser (e.g. lxml) removes the bs warning
@@ -228,13 +224,9 @@ def get_company_disclosures_page(self, page=1):
td = tr.find_all("td")
row = [tr.text for tr in td]
onclicks_raw = [
- tr.a["onclick"]
- for tr in td
- if tr.a and "onclick" in tr.a.attrs.keys()
- ]
- onclicks = [
- s[s.find("('") + 2 : s.find("')")] for s in onclicks_raw
+ tr.a["onclick"] for tr in td if tr.a and "onclick" in tr.a.attrs.keys()
]
+ onclicks = [s[s.find("('") + 2 : s.find("')")] for s in onclicks_raw]
lines.append(row)
if onclicks:
edge_nos.append(onclicks[0])
@@ -251,12 +243,8 @@ def get_company_disclosures_page(self, page=1):
# Filter to rows where not all columns are null
df = df[df.isna().mean(axis=1) < 1]
df["edge_no"] = edge_nos
- df["url"] = (
- "https://edge.pse.com.ph/openDiscViewer.do?edge_no=" + df.edge_no
- )
- df["Announce Date and Time"] = pd.to_datetime(
- df["Announce Date and Time"]
- )
+ df["url"] = "https://edge.pse.com.ph/openDiscViewer.do?edge_no=" + df.edge_no
+ df["Announce Date and Time"] = pd.to_datetime(df["Announce Date and Time"])
# ensure index starts at 0
return df.reset_index(drop=True)
@@ -364,9 +352,7 @@ def get_company_summary(self, edge_no):
keys = []
values = []
- for dt, dd in zip(
- parsed_html.find_all("dt"), parsed_html.find_all("dd")
- ):
+ for dt, dd in zip(parsed_html.find_all("dt"), parsed_html.find_all("dd")):
# Take out first token (number followed by a period)
key = " ".join(dt.text.strip().split()[1:])
value = dd.text.strip()
@@ -440,9 +426,7 @@ def load_disclosures(self):
disclosure_details = {}
# append older disclosures
- older = (
- oldest_date > self.company_disclosures["Announce Date and Time"]
- )
+ older = oldest_date > self.company_disclosures["Announce Date and Time"]
idxs1 = np.flatnonzero(older)
if older.sum() > 0:
for idx in tqdm(idxs1):
@@ -463,9 +447,7 @@ def load_disclosures(self):
print(e)
# append newer disclosures
- newer = (
- newest_date < self.company_disclosures["Announce Date and Time"]
- )
+ newer = newest_date < self.company_disclosures["Announce Date and Time"]
idxs2 = np.flatnonzero(newer)
# append newer disclosures
if newer.sum() > 0:
@@ -510,9 +492,7 @@ def get_all_disclosure_tables_df(self):
values.append(json.dumps(df_dict))
return pd.DataFrame(values, columns=["disclosure_table"])
- def get_disclosure_details(
- self, key="Background/Description of the Disclosure"
- ):
+ def get_disclosure_details(self, key="Background/Description of the Disclosure"):
"""
Returns a dataframe of specific data from disclosure_tables
"""
@@ -555,9 +535,7 @@ def filter_disclosures(self, indicator="close", operation="max"):
df = self.disclosures_combined.copy()
df.dropna(subset=["Announce Date and Time"], inplace=True)
- disclosure_dates = df["Announce Date and Time"].apply(
- lambda x: x.date()
- )
+ disclosure_dates = df["Announce Date and Time"].apply(lambda x: x.date())
if self.stock_data is None:
_ = self.get_stock_data()
@@ -589,9 +567,7 @@ def plot_disclosures(
Returns a figure instance
"""
disclosure_type = (
- self.disclosure_type
- if disclosure_type is None
- else disclosure_type
+ self.disclosure_type if disclosure_type is None else disclosure_type
)
fig = pl.figure(figsize=(15, 10))
diff --git a/python/fastquant/disclosures/sentiment.py b/python/fastquant/disclosures/sentiment.py
index 627ecb79..1bfd5125 100644
--- a/python/fastquant/disclosures/sentiment.py
+++ b/python/fastquant/disclosures/sentiment.py
@@ -40,9 +40,7 @@ def get_sentiments(passage):
return 0
-def get_disclosure_sentiment(
- stock_code, start_date, end_date=None, source="pse"
-):
+def get_disclosure_sentiment(stock_code, start_date, end_date=None, source="pse"):
"""
This function scrapes pse/investagram disclosure using fastquant and calculate the
sentiment on each disclosure.
@@ -68,25 +66,19 @@ def get_disclosure_sentiment(
The dictionary output of the data in form of {date: sentiment score}
"""
- start_date = (
- datetime.strptime(start_date, "%Y-%m-%d").date().strftime("%m-%d-%Y")
- )
+ start_date = datetime.strptime(start_date, "%Y-%m-%d").date().strftime("%m-%d-%Y")
if end_date is not None:
- end_date = (
- datetime.strptime(end_date, "%Y-%m-%d").date().strftime("%m-%d-%Y")
- )
- dpse = DisclosuresPSE(
- symbol=stock_code, start_date=start_date, end_date=end_date
- )
+ end_date = datetime.strptime(end_date, "%Y-%m-%d").date().strftime("%m-%d-%Y")
+ dpse = DisclosuresPSE(symbol=stock_code, start_date=start_date, end_date=end_date)
df = dpse.get_combined_disclosures()[
["Announce Date and Time", "Background/Description of the Disclosure"]
]
df.columns = ["date", "description"]
df["sentiments"] = df["description"].apply(get_sentiments)
- df["date"] = pd.to_datetime(
- df["date"].apply(lambda x: str(x)[:11])
- ).dt.strftime("%d %b %Y")
+ df["date"] = pd.to_datetime(df["date"].apply(lambda x: str(x)[:11])).dt.strftime(
+ "%d %b %Y"
+ )
date_sentiment = {}
for k, v in zip(df["date"], df["sentiments"]):
diff --git a/python/fastquant/indicators/sentiment.py b/python/fastquant/indicators/sentiment.py
index 17905486..f6f0d109 100644
--- a/python/fastquant/indicators/sentiment.py
+++ b/python/fastquant/indicators/sentiment.py
@@ -24,9 +24,7 @@ class Sentiment(bt.Indicator):
lines = ("sentiment",)
- plotinfo = dict(
- plotymargin=0.15, plothlines=[0], plotyticks=[1.0, 0, -1.0]
- )
+ plotinfo = dict(plotymargin=0.15, plothlines=[0], plotyticks=[1.0, 0, -1.0])
plotlines = dict(
sentiment=dict(
diff --git a/python/fastquant/network.py b/python/fastquant/network.py
index 5719051b..859afc68 100644
--- a/python/fastquant/network.py
+++ b/python/fastquant/network.py
@@ -72,9 +72,7 @@ def __init__(
self.interpolation_method = interpolation_method
self.n_companies = n_companies
self.update_cache = update_cache
- self.data_cache = get_pse_data_cache(
- update=self.update_cache, verbose=False
- )
+ self.data_cache = get_pse_data_cache(update=self.update_cache, verbose=False)
self.data = self.data_cache.xs(indicator, level=1, axis=1)
self.filtered_data = self.filter_data()
self.company_table = self.load_company_table()
@@ -95,9 +93,7 @@ def load_company_table(self):
def get_sector_of_symbol(self, symbol):
"""get sector or subsector where symbol belongs"""
info = self.company_table.copy()
- sector = info.loc[
- info["Stock Symbol"].isin([symbol]), "Sector"
- ].values[0]
+ sector = info.loc[info["Stock Symbol"].isin([symbol]), "Sector"].values[0]
return sector
def get_symbols_of_a_sector(self, sector, subsector=False, verbose=False):
@@ -108,9 +104,7 @@ def get_symbols_of_a_sector(self, sector, subsector=False, verbose=False):
sector_indices = sectors_dict[sector]
sector_symbols = info.loc[sector_indices, "Stock Symbol"]
- data_availability_indices = self.filtered_data.columns.isin(
- sector_symbols
- )
+ data_availability_indices = self.filtered_data.columns.isin(sector_symbols)
symbols_with_data = self.filtered_data.columns[
data_availability_indices
].tolist()
@@ -119,9 +113,7 @@ def get_symbols_of_a_sector(self, sector, subsector=False, verbose=False):
].tolist()
if verbose:
print(
- "Symbols without data in {}:\n{}".format(
- sector, symbols_without_data
- )
+ "Symbols without data in {}:\n{}".format(sector, symbols_without_data)
)
return symbols_with_data
@@ -360,11 +352,7 @@ def plot_corr_company(
if positive:
# positively correlated
if symbol2 is None:
- symbol2 = (
- rank.index[-2]
- if rank.index[-1] == symbol
- else rank.index[-1]
- )
+ symbol2 = rank.index[-2] if rank.index[-1] == symbol else rank.index[-1]
else:
# negatively correlated
diff --git a/python/fastquant/notification.py b/python/fastquant/notification.py
index 1b8beee9..822899a3 100644
--- a/python/fastquant/notification.py
+++ b/python/fastquant/notification.py
@@ -75,9 +75,7 @@ def email_notif(
msg = MIMEMultipart() # create a message
date = date or datetime.utcnow().strftime("%Y-%m-%d")
- message = (
- message or "Today is " + date + ": " + action + " " + symbol or ""
- )
+ message = message or "Today is " + date + ": " + action + " " + symbol or ""
# setup the parameters of the message
msg["From"] = my_address
diff --git a/python/fastquant/strategies/base.py b/python/fastquant/strategies/base.py
index cb48f0c8..6db9bf50 100644
--- a/python/fastquant/strategies/base.py
+++ b/python/fastquant/strategies/base.py
@@ -115,7 +115,7 @@ def __init__(self):
self.strategy_position = 0
else:
self.strategy_position = None
-
+
# Longer term, we plan to add `freq` like notation, similar to pandas datetime
# https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html
# M means add cash at the first day of each month
@@ -239,8 +239,7 @@ def notify_trade(self, trade):
return
if self.transaction_logging:
self.log(
- "OPERATION PROFIT, GROSS: %.2f, NET: %.2f"
- % (trade.pnl, trade.pnlcomm)
+ "OPERATION PROFIT, GROSS: %.2f, NET: %.2f" % (trade.pnl, trade.pnlcomm)
)
def notify_cashvalue(self, cash, value):
@@ -254,9 +253,7 @@ def stop(self):
# Saving to self so it's accessible later during optimization
self.final_value = self.broker.getvalue()
# Note that PnL is the final portfolio value minus the initial cash balance minus the total cash added
- self.pnl = round(
- self.final_value - self.init_cash - self.total_cash_added, 2
- )
+ self.pnl = round(self.final_value - self.init_cash - self.total_cash_added, 2)
if self.strategy_logging:
self.log("Final Portfolio Value: {}".format(self.final_value))
self.log("Final PnL: {}".format(self.pnl))
@@ -289,11 +286,7 @@ def next(self):
self.next_cash_datetime = self.cron.get_next(datetime.datetime)
if self.transaction_logging:
- self.log(
- "Start date: {}".format(
- start_date.strftime("%Y-%m-%d")
- )
- )
+ self.log("Start date: {}".format(start_date.strftime("%Y-%m-%d")))
self.log(
"Next cash date: {}".format(
self.next_cash_datetime.strftime("%Y-%m-%d")
@@ -312,9 +305,7 @@ def next(self):
if self.transaction_logging:
self.log("Cash added: {}".format(self.add_cash_amount))
- self.log(
- "Total cash added: {}".format(self.total_cash_added)
- )
+ self.log("Total cash added: {}".format(self.total_cash_added))
self.log(
"Next cash date: {}".format(
self.next_cash_datetime.strftime("%Y-%m-%d")
@@ -359,8 +350,7 @@ def next(self):
# Margin is required for buy commission
# Add allowance to commission per transaction (avoid margin)
afforded_size = self.cash / (
- (self.dataclose[0] * (1 + self.slippage))
- * (1 + self.commission)
+ (self.dataclose[0] * (1 + self.slippage)) * (1 + self.commission)
)
buy_prop_size = afforded_size * self.buy_prop
@@ -384,9 +374,7 @@ def next(self):
# Implement stop loss at the purchase level (only this specific trade is closed)
if self.stop_loss:
- stop_price = self.data.close[0] * (
- 1.0 - self.stop_loss
- )
+ stop_price = self.data.close[0] * (1.0 - self.stop_loss)
if self.transaction_logging:
self.log("Stop price: {}".format(stop_price))
self.stoploss_order = self.sell(
@@ -399,9 +387,7 @@ def next(self):
# Create a stoploss trail order if None
if self.stoploss_trail_order is None:
if self.transaction_logging:
- self.log(
- "Stop trail: {}".format(self.stop_trail)
- )
+ self.log("Stop trail: {}".format(self.stop_trail))
self.stoploss_trail_order = self.sell(
exectype=bt.Order.StopTrail,
trailpercent=self.stop_trail,
@@ -415,8 +401,7 @@ def next(self):
else:
# Margin is required for buy commission
afforded_size = int(
- self.cash
- / (self.dataopen[1] * (1 + self.commission + 0.001))
+ self.cash / (self.dataopen[1] * (1 + self.commission + 0.001))
)
final_size = min(buy_prop_size, afforded_size)
if self.transaction_logging:
@@ -427,9 +412,7 @@ def next(self):
# Implement stop loss at the purchase level (only this specific trade is closed)
if self.stop_loss:
- stop_price = self.data.close[0] * (
- 1.0 - self.stop_loss
- )
+ stop_price = self.data.close[0] * (1.0 - self.stop_loss)
if self.transaction_logging:
self.log("Stop price: {}".format(stop_price))
self.stoploss_order = self.sell(
@@ -463,11 +446,7 @@ def next(self):
# The max incremental short allowed is the short that would lead to a cumulative short position
# equal to the maximum short position (initial cash times the maximum short ratio, which is 1.5 by default)
max_position_size = max(
- int(
- self.broker.getvalue()
- * self.short_max
- / self.dataclose[1]
- )
+ int(self.broker.getvalue() * self.short_max / self.dataclose[1])
+ self.position.size,
0,
)
@@ -487,11 +466,7 @@ def next(self):
# The max incremental short allowed is the short that would lead to a cumulative short position
# equal to the maximum short position (initial cash times the maximum short ratio, which is 1.5 by default)
max_position_size = max(
- int(
- self.broker.getvalue()
- * self.short_max
- / self.dataopen[1]
- )
+ int(self.broker.getvalue() * self.short_max / self.dataopen[1])
+ self.position.size,
0,
)
@@ -515,17 +490,13 @@ def next(self):
# Sell based on the closing price of the previous closing day
self.order = self.sell(
size=int(
- (stock_value / (self.dataclose[1]))
- * self.sell_prop
+ (stock_value / (self.dataclose[1])) * self.sell_prop
),
)
else:
# Sell based on the opening price of the next closing day (only works "open" data exists in the dataset)
self.order = self.sell(
- size=int(
- (self.init_cash / self.dataopen[1])
- * self.sell_prop
- )
+ size=int((self.init_cash / self.dataopen[1]) * self.sell_prop)
)
# Explicitly cancel stoploss order
diff --git a/python/fastquant/strategies/ma_crossover.py b/python/fastquant/strategies/ma_crossover.py
index 7cc73b8d..c80862ea 100644
--- a/python/fastquant/strategies/ma_crossover.py
+++ b/python/fastquant/strategies/ma_crossover.py
@@ -46,9 +46,7 @@ def __init__(self):
print("slow_period :", self.slow_period)
sma_fast = bt.ind.SMA(period=self.fast_period) # fast moving average
sma_slow = bt.ind.SMA(period=self.slow_period) # slow moving average
- self.crossover = bt.ind.CrossOver(
- sma_fast, sma_slow
- ) # crossover signal
+ self.crossover = bt.ind.CrossOver(sma_fast, sma_slow) # crossover signal
def buy_signal(self):
return self.crossover > 0
@@ -88,9 +86,7 @@ def __init__(self):
print("slow_period :", self.slow_period)
ema_fast = bt.ind.EMA(period=self.fast_period) # fast moving average
ema_slow = bt.ind.EMA(period=self.slow_period) # slow moving average
- self.crossover = bt.ind.CrossOver(
- ema_fast, ema_slow
- ) # crossover signal
+ self.crossover = bt.ind.CrossOver(ema_fast, ema_slow) # crossover signal
def buy_signal(self):
return self.crossover > 0
diff --git a/python/tests/test_fastquant.py b/python/tests/test_fastquant.py
index 497bbfc4..62f55ba9 100644
--- a/python/tests/test_fastquant.py
+++ b/python/tests/test_fastquant.py
@@ -36,14 +36,10 @@ def test_get_yahoo_data_dividend():
def test_get_stock_data():
# Test w/ respective sources
- stock_df = get_stock_data(
- PHISIX_SYMBOL, DATE_START, DATE_END, source="phisix"
- )
+ stock_df = get_stock_data(PHISIX_SYMBOL, DATE_START, DATE_END, source="phisix")
assert isinstance(stock_df, pd.DataFrame)
- stock_df = get_stock_data(
- YAHOO_SYMBOL, DATE_START, DATE_END, source="yahoo"
- )
+ stock_df = get_stock_data(YAHOO_SYMBOL, DATE_START, DATE_END, source="yahoo")
assert isinstance(stock_df, pd.DataFrame)
# Test getting yahoo when (default) phisix fails on a non PSE SYMBOL
diff --git a/python/tests/test_network.py b/python/tests/test_network.py
index ee96fda8..5841efd3 100644
--- a/python/tests/test_network.py
+++ b/python/tests/test_network.py
@@ -2,9 +2,7 @@
import pandas as pd
from fastquant import Network
-nw = Network(
- symbol="JFC", start_date="2020-01-01", end_date="2020-04-01", metric="b"
-)
+nw = Network(symbol="JFC", start_date="2020-01-01", end_date="2020-04-01", metric="b")
def test_network_init():
diff --git a/python/tests/test_strategies.py b/python/tests/test_strategies.py
index 3142e4d1..ce3a5fcf 100644
--- a/python/tests/test_strategies.py
+++ b/python/tests/test_strategies.py
@@ -32,9 +32,7 @@ def test_backtest():
for strategy in STRATEGY_MAPPING.keys():
if strategy == "sentiment":
- data = get_yahoo_data(
- "TSLA", "2020-01-01", "2020-07-04", dividends=True
- )
+ data = get_yahoo_data("TSLA", "2020-01-01", "2020-07-04", dividends=True)
# use cached data instead of scraping for tests purposes.
# sentiments = get_bt_news_sentiment(keyword="tesla", page_nums=2)
with open(SENTI_PKL, "rb") as handle:
@@ -42,9 +40,7 @@ def test_backtest():
cerebro = backtest(
strategy, data, sentiments=sentiments, senti=0.4, plot=False
)
- errmsg = "Backtest encountered error for strategy '{}'!".format(
- strategy
- )
+ errmsg = "Backtest encountered error for strategy '{}'!".format(strategy)
assert cerebro is not None, errmsg
data_disclosures = get_stock_data(
@@ -70,18 +66,14 @@ def test_backtest():
senti=0.2,
plot=False,
)
- errmsg_disclosures = (
- "Backtest encountered error for strategy '{}'!".format(
- strategy
- )
+ errmsg_disclosures = "Backtest encountered error for strategy '{}'!".format(
+ strategy
)
assert cerebro_disclosures is not None, errmsg_disclosures
else:
cerebro = backtest(strategy, sample, plot=False)
- errmsg = "Backtest encountered error for strategy '{}'!".format(
- strategy
- )
+ errmsg = "Backtest encountered error for strategy '{}'!".format(strategy)
assert cerebro is not None, errmsg
@@ -91,9 +83,7 @@ def test_multi_backtest():
"""
sample = pd.read_csv(SAMPLE_CSV, parse_dates=["dt"])
cerebro = backtest("multi", sample, strats=SAMPLE_STRAT_DICT, plot=False)
- assert (
- cerebro is not None
- ), "Backtest encountered error for strategy 'multi'!"
+ assert cerebro is not None, "Backtest encountered error for strategy 'multi'!"
def test_grid_backtest():
@@ -108,6 +98,4 @@ def test_grid_backtest():
slow_period=range(40, 55, 3),
plot=False,
)
- assert (
- cerebro is not None
- ), "Backtest encountered error doing grid search on SMAC!"
+ assert cerebro is not None, "Backtest encountered error doing grid search on SMAC!"
diff --git a/python/tests/test_twitter.py b/python/tests/test_twitter.py
index 17b24b98..81d4b6da 100644
--- a/python/tests/test_twitter.py
+++ b/python/tests/test_twitter.py
@@ -15,9 +15,7 @@ def test_get_twitter_sentiment():
access_token = ""
access_secret = ""
- api = tweepy_api(
- consumer_key, consumer_secret, access_token, access_secret
- )
+ api = tweepy_api(consumer_key, consumer_secret, access_token, access_secret)
assert api is not None
# account_list = ["2TradeAsia", "colfinancial"]
# sentiment_dict = get_twitter_sentiment('$ALI', api, '2020-06-14', account_list)