├── .github └── workflows │ └── gh-actions.yml ├── Analysis ├── 01 Fundamental Factor Analysis.ipynb ├── 02 Kalman Filter Based Pairs Trading.ipynb ├── 03 Mean-Variance Portfolio Optimization .ipynb ├── 04 EMA Cross Strategy Based on VXX.ipynb └── 05 Pairs Trading Strategy Based on Cointegration.ipynb ├── Documentation └── Python │ ├── Plotting-Price-Data-Using-Matplotlib.ipynb │ ├── Plotting-Price-Data-Using-Plotly.ipynb │ ├── Plotting-Price-Data-Using-Seaborn.ipynb │ ├── Predicting-Future-Prices-With-Keras.ipynb │ ├── Predicting-Future-Prices-With-Sklearn.ipynb │ ├── Predicting-Future-Prices-With-TensorFlow.ipynb │ └── Stock-Picking-With-Fundamental-Data.ipynb ├── Explore ├── Inter font │ ├── Inter-VariableFont_slnt,wght.ttf │ ├── OFL.txt │ ├── README.txt │ └── static │ │ ├── Inter-Black.ttf │ │ ├── Inter-Bold.ttf │ │ ├── Inter-ExtraBold.ttf │ │ ├── Inter-ExtraLight.ttf │ │ ├── Inter-Light.ttf │ │ ├── Inter-Medium.ttf │ │ ├── Inter-Regular.ttf │ │ ├── Inter-SemiBold.ttf │ │ └── Inter-Thin.ttf ├── default_photo.png ├── generate_social_media_thumbnails.py ├── template_landscape.png └── template_square.png ├── LICENSE ├── README.md ├── Research2Production ├── CSharp │ ├── 01 Mean Reversion CSharp.ipynb │ ├── 03 Uncorrelated Assets CSharp.ipynb │ ├── 04 Kalman Filters and Pairs Trading CSharp.ipynb │ └── 05 Stationary Processes and Z-Scores CSharp.ipynb └── Python │ ├── 01 Mean Reversion.ipynb │ ├── 02 Random Forest Regression.ipynb │ ├── 03 Uncorrelated Assets.ipynb │ ├── 04 Kalman Filters and Pairs Trading.ipynb │ ├── 05 Stationary Processes and Z-Scores.ipynb │ ├── 06 Principle Compenent Analysis.ipynb │ ├── 07 Hidden Markov Models.ipynb │ └── 08 Long Short-Term Memory.ipynb ├── Scratch Notebooks └── Optimization Analysis - Draft.ipynb ├── Topical ├── 20200507_hopevfear_research.ipynb └── 20200601_airlinebuybacks_research.ipynb └── flask.png /.github/workflows/gh-actions.yml: -------------------------------------------------------------------------------- 1 | name: Update Social Media Thumbnails for Algorithm Explorer 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */3 * * *" # Runs "at minute 0 past every 3rd hour." (see https://crontab.guru) 6 | workflow_dispatch: # Runs on manual trigger 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | environment: Content Deploy 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Install dependencies 16 | run: |- 17 | python -m pip install --upgrade pip 18 | pip install kaleido==0.2.1 19 | pip install numpy==1.24.2 20 | pip install Pillow==9.3.0 21 | pip install plotly==5.13.1 22 | 23 | - name: Social Media Thumbnails 24 | run: python ./Explore/generate_social_media_thumbnails.py 25 | 26 | - name: Configure AWS Credentials 27 | uses: aws-actions/configure-aws-credentials@v1 28 | with: 29 | aws-access-key-id: ${{ secrets.AWS_KEY }} 30 | aws-secret-access-key: ${{ secrets.AWS_SECRET }} 31 | aws-region: us-west-1 32 | 33 | - name: Copy files to the S3 website content bucket 34 | run: |- 35 | aws s3 cp ./Explore/thumbnails s3://${{ secrets.AWS_BUCKET }}/explore/thumbnails --recursive --acl bucket-owner-full-control --exclude "*" --include "*.png" --content-type "image/png" -------------------------------------------------------------------------------- /Documentation/Python/Plotting-Price-Data-Using-Matplotlib.ipynb: -------------------------------------------------------------------------------- 1 | {"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", "
"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Visualizing Data with Matplotlib\n", "#### Line Plots, Histograms, Scatter plots, and Heat Maps"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Import libraries\n", "Let's start by importing the libraries we need."]}, {"cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": ["import matplotlib.pyplot as plt\n", "import numpy as np # used later for OLS"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Visualizing SPY\n", "We will visualize SPY using a time-series line plot and a histogram."]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Gathering Data\n", "To begin, we retrieve daily close data for SPY"]}, {"cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": ["qb = QuantBook()\n", "spy = qb.AddEquity(\"SPY\")\n", "history = qb.History(qb.Securities.Keys, 360, Resolution.Daily)\n", "spy_hist = history.loc['SPY']['close']\n", "spy_hist"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Here, we visualize SPY with a time-series line plot. Note that pandas has built-in support for matplotlib, making it easier and quicker for us to create plots."]}, {"cell_type": "code", "execution_count": 4, "metadata": {"scrolled": true}, "outputs": [], "source": ["fig, ax = plt.subplots()\n", "spy_hist.plot(ax=ax) \n", "plt.title('SPY Daily Close')"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Next, we want to visualize the returns distribution of SPY. We call pct_change() on our SPY Series to get the daily returns."]}, {"cell_type": "code", "execution_count": 5, "metadata": {"scrolled": true}, "outputs": [], "source": ["spy_daily_ret = spy_hist.pct_change()\n", "plt.hist(spy_daily_ret, bins=50)\n", "plt.title('Are normally distributed returns a hoax?')\n", "plt.xlabel('Daily Return')\n", "plt.ylabel('Count')"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Visualizing the Correlation Between GLD and BAR Returns\n", "GLD and BAR are two different Gold-tracking ETFs, and we would like to visualize their correlation. To do this, we will employ a scatter plot of their returns."]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Gathering Data\n", "First, we get daily close returns data for GLD and BAR"]}, {"cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": ["qb.AddEquity('GLD')\n", "qb.AddEquity('BAR') \n", "\n", "gold_etf_hist = qb.History(['GLD', 'BAR'], 300, Resolution.Daily)\n", "\n", "gld_ret = gold_etf_hist.loc['GLD']['close'].pct_change().dropna()\n", "bar_ret = gold_etf_hist.loc['BAR']['close'].pct_change().dropna()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Since we would like to include a trend line (OLS) in our scatter plot, we will need to employ numpy to calculate our OLS line."]}, {"cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": ["# get coefficients of the OLS Line (trend line)\n", "m, b = np.polyfit(gld_ret, bar_ret, deg=1)\n", "\n", "x = np.linspace(-.045, .06)\n", "plt.plot(x, x * m + b, color='red')\n", "\n", "plt.scatter(gld_ret, bar_ret)\n", "\n", "plt.title('GLD vs BAR daily returns Scatter Plot')\n", "plt.xlabel('GLD')\n", "plt.ylabel('BAR')\n", "plt.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Visualizing Correlations Between Banking Stocks\n", "Correlations between stocks in the same sector are commonly applied to develop pairs trading strategies. We will visualize the correlations between a few stocks in the banking sector using a heat map. Note: we strongly advise using Seaborn instead for heat maps, as it needs only roughly 1/10th of the number of lines of code."]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Gathering Data\n", "First, we get historical closing prices for four banking sector stocks."]}, {"cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": ["tickers = [\n", " \"COF\", # Capital One Financial Corporation\n", " \"JPM\", # J P Morgan Chase & Co\n", " \"STI\", # SunTrust Banks, Inc.\n", " \"WFC\" # Wells Fargo & Company\n", "]\n", "symbols = [qb.AddEquity(ticker, Resolution.Daily).Symbol for ticker in tickers]\n", "\n", "history = qb.History(tickers, \n", " datetime(2020, 2, 1), \n", " datetime(2020, 7, 1), \n", " Resolution.Daily)\n", "\n", "df = history['close'].unstack(level=0)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Before we create the heat map, we must create a correlation matrix, which has all the correlations between all possible pairs of symbols. This can be easily done with pandas DataFrame's built-in corr() method."]}, {"cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": ["corr_matrix = df.corr()\n", "corr_matrix"]}, {"cell_type": "markdown", "metadata": {}, "source": ["With the correlation matrix, we are ready to create a heat map. "]}, {"cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [], "source": ["fig, ax = plt.subplots()\n", "im = ax.imshow(corr_matrix)\n", "\n", "# full symbol data will clutter the heat map\n", "tickers_ordered = [symbol.split()[0] for symbol in corr_matrix.columns]\n", "\n", "# matplotlib has a bug where the first tick doesn't appear\n", "# so we've put in a place holder\n", "# we STRONGLY advise using Seaborn for heat maps\n", "ax.set_xticklabels(['-'] + tickers_ordered)\n", "ax.set_yticklabels(['-'] + tickers_ordered)\n", "\n", "# make sure x and y-axes don't have too many ticks\n", "ax.locator_params(axis='x', nbins=len(tickers_ordered))\n", "ax.locator_params(axis='y', nbins=len(tickers_ordered))\n", "\n", "# Rotate the x tick labels and set their alignment.\n", "plt.setp(ax.get_xticklabels(), rotation=45, ha=\"right\",\n", " rotation_mode=\"anchor\")\n", "\n", "# Loop over data dimensions and create text annotations.\n", "for i in range(len(corr_matrix.index)):\n", " for c in range(len(corr_matrix.columns)):\n", " label = round(corr_matrix.iloc[i][corr_matrix.columns[c]], 2)\n", " text = ax.text(c, i, label,\n", " ha=\"center\", va=\"center\", color=\"w\")\n", "\n", "ax.set_title(\"Banking Stocks Correlation Heat Map\")"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8"}}, "nbformat": 4, "nbformat_minor": 2} -------------------------------------------------------------------------------- /Documentation/Python/Plotting-Price-Data-Using-Plotly.ipynb: -------------------------------------------------------------------------------- 1 | {"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", "
"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Visualizing Data with Plotly\n", "### Line Charts, Candlestick Charts, Bar Charts, and Heat Maps"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Import Libraries\n", "Let's start by importing the libraries we need."]}, {"cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": ["import plotly.express as px\n", "import plotly.graph_objects as go"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Visualizing SPY\n", "We will visualize SPY using a time-series line chart and a candlestick chart."]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Gathering Data\n", "To begin, we retrieve daily close data for SPY."]}, {"cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": ["qb = QuantBook()\n", "spy = qb.AddEquity(\"SPY\")\n", "history = qb.History(qb.Securities.Keys, 360, Resolution.Daily)\n", "spy_hist = history.loc['SPY']\n", "spy_hist"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### SPY Time-Series Line Chart\n", "We reset the index of the DataFrame so that it is easier to use with plotly."]}, {"cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": ["spy_hist2 = spy_hist.reset_index()\n", "fig = px.line(spy_hist2, x='time', y='high') \n", "fig.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### SPY Candlesticks"]}, {"cell_type": "code", "execution_count": 6, "metadata": {"scrolled": false}, "outputs": [], "source": ["fig = go.Figure(data=[go.Candlestick(x=spy_hist.index,\n", " open=spy_hist['open'],\n", " high=spy_hist['high'],\n", " low=spy_hist['low'],\n", " close=spy_hist['close'])],\n", " layout=go.Layout(\n", " title=go.layout.Title(text='SPY OHLC'),\n", " xaxis_title='Date',\n", " yaxis_title='Price',\n", " xaxis_rangeslider_visible=False\n", " ))\n", "\n", "fig.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Visualizing the Correlation Between GLD and BAR Returns\n", "GLD and BAR are two different Gold-tracking ETFs, and we would like to visualize their correlation. To do this, we will employ a scatter plot of their returns."]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Gathering Data\n", "First, we get daily close data for GLD and BAR, and call pct_change() on the DataFrames to get daily returns."]}, {"cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": ["qb.AddEquity('GLD')\n", "qb.AddEquity('BAR')\n", "\n", "gold_etf_hist = qb.History(['GLD', 'BAR'], 300, Resolution.Daily)\n", "\n", "gld_ret = gold_etf_hist.loc['GLD']['close'].pct_change()[1:]\n", "bar_ret = gold_etf_hist.loc['BAR']['close'].pct_change()[1:]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now, we can plot the daily returns of GLD vs the daily returns of BAR. We include a trendline by specifying a trendline key word argument, and setting to OLS, which is the most common way to calculate a trend line."]}, {"cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": ["fig = px.scatter(x=gld_ret, y=bar_ret, trendline='ols', labels={'x': 'GLD', 'y': 'BAR'}) \n", "fig.update_layout(title='GLD vs BAR Daily % Returns')\n", "fig.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Visualizing the Average Daily Percent Returns of Banking Stocks\n", "We use a bar chart to compare the average daily percent returns of various banking stocks."]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Gathering Data\n", "First, we get historical closing prices for four banking sector stocks, then compute the average daily return for each stock."]}, {"cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": ["banking_tickers = [\n", " \"COF\", # Capital One Financial Corporation\n", " \"JPM\", # J P Morgan Chase & Co\n", " \"STI\", # SunTrust Banks, Inc.\n", " \"WFC\", # Wells Fargo & Company\n", "]\n", "for ticker in banking_tickers:\n", " qb.AddEquity(ticker)\n", "\n", "banking_hist = qb.History(banking_tickers, 300, Resolution.Daily)\n", "banking_close = banking_hist.unstack(level=0)['close'] \n", "banking_avg_daily_ret = pd.DataFrame()\n", "banking_avg_daily_ret['avg_daily_ret'] = banking_close.pct_change().dropna().apply(lambda x: x.mean(), axis=0) \n", "# reset_index() is necessary because plotly only uses columns\n", "banking_avg_daily_ret = banking_avg_daily_ret.reset_index()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["With the average daily returns for each stock, we can now create our bar chart to compare these values."]}, {"cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": ["fig = px.bar(banking_avg_daily_ret, x='symbol', y='avg_daily_ret')\n", "fig.update_layout(title='Banking Stocks Average Daily % Returns')"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Visualizing Correlations Between Banking Stocks\n", "Correlations between stocks in the same sector are commonly applied to develop pairs trading strategies. We will visualize the correlations between a few stocks in the banking sector using a heat map."]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Gathering Data\n", "First, we get historical closing prices for four banking sector stocks."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Before we create the heat map, we must create a correlation matrix, which has all the correlations between all possible pairs of symbols. This can be easily done with pandas DataFrame's built-in corr() method."]}, {"cell_type": "code", "execution_count": 11, "metadata": {"scrolled": true}, "outputs": [], "source": ["corr_matrix = banking_close.corr()\n", "corr_matrix"]}, {"cell_type": "markdown", "metadata": {}, "source": ["With the correlation matrix, we are ready to create a heat map."]}, {"cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": ["px.imshow(corr_matrix, x=banking_tickers, y=banking_tickers)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": []}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8"}}, "nbformat": 4, "nbformat_minor": 2} -------------------------------------------------------------------------------- /Documentation/Python/Plotting-Price-Data-Using-Seaborn.ipynb: -------------------------------------------------------------------------------- 1 | {"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", "
"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Visualizing Data With Seaborn\n", "#### An example of visualizing data with Seaborn's plots and heatmap."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Import Library\n", "Let's start by importing Seaborn"]}, {"cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [], "source": ["import seaborn as sns\n", "sns.set(rc={'figure.figsize':(16,8)})"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Gather & Prepare Data\n", "Make a History request to fetch daily prices of several securities."]}, {"cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [], "source": ["qb = QuantBook()\n", "\n", "# Make symbols for each plot\n", "line_tickers = ['SPY']\n", "line_symbols = [qb.Symbol(ticker) for ticker in line_tickers]\n", "\n", "scatter_tickers = [\"BAR\", \"GLD\"]\n", "scatter_symbols = [qb.Symbol(ticker) for ticker in scatter_tickers]\n", "\n", "heatmap_tickers = [\n", " \"BAC\", # Bank of America Corporation\n", " \"COF\", # Capital One Financial Corporation\n", " \"C\", # Citigroup Inc.\n", " \"JPM\", # J P Morgan Chase & Co\n", " \"WFC\", # Wells Fargo & Company\n", "]\n", "heatmap_symbols = [qb.Symbol(ticker) for ticker in heatmap_tickers]\n", "\n", "# Make history request\n", "history = qb.History(line_symbols + scatter_symbols + heatmap_symbols, \n", " datetime(2019, 1, 1), \n", " datetime(2020, 7, 1), \n", " Resolution.Daily).close.unstack(level=0)\n", "\n", "# Seperate the history for each plot\n", "line_history = history[[str(symbol.ID) for symbol in line_symbols]]\n", "scatter_history = history[[str(symbol.ID) for symbol in scatter_symbols]]\n", "heatmap_history = history[[str(symbol.ID) for symbol in heatmap_symbols]]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Line Plot\n", "Plot the price data using line plots for a quick visualization."]}, {"cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [], "source": ["plot = sns.lineplot(x='time', \n", " y=str(line_symbols[0].ID), \n", " data=line_history.reset_index())\n", "plot.set(ylabel=\"price\", title=\"SPY Price Over Time\");"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Scatter Plot\n", "Use scatterplot to study the price cointegration."]}, {"cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [], "source": ["returns = scatter_history.pct_change()[1:]\n", "plot = sns.regplot(x=returns.columns[0], y=returns.columns[1], data=returns)\n", "plot.set(xlabel='GLD % Returns', ylabel='BAR % Returns', title='GLD vs BAR Daily % Returns');"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Heatmap\n", "Use heatmap to visualize the correlation between security returns."]}, {"cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [], "source": ["sns.heatmap(heatmap_history.corr());"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": []}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8"}}, "nbformat": 4, "nbformat_minor": 2} -------------------------------------------------------------------------------- /Documentation/Python/Predicting-Future-Prices-With-Keras.ipynb: -------------------------------------------------------------------------------- 1 | {"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", "
"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Predicting Future Prices With Keras\n", "#### An example of Keras model building, training, saving in the ObjectStore, and loading."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Import Libraries\n", "Let's start by importing the functionality we'll need to build the model and serialize/unserialize the model for saving and loading"]}, {"cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": ["from tensorflow.keras import utils\n", "from tensorflow.keras.models import Sequential\n", "from tensorflow.keras.layers import Dense, Flatten\n", "from tensorflow.keras.optimizers import RMSprop\n", "import json\n", "from keras.utils.generic_utils import serialize_keras_object "]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Gather & Prepare Data\n", "Let's retreive some daily data for the SPY by making a History request."]}, {"cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": ["qb = QuantBook()\n", "spy = qb.AddEquity('SPY')\n", "history = qb.History(qb.Securities.Keys, 360, Resolution.Daily)\n", "spy_hist = history.loc['SPY']\n", "spy_hist"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We create a function that prepares our data suitable for training and testing our Model. We use 5 steps of OHLCV data to predict the closing price of the bar right after. By tying this to a function, we increase clarity, as well as reusability, especially if we were to copy it into a class in a .py file."]}, {"cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": ["# function to prepare our data for training our NN\n", "def prep_data(data, n_tsteps=5):\n", " # n_tsteps is the number of time steps at and before time t we want to use\n", " # to predict the close price at time t + 1\n", " \n", " # this helps normalizes the data\n", " df = data.pct_change()[1:]\n", " \n", " features = []\n", " labels = []\n", "\n", " for i in range(len(df)-n_tsteps):\n", " input_data = df.iloc[i:i+n_tsteps].values\n", " features.append(input_data)\n", " label = df['close'].iloc[i+n_tsteps]\n", " labels.append(label)\n", "\n", " return np.array(features), np.array(labels)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Build the Model (Regression Neural Network)\n", "Let's build the neural network using Keras. We create a function that creates our Model, building up the input and output layers, and the layers Between. We tie this to a function for the same reason mentioned before."]}, {"cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": ["def build_model():\n", " model = Sequential([\n", " # 5 input variables (OHLCV) by 5 time steps\n", " Dense(10, input_shape=(5,5), activation='relu'),\n", " Dense(10, activation='relu'),\n", " # Flatten layer required because input shape is 2D\n", " Flatten(),\n", " # since we are performing regression, we only need 1 output node\n", " Dense(1)\n", " ])\n", "\n", " model.compile(loss='mse',\n", " optimizer=RMSprop(0.001),\n", " metrics=['mae', 'mse'])\n", " return model"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We'll train the neural network by preparing our data with the function we defined earlier and feeding the result into our model"]}, {"cell_type": "code", "execution_count": 5, "metadata": {"scrolled": true}, "outputs": [], "source": ["X, y = prep_data(spy_hist)\n", "model = build_model()\n", "\n", "# split data into training/testing sets\n", "X_train = X[:300]\n", "X_test = X[300:]\n", "y_train = y[:300]\n", "y_test = y[300:]\n", "\n", "model.fit(X_train, y_train, epochs=5)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Analyze Performance\n", "We then make predictions on the testing data set. We compare our Predicted Values with the Expected Values by plotting both to see if our Model has predictive power."]}, {"cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": ["y_hat = model.predict(X_test)\n", "df = pd.DataFrame({'y': y_test.flatten(), 'y_hat': y_hat.flatten()})\n", "df.plot(title='Model Performance: predicted vs actual %change in closing price')"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Save the Model to ObjectStore\n", "We first serialize our model into a JSON string, then we save our model to ObjectStore. This way, the model doesn't need to be retrained, saving time and computational resources."]}, {"cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": ["model_key = 'spy_model'\n", "\n", "modelStr = json.dumps(serialize_keras_object(model))\n", "qb.ObjectStore.Save(model_key, modelStr)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Load Model from the ObjectStore\n", "Let's first retreive the JSON for the Keras model that we saved in the ObjectStore, then restore our model from this JSON string"]}, {"cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": ["if qb.ObjectStore.ContainsKey(model_key):\n", " modelStr = qb.ObjectStore.Read(model_key)\n", " config = json.loads(modelStr)['config']\n", " loaded_model = Sequential.from_config(config)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To ensure loading the model was successfuly, let's test the model by having it make predictions."]}, {"cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": ["y_hat = loaded_model.predict(X_test)\n", "df = pd.DataFrame({'y': y_test.flatten(), 'y_hat': y_hat.flatten()})\n", "df.plot(title='Model Performance: predicted vs actual %change in closing price')"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Appendix\n", "Below are some helper methods to manage the ObjectStore keys. We can use these to validate the saving and loading is successful."]}, {"cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": ["def get_ObjectStore_keys():\n", " return [str(j).split(',')[0][1:] for _, j in enumerate(qb.ObjectStore.GetEnumerator())]\n", "\n", "def clear_ObjectStore():\n", " for key in get_ObjectStore_keys():\n", " qb.ObjectStore.Delete(key)"]}, {"cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": ["clear_ObjectStore()"]}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8"}}, "nbformat": 4, "nbformat_minor": 2} 2 | -------------------------------------------------------------------------------- /Documentation/Python/Predicting-Future-Prices-With-Sklearn.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "Untitled6.ipynb", 7 | "provenance": [] 8 | }, 9 | "kernelspec": { 10 | "name": "python3", 11 | "display_name": "Python 3" 12 | } 13 | }, 14 | "cells": [ 15 | { 16 | "cell_type": "markdown", 17 | "metadata": { 18 | "id": "90IRRTyD7wd6", 19 | "colab_type": "text" 20 | }, 21 | "source": [ 22 | "# Predicting Prices Movements With sklearn\n", 23 | "#### An example of sklearn model building, training, saving in the ObjectStore, and loading." 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": { 29 | "id": "IZk8tKlh7vXF", 30 | "colab_type": "text" 31 | }, 32 | "source": [ 33 | "### Import Libraries\n", 34 | "Let's start by importing the functionality we'll need to build the model and to split our data into training/testing sets. We also import pickle so we can store our model in ObjectStore later." 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "metadata": { 40 | "id": "2dHcNlYW7zXM", 41 | "colab_type": "code", 42 | "colab": {} 43 | }, 44 | "source": [ 45 | "from sklearn.svm import SVR\n", 46 | "from sklearn.model_selection import GridSearchCV\n", 47 | "from sklearn.model_selection import train_test_split\n", 48 | "import pickle" 49 | ], 50 | "execution_count": 3, 51 | "outputs": [] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": { 56 | "id": "6TMj7Yd6726F", 57 | "colab_type": "text" 58 | }, 59 | "source": [ 60 | "### Gather & Prepare Data\n", 61 | "Let's retrieve some intraday data for the SPY by making a History request." 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "metadata": { 67 | "id": "h32MeKvA73-4", 68 | "colab_type": "code", 69 | "colab": {} 70 | }, 71 | "source": [ 72 | "qb = QuantBook()\n", 73 | "spy = qb.AddEquity('SPY')\n", 74 | "history = qb.History(qb.Securities.Keys, 360, Resolution.Daily)\n", 75 | "spy_hist = history.loc['SPY']\n", 76 | "spy_hist" 77 | ], 78 | "execution_count": null, 79 | "outputs": [] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": { 84 | "id": "3oOmbM7d76-5", 85 | "colab_type": "text" 86 | }, 87 | "source": [ 88 | "We create a function that prepares our data suitable for training and testing our Model. We use 5 steps of OHLCV data to predict the closing price of the bar right after. By tying this to a function, we increase clarity, as well as reusability, especially if we were to copy it into a class in a .py file." 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "metadata": { 94 | "id": "-cs_jJ2u77ZV", 95 | "colab_type": "code", 96 | "colab": {} 97 | }, 98 | "source": [ 99 | "# function to prepare our data for training our ML Model\n", 100 | "def prep_data(data, n_tsteps=5):\n", 101 | " # n_tsteps is the number of time steps at and before time t we want to use\n", 102 | " # to predict the close price at time t + 1\n", 103 | " \n", 104 | " # this helps normalizes the data\n", 105 | " df = data.pct_change()\n", 106 | " \n", 107 | " # drop the NaNs and infinities\n", 108 | " with pd.option_context('mode.use_inf_as_na', True):\n", 109 | " df = df.dropna()\n", 110 | " \n", 111 | " features = []\n", 112 | " labels = []\n", 113 | "\n", 114 | " for i in range(len(df)-n_tsteps):\n", 115 | " input_data = df.iloc[i:i+n_tsteps].values.flatten()\n", 116 | " features.append(input_data)\n", 117 | " label = df['close'].iloc[i+n_tsteps]\n", 118 | " labels.append(label)\n", 119 | "\n", 120 | " return np.array(features), np.array(labels)" 121 | ], 122 | "execution_count": null, 123 | "outputs": [] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": { 128 | "id": "TWlDK4MV79Je", 129 | "colab_type": "text" 130 | }, 131 | "source": [ 132 | "### Build the Model (SVR with GridSearch Hyperparameter Optimization)\n", 133 | "Let's build the model using sklearn. We use a Support Vector Regressor as it works well with non-linear data. Furthermore, we optimize the hyperparameters of this model using GridSearchCV. We encourage users to experiment with different optimizable hyperparameters (e.g. kernel type) and models (e.g. Random Forests)." 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "metadata": { 139 | "id": "zMZ2HfA97_FD", 140 | "colab_type": "code", 141 | "colab": {} 142 | }, 143 | "source": [ 144 | "def build_model(X, y):\n", 145 | " # note: grid parameters are typically unique to the model\n", 146 | " param_grid = {'C': [.05, .1, .5, 1, 5, 10], 'epsilon': [0.001, 0.005, 0.01, 0.05, 0.1], 'gamma': ['auto', 'scale']} \n", 147 | " gsc = GridSearchCV(SVR(), param_grid, scoring='neg_mean_squared_error', cv=5)\n", 148 | " model = gsc.fit(X, y).best_estimator_\n", 149 | " return model\n", 150 | "\n", 151 | "def build_model_simple(X, y):\n", 152 | " # similar to above, but without hyperparameter optimization\n", 153 | " model = SVR()\n", 154 | " model.fit(X, y)\n", 155 | " return model" 156 | ], 157 | "execution_count": null, 158 | "outputs": [] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": { 163 | "id": "uMcWok128AgJ", 164 | "colab_type": "text" 165 | }, 166 | "source": [ 167 | "Let's build and train our model by feeding in data prepared using the prep_data function." 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "metadata": { 173 | "id": "ahyRIPSn8CCE", 174 | "colab_type": "code", 175 | "colab": {} 176 | }, 177 | "source": [ 178 | "X, y = prep_data(spy_hist)\n", 179 | "\n", 180 | "# split the data for training and testing\n", 181 | "# we need testing data to evaluate how well our model performs on new data \n", 182 | "X_train, X_test, y_train, y_test = train_test_split(X, y)\n", 183 | "\n", 184 | "model = build_model(X_train, y_train)" 185 | ], 186 | "execution_count": null, 187 | "outputs": [] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": { 192 | "id": "9NXg5J7w8C_j", 193 | "colab_type": "text" 194 | }, 195 | "source": [ 196 | "### Analyze Performance\n", 197 | "We then make predictions on the testing data set. We compare our Predicted Values with the Expected Values by plotting both to see if our Model has predictive power." 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "metadata": { 203 | "id": "I1JWsT_X8EXr", 204 | "colab_type": "code", 205 | "colab": {} 206 | }, 207 | "source": [ 208 | "y_hat = model.predict(X_test)\n", 209 | "df = pd.DataFrame({'y': y_test.flatten(), 'y_hat': y_hat.flatten()})\n", 210 | "df.plot(title='Model Performance: predicted vs actual %change in closing price')" 211 | ], 212 | "execution_count": null, 213 | "outputs": [] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "metadata": { 218 | "id": "Neo6axvI8G5D", 219 | "colab_type": "text" 220 | }, 221 | "source": [ 222 | "### Save the Model to ObjectStore\n", 223 | "We dump the model using the pickle module and save the resulting bytes to ObjectStore" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "metadata": { 229 | "id": "JDIpax7A8ICm", 230 | "colab_type": "code", 231 | "colab": {} 232 | }, 233 | "source": [ 234 | "model_key = 'spy_model'\n", 235 | "\n", 236 | "pickled_model = pickle.dumps(model)\n", 237 | "qb.ObjectStore.SaveBytes(model_key, pickled_model)" 238 | ], 239 | "execution_count": null, 240 | "outputs": [] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": { 245 | "id": "OD63cPAD8I84", 246 | "colab_type": "text" 247 | }, 248 | "source": [ 249 | "### Load Model from the ObjectStore\n", 250 | "Let's first retrieve the bytes of the model from ObjectStore. When we retrieve the bytes from ObjectStore, we need to cast it into a form useable by pickle with the bytearray() method." 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "metadata": { 256 | "id": "Dmfa8etn8KeH", 257 | "colab_type": "code", 258 | "colab": {} 259 | }, 260 | "source": [ 261 | "if qb.ObjectStore.ContainsKey(model_key):\n", 262 | " model_bytes = qb.ObjectStore.ReadBytes(model_key)\n", 263 | " model_bytes = bytearray(model_bytes)\n", 264 | " loaded_model = pickle.loads(model_bytes)" 265 | ], 266 | "execution_count": null, 267 | "outputs": [] 268 | }, 269 | { 270 | "cell_type": "markdown", 271 | "metadata": { 272 | "id": "i7PFY3b88Lh0", 273 | "colab_type": "text" 274 | }, 275 | "source": [ 276 | "To ensure the model was successfully loaded, let's see if the model is able to make predictions." 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "metadata": { 282 | "id": "ubINg8l-8MVu", 283 | "colab_type": "code", 284 | "colab": {} 285 | }, 286 | "source": [ 287 | "y_hat = loaded_model.predict(X_test)\n", 288 | "df = pd.DataFrame({'y': y_test.flatten(), 'y_hat': y_hat.flatten()})\n", 289 | "df.plot(title='Model Performance: predicted vs actual %change in closing price')" 290 | ], 291 | "execution_count": null, 292 | "outputs": [] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "metadata": { 297 | "id": "PhYM1KEN8Q9C", 298 | "colab_type": "text" 299 | }, 300 | "source": [ 301 | "# Appendix\n", 302 | "Below are some helper methods to manage the ObjectStore keys. We can use these to validate the saving and loading is successful." 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "metadata": { 308 | "id": "PGjnx2VA8QD-", 309 | "colab_type": "code", 310 | "colab": {} 311 | }, 312 | "source": [ 313 | "def get_ObjectStore_keys():\n", 314 | " return [str(j).split(',')[0][1:] for _, j in enumerate(qb.ObjectStore.GetEnumerator())]\n", 315 | "\n", 316 | "def clear_ObjectStore():\n", 317 | " for key in get_ObjectStore_keys():\n", 318 | " qb.ObjectStore.Delete(key)" 319 | ], 320 | "execution_count": null, 321 | "outputs": [] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "metadata": { 326 | "id": "Zoh62C0n8Vdf", 327 | "colab_type": "code", 328 | "colab": {} 329 | }, 330 | "source": [ 331 | "clear_ObjectStore()" 332 | ], 333 | "execution_count": null, 334 | "outputs": [] 335 | } 336 | ] 337 | } -------------------------------------------------------------------------------- /Documentation/Python/Predicting-Future-Prices-With-TensorFlow.ipynb: -------------------------------------------------------------------------------- 1 | {"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", "
"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Predicting Future Prices With TensorFlow \n", "#### An example of TF model building, training, saving in the ObjectStore, and loading."]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Import Libraries\n", "Let's start by importing the functionality we'll need to build the model, split the data, and serialize/unserialize the model for saving and loading."]}, {"cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": ["import tensorflow as tf\n", "from sklearn.model_selection import train_test_split\n", "import json5\n", "from google.protobuf import json_format"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Gather & Prepare Data\n", "Let's retreive some intraday data for the SPY by making a History request."]}, {"cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": ["qb = QuantBook()\n", "spy = qb.AddEquity(\"SPY\").Symbol\n", "data = qb.History(spy, \n", " datetime(2020, 6, 22), \n", " datetime(2020, 6, 27), \n", " Resolution.Minute).loc[spy].close\n", "data"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We'll use the last 5 closing prices of the SPY as inputs to our model. Here, we create a DataFrame containing this data."]}, {"cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": ["lookback = 5\n", "lookback_series = []\n", "for i in range(1, lookback + 1):\n", " df = data.shift(i)[lookback:-1]\n", " df.name = f\"close_-{i}\"\n", " lookback_series.append(df)\n", "X = pd.concat(lookback_series, axis=1).reset_index(drop=True)\n", "X"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We'd like the model to predict the closing price of the SPY 1 timestep into the future, so let's create a DataFrame containing this data."]}, {"cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": ["Y = data.shift(-1)[lookback:-1].reset_index(drop=True)\n", "Y.plot(figsize=(16, 6))\n", "plt.title(\"SPY price 1 timestep in the future\")\n", "plt.xlabel(\"Time step\")\n", "plt.ylabel(\"Price\")\n", "plt.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now, let's split the data into testing and training sets."]}, {"cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": ["test_size = 0.33\n", "X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.33, shuffle=False)\n", "print(f\"Train index: {X_train.index[0]}...{X_train.index[-1]}\")\n", "print(f\"Test index: {X_test.index[0]}...{X_test.index[-1]}\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Define a Testing Method\n", "To test the model, we'll setup a method to plot test set predictions ontop of the SPY price."]}, {"cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": ["def test_model(sess, output, title, X):\n", " prediction = sess.run(output, feed_dict={X: X_test})\n", " prediction = prediction.reshape(prediction.shape[1], 1)\n", "\n", " y_test.reset_index(drop=True).plot(figsize=(16, 6), label=\"Actual\")\n", " plt.plot(prediction, label=\"Prediction\")\n", " plt.title(title)\n", " plt.xlabel(\"Time step\")\n", " plt.ylabel(\"SPY Price\")\n", " plt.legend()\n", " plt.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Manually Build the Model \n", "Let's build the neural network architecture by utilizing the TensorFlow library. Note how we name the input and output nodes so we can retreive them when loading the model from the ObjectStore."]}, {"cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": ["tf.reset_default_graph()\n", "sess = tf.Session()\n", "\n", "num_factors = X_test.shape[1]\n", "num_neurons_1 = 32\n", "num_neurons_2 = 16\n", "num_neurons_3 = 8\n", "\n", "X = tf.placeholder(dtype=tf.float32, shape=[None, num_factors], name='X')\n", "Y = tf.placeholder(dtype=tf.float32, shape=[None])\n", "\n", "# Initializers\n", "weight_initializer = tf.variance_scaling_initializer(mode=\"fan_avg\", distribution=\"uniform\", scale=1)\n", "bias_initializer = tf.zeros_initializer()\n", "\n", "# Hidden weights\n", "W_hidden_1 = tf.Variable(weight_initializer([num_factors, num_neurons_1]))\n", "bias_hidden_1 = tf.Variable(bias_initializer([num_neurons_1]))\n", "W_hidden_2 = tf.Variable(weight_initializer([num_neurons_1, num_neurons_2]))\n", "bias_hidden_2 = tf.Variable(bias_initializer([num_neurons_2]))\n", "W_hidden_3 = tf.Variable(weight_initializer([num_neurons_2, num_neurons_3]))\n", "bias_hidden_3 = tf.Variable(bias_initializer([num_neurons_3]))\n", "\n", "# Output weights\n", "W_out = tf.Variable(weight_initializer([num_neurons_3, 1]))\n", "bias_out = tf.Variable(bias_initializer([1]))\n", "\n", "# Hidden layer\n", "hidden_1 = tf.nn.relu(tf.add(tf.matmul(X, W_hidden_1), bias_hidden_1))\n", "hidden_2 = tf.nn.relu(tf.add(tf.matmul(hidden_1, W_hidden_2), bias_hidden_2))\n", "hidden_3 = tf.nn.relu(tf.add(tf.matmul(hidden_2, W_hidden_3), bias_hidden_3))\n", "\n", "# Output layer\n", "output = tf.transpose(tf.add(tf.matmul(hidden_3, W_out), bias_out), name='outer')"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We'll train the neural network by iteratively minimizing the mean squared difference between the model predictions and the actual SPY price."]}, {"cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": ["loss = tf.reduce_mean(tf.squared_difference(output, Y))\n", "optimizer = tf.train.AdamOptimizer().minimize(loss)\n", "sess.run(tf.global_variables_initializer())\n", "\n", "batch_size = len(y_train) // 10\n", "epochs = 20\n", "for _ in range(epochs):\n", " for i in range(0, len(y_train) // batch_size):\n", " start = i * batch_size\n", " batch_x = X_train[start:start + batch_size]\n", " batch_y = y_train[start:start + batch_size]\n", " sess.run(optimizer, feed_dict={X: batch_x, Y: batch_y})"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To ensure the model we've built and trained is working, let's plot it's predictions on the test set."]}, {"cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [" test_model(sess, output, \"Test Set Results from Original Model\", X)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Save Model to the ObjectStore\n", "We first serialize the TensorFlow graph and weights to JSON format, then save these in the ObjectStore by using the `Save` method."]}, {"cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": ["graph_definition = tf.compat.v1.train.export_meta_graph()\n", "json_graph = json_format.MessageToJson(graph_definition)\n", "\n", "def get_json_weights(sess):\n", " weights = sess.run(tf.compat.v1.trainable_variables())\n", " weights = [w.tolist() for w in weights]\n", " weights_list = json5.dumps(weights)\n", " return weights_list\n", "json_weights = get_json_weights(sess)\n", "sess.close()\n", "\n", "qb.ObjectStore.Save('graph', json_graph)\n", "qb.ObjectStore.Save('weights', json_weights)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Load Model from the ObjectStore\n", "Let's first retreive the JSON for the TensorFlow graph and weights that we saved in the ObjectStore."]}, {"cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": ["json_graph = qb.ObjectStore.Read('graph')\n", "json_weights = qb.ObjectStore.Read('weights')"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now let's restore the TensorFlow graph from JSON and select the input and output nodes."]}, {"cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": ["tf.reset_default_graph()\n", "graph_definition = json_format.Parse(json_graph, tf.compat.v1.MetaGraphDef())\n", "sess = tf.Session()\n", "tf.compat.v1.train.import_meta_graph(graph_definition)\n", "X = tf.compat.v1.get_default_graph().get_tensor_by_name('X:0')\n", "output = tf.compat.v1.get_default_graph().get_tensor_by_name('outer:0')"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To avoid retraining the model after loading, let's restore the weights."]}, {"cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": ["weights = [np.asarray(x) for x in json5.loads(json_weights)]\n", "\n", "assign_ops = []\n", "feed_dict = {}\n", "vs = tf.compat.v1.trainable_variables()\n", "zipped_values = zip(vs, weights)\n", "for var, value in zipped_values:\n", " value = np.asarray(value)\n", " assign_placeholder = tf.placeholder(var.dtype, shape=value.shape)\n", " assign_op = var.assign(assign_placeholder)\n", " assign_ops.append(assign_op)\n", " feed_dict[assign_placeholder] = value\n", "sess.run(assign_ops, feed_dict=feed_dict);"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To ensure loading the model was successfuly, let's test the model."]}, {"cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": ["test_model(sess, output, \"Test Set Results from Loaded Model\", X)\n", "sess.close()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Appendix\n", "Below are some helper methods to manage the ObjectStore keys. We can use these to validate the saving and loading is successful."]}, {"cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": ["def get_ObjectStore_keys():\n", " return [str(j).split(',')[0][1:] for _, j in enumerate(qb.ObjectStore.GetEnumerator())]\n", "\n", "def clear_ObjectStore():\n", " for key in get_ObjectStore_keys():\n", " qb.ObjectStore.Delete(key)"]}, {"cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": ["clear_ObjectStore()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": []}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8"}}, "nbformat": 4, "nbformat_minor": 2} -------------------------------------------------------------------------------- /Documentation/Python/Stock-Picking-With-Fundamental-Data.ipynb: -------------------------------------------------------------------------------- 1 | {"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", "
"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Stock Picking With Fundamental Data\n", "#### An example of selecting stocks in a sector based on their relative PE ratios."]}, {"cell_type": "markdown", "metadata": {}, "source": ["First we import the matplotlib library to assist with plotting."]}, {"cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": ["import matplotlib.pyplot as plt"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We create our QuantBook object."]}, {"cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": ["qb = QuantBook()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Next, we create a list of symbol objects for several large airline companies."]}, {"cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": ["tickers = [\n", " \"ALK\", # Alaska Air Group, Inc.\n", " \"AAL\", # American Airlines Group, Inc.\n", " \"DAL\", # Delta Air Lines, Inc.\n", " \"LUV\", # Southwest Airlines Company\n", " \"UAL\", # United Air Lines\n", " \"ALGT\", # Allegiant Travel Company\n", " \"SKYW\" # SkyWest, Inc.\n", "]\n", "symbols = [qb.AddEquity(ticker, Resolution.Daily).Symbol for ticker in tickers]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We get the PE ratios of the symbols throughout 2014."]}, {"cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": ["pe_ratios = qb.GetFundamental(symbols, \n", " \"ValuationRatios.PERatio\", \n", " datetime(2014, 1, 1),\n", " datetime(2015, 1, 1))\n", "pe_ratios"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we plot the PE ratios across time."]}, {"cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": ["pe_ratios.plot(figsize=(16, 8), title=\"PE Ratio Over Time\")\n", "plt.xlabel(\"Time\")\n", "plt.ylabel(\"PE Ratio\")\n", "plt.show()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We want to select the symbols with the highest and lowest average PE ratios. First, we call `mean()` on the DataFrame and then sort the values with `sort_values()`."]}, {"cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": ["sorted_by_mean_pe = pe_ratios.mean().sort_values()\n", "sorted_by_mean_pe"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We select the symbols with the highest and lowest average PE ratios by selecting the first and last element in the series index."]}, {"cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": ["highest_avg_pe = qb.Symbol(sorted_by_mean_pe.index[-1])\n", "lowest_avg_pe = qb.Symbol(sorted_by_mean_pe.index[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We now gather the historical closing prices for the securities we found to have the highest and lowest PE ratios."]}, {"cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": ["history = qb.History([highest_avg_pe, lowest_avg_pe], \n", " datetime(2009, 1, 1), \n", " datetime(2015, 1, 1), \n", " Resolution.Daily).close.unstack(level=0)\n", "history"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Lastly, we plot the returns generated by investing in these securities from 2009 to 2015."]}, {"cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": ["returns_over_time = ((history.pct_change()[1:] + 1).cumprod() - 1)\n", "returns_over_time.plot(figsize=(16, 8), title=\"Returns Over Time\")\n", "plt.ylabel(\"Return\")\n", "plt.show()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": []}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8"}}, "nbformat": 4, "nbformat_minor": 2} -------------------------------------------------------------------------------- /Explore/Inter font/Inter-VariableFont_slnt,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/Inter-VariableFont_slnt,wght.ttf -------------------------------------------------------------------------------- /Explore/Inter font/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /Explore/Inter font/README.txt: -------------------------------------------------------------------------------- 1 | Inter Variable Font 2 | =================== 3 | 4 | This download contains Inter as both a variable font and static fonts. 5 | 6 | Inter is a variable font with these axes: 7 | slnt 8 | wght 9 | 10 | This means all the styles are contained in a single file: 11 | Inter-VariableFont_slnt,wght.ttf 12 | 13 | If your app fully supports variable fonts, you can now pick intermediate styles 14 | that aren’t available as static fonts. Not all apps support variable fonts, and 15 | in those cases you can use the static font files for Inter: 16 | static/Inter-Thin.ttf 17 | static/Inter-ExtraLight.ttf 18 | static/Inter-Light.ttf 19 | static/Inter-Regular.ttf 20 | static/Inter-Medium.ttf 21 | static/Inter-SemiBold.ttf 22 | static/Inter-Bold.ttf 23 | static/Inter-ExtraBold.ttf 24 | static/Inter-Black.ttf 25 | 26 | Get started 27 | ----------- 28 | 29 | 1. Install the font files you want to use 30 | 31 | 2. Use your app's font picker to view the font family and all the 32 | available styles 33 | 34 | Learn more about variable fonts 35 | ------------------------------- 36 | 37 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 38 | https://variablefonts.typenetwork.com 39 | https://medium.com/variable-fonts 40 | 41 | In desktop apps 42 | 43 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 44 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 45 | 46 | Online 47 | 48 | https://developers.google.com/fonts/docs/getting_started 49 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 50 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 51 | 52 | Installing fonts 53 | 54 | MacOS: https://support.apple.com/en-us/HT201749 55 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 56 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 57 | 58 | Android Apps 59 | 60 | https://developers.google.com/fonts/docs/android 61 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 62 | 63 | License 64 | ------- 65 | Please read the full license text (OFL.txt) to understand the permissions, 66 | restrictions and requirements for usage, redistribution, and modification. 67 | 68 | You can use them in your products & projects – print or digital, 69 | commercial or otherwise. 70 | 71 | This isn't legal advice, please consider consulting a lawyer and see the full 72 | license for all details. 73 | -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-Black.ttf -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-Bold.ttf -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-ExtraLight.ttf -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-Light.ttf -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-Medium.ttf -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-Regular.ttf -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /Explore/Inter font/static/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/Inter font/static/Inter-Thin.ttf -------------------------------------------------------------------------------- /Explore/default_photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/default_photo.png -------------------------------------------------------------------------------- /Explore/generate_social_media_thumbnails.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import plotly.graph_objects as go 4 | from io import BytesIO 5 | from pathlib import Path 6 | from PIL import Image, ImageDraw, ImageFont 7 | from requests import get 8 | from typing import List 9 | 10 | def __get_text_content(url: str) -> str: 11 | return get(url).content.decode('utf-8') 12 | 13 | def __get_json_content(url: str) -> List: 14 | content = __get_text_content(url) \ 15 | .replace("null", "None").replace("true", "True").replace("false", "False") 16 | try: 17 | return eval(content) 18 | except: 19 | exit(f'Invalid content for {url}') 20 | 21 | def __get_profile(url: str) -> Image: 22 | if not url or 'icon' in url: 23 | return None 24 | 25 | image = get(url.replace("\\",""), stream=True) 26 | if image.status_code != 200: 27 | return None 28 | 29 | profile = Image.open(image.raw).convert("RGB") 30 | 31 | h,w = profile.size 32 | 33 | # creating luminous image 34 | lum_img = Image.new('L',[h,w] ,0) 35 | draw = ImageDraw.Draw(lum_img) 36 | draw.pieslice([(0,0),(h,w)],0,360,fill=255) 37 | img_arr = np.array(profile) 38 | lum_img_arr = np.array(lum_img) 39 | final_img_arr = np.dstack((img_arr, lum_img_arr)) 40 | return Image.fromarray(final_img_arr).resize((80,80)) 41 | 42 | def __get_equity_curve(strategy, width=1200, height=400) -> Image: 43 | # Add line 44 | fig = go.Figure() 45 | 46 | x, y = [],[] 47 | statistics = strategy.get('statistics') 48 | if statistics: 49 | for point in statistics.get('sparkline',[]): 50 | x.append(point['time']) 51 | y.append(point['value']) 52 | 53 | fig.add_trace(go.Scatter(x=x, y=y, line=dict(color='#0096FF', width=3))) 54 | 55 | fig.update_layout( 56 | autosize=False, 57 | width=width, 58 | height=height, 59 | paper_bgcolor='rgba(0,0,0,0)', 60 | plot_bgcolor='rgba(0,0,0,0)', 61 | margin=dict(l=0, r=0, t=0, b=0), 62 | xaxis=dict(visible=False), 63 | yaxis=dict(visible=False) 64 | ) 65 | 66 | fig_bytes = fig.to_image(format="png") 67 | return Image.open(BytesIO(fig_bytes)) 68 | 69 | def __create_landscape(template, strategy, profile) -> Image: 70 | # Create a copy of the template to add elements 71 | copy = template.copy() 72 | 73 | # Add profile picture 74 | copy.paste(profile, (681, 120),mask=profile) 75 | 76 | # Add texts 77 | I1 = ImageDraw.Draw(copy) 78 | name = strategy.get('name') 79 | author_name = strategy.get('authorName') 80 | category = strategy.get('category') 81 | if not category: category = '' 82 | category = category.upper() 83 | 84 | width = name_font.getlength(name) 85 | if width < 530: 86 | I1.text((105,120), name, font=name_font, fill='#313131') 87 | else: 88 | parts = name.split(' ') 89 | for i in reversed(range(len(parts))): 90 | if name_font.getlength(' '.join(parts[:i])) <= 530: 91 | break 92 | I1.text((105,120), ' '.join(parts[:i]), font=name_font, fill='#313131') 93 | I1.text((105,155), ' '.join(parts[i:]), font=name_font, fill='#313131') 94 | 95 | width = author_font.getlength(author_name) 96 | if width < 250: 97 | I1.text((775,157), author_name, font=author_font, fill='#313131') 98 | else: 99 | parts = author_name.split(' ') 100 | for i in reversed(range(len(parts))): 101 | if author_font.getlength(' '.join(parts[:i])) <= 250: 102 | break 103 | if i == 0: 104 | while author_font.getlength(author_name) > 250: 105 | author_name = author_name[:-1] 106 | I1.text((775,157), f'{author_name}...', font=author_font, fill='#313131') 107 | else: 108 | I1.text((775,157), ' '.join(parts[:i]), font=author_font, fill='#313131') 109 | I1.text((775,182), ' '.join(parts[i:]), font=author_font, fill='#313131') 110 | 111 | I1.text((105,200), category, font=category_font, 112 | fill=colors_by_category.get(category, '#313131')) 113 | 114 | fig = __get_equity_curve(strategy) 115 | copy.paste(fig, (0, 210),mask=fig) 116 | return copy 117 | 118 | def __create_square(template, strategy, profile) -> Image: 119 | # Create a copy of the template to add elements 120 | copy = template.copy() 121 | 122 | # Add texts 123 | I1 = ImageDraw.Draw(copy) 124 | name = strategy.get('name') 125 | author_name = strategy.get('authorName') 126 | category = strategy.get('category') 127 | if not category: category = '' 128 | category = category.upper() 129 | 130 | width = name_font.getlength(name) 131 | if width < 650: 132 | I1.text((42,160), name, font=name_font, fill='#313131') 133 | else: 134 | parts = name.split(' ') 135 | for i in reversed(range(len(parts))): 136 | if name_font.getlength(' '.join(parts[:i])) <= 650: 137 | break 138 | I1.text((42,160), ' '.join(parts[:i]), font=name_font, fill='#313131') 139 | I1.text((42,195), ' '.join(parts[i:]), font=name_font, fill='#313131') 140 | 141 | I1.text((42,240), category, font=category_font, 142 | fill=colors_by_category.get(category, '#313131')) 143 | 144 | # Add profile picture 145 | copy.paste(profile, (42, 325),mask=profile) 146 | 147 | I1.text((137, 360), author_name, font=author_font, fill='#313131') 148 | 149 | fig = __get_equity_curve(strategy, 860, 285) 150 | copy.paste(fig, (0, 440),mask=fig) 151 | return copy 152 | 153 | colors_by_category = { 154 | 'CRYPTO': '#C39E78', 155 | 'EQUITIES': '#BF7C7C', 156 | 'US EQUITIES': '#9A74BF', 157 | 'ETF': '#B44444', 158 | 'FOREX': '#5FB26F', 159 | 'FUTURES': '#5D6586' 160 | } 161 | 162 | if __name__ == '__main__': 163 | 164 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 165 | destination_folder = Path('thumbnails') 166 | destination_folder.mkdir(parents=True, exist_ok=True) 167 | 168 | template_landscape = Image.open('template_landscape.png') 169 | template_square = Image.open('template_square.png') 170 | name_font = ImageFont.FreeTypeFont('Inter font/static/Inter-SemiBold.ttf', 35) 171 | category_font = ImageFont.FreeTypeFont('Inter font/static/Inter-Regular.ttf', 20) 172 | author_font = ImageFont.FreeTypeFont('Inter font/static/Inter-Regular.ttf', 24) 173 | default_photo = Image.open('default_photo.png').resize((80,80)) 174 | profile_errors = set() 175 | content = __get_json_content('https://www.quantconnect.com/api/v2/sharing/strategies/list/') 176 | strategies = content.get('strategies', []) 177 | 178 | for strategy in strategies: 179 | id = strategy.get('projectId') 180 | if not id: 181 | print(f'No project Id for {strategy}') 182 | continue 183 | 184 | profile_picture = __get_profile(strategy['authorProfile']) 185 | if not profile_picture: 186 | profile_picture = default_photo 187 | profile_errors.add(strategy.get("authorName")) 188 | 189 | landscape = __create_landscape(template_landscape, strategy, profile_picture) 190 | landscape.save(f"thumbnails/{id}.png") 191 | 192 | square = __create_square(template_square, strategy, profile_picture) 193 | square.save(f"thumbnails/{id}_square.png") 194 | 195 | if profile_errors: 196 | print("Cannot download profile images of: " + ','.join(profile_errors)) -------------------------------------------------------------------------------- /Explore/template_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/template_landscape.png -------------------------------------------------------------------------------- /Explore/template_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/Explore/template_square.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt tag](https://cdn.quantconnect.com/research/i/research-banner.png) 2 | 3 | This repository is a collection of research notebooks and tutorials using the QuantConnect LEAN platform. Research covers a range of topics from tutorial focused demonstrations to topical analysis of modern movements in the financial markets. 4 | 5 | ### Topical Events 6 | 7 | - May 7, 2020 - [S&P500 Hope vs Fear CV2019](https://github.com/QuantConnect/Research/blob/master/Topical/20200507_hopevfear_research.ipynb) 8 | - June 4, 2020 - [Airline Bailout & Buybacks Research](https://github.com/QuantConnect/Research/blob/master/Topical/20200601_airlinebuybacks_research.ipynb) 9 | 10 | ### Idea Streams PodCast 11 | 12 | - May 28, 2020 Episode 5 - [Tail Risk Hedging](https://www.youtube.com/watch?v=dA7VaQvpCGg&t=1s) 13 | - May 22, 2020 Episode 4 - [Nowcasting News Announcements of Vaccine Trials](https://www.youtube.com/watch?v=ZmatDMCvKTE&t=686s) 14 | - May 10, 2020 Episode 3 - [Correlation Analysis Major CV19 Economies](https://www.youtube.com/watch?v=wflTPzl9YF4) 15 | - April 28, 2020 Episode 2 - [Unemployment Claims with SKLean](https://www.youtube.com/watch?v=VCf9e0S4rDg) 16 | - April 20th, 2020 Episode 1 - [Traunch Rebalancing](https://www.youtube.com/watch?v=q1VjM1nHPfE) 17 | 18 | ### Research 2 Production Notebook Series 19 | 20 | - [Mean Reversion](https://github.com/QuantConnect/Research/blob/master/Research2Production/01%20Mean%20Reversion.ipynb) 21 | - [Random Forest Regression](https://github.com/QuantConnect/Research/blob/master/Research2Production/02%20Random%20Forest%20Regression.ipynb) 22 | - [Uncorrelated Assets](https://github.com/QuantConnect/Research/blob/master/Research2Production/03%20Uncorrelated%20Assets.ipynb) 23 | - [Kalman Filters and Pairs Trading](https://github.com/QuantConnect/Research/blob/master/Research2Production/04%20Kalman%20Filters%20and%20Pairs%20Trading.ipynb) 24 | - [Stationary Processes and Z-Scores](https://github.com/QuantConnect/Research/blob/master/Research2Production/05%20Stationary%20Processes%20and%20Z-Scores.ipynb) 25 | - [Principal Component Analysis](https://github.com/QuantConnect/Research/blob/master/Research2Production/06%20Principal%20Component%20Analysis.ipynb) 26 | - [Hidden Markov Models](https://github.com/QuantConnect/Research/blob/master/Research2Production/07%20Hidden%20Markov%20Models.ipynb) 27 | - [Long Short-Term Memory](https://github.com/QuantConnect/Research/blob/master/Research2Production/08%20Long%20Short-Term%20Memory.ipynb) 28 | 29 | ### Analysis Examples 30 | - [Fudamental Factor Analysis](https://github.com/QuantConnect/Research/blob/master/Analysis/01%20Fudamental%20Factor%20Analysis.ipynb): This research applies MorningStar fundamental data to demonstrate how to select the effective factors for long/short strategies. 31 | 32 | - [Kalman Filter Based Pairs Trading](https://github.com/QuantConnect/Research/blob/master/Analysis/02%20Kalman%20Filter%20Based%20Pairs%20Trading.ipynb): This research demonstrates the basic principle of pairs trading and introduces the concepts of cointegration and Kalman Filter for pairs trading. 33 | 34 | - [Mean-Variance Portfolio Optimization](https://github.com/QuantConnect/Research/blob/master/Analysis/03%20Mean-Variance%20Portfolio%20Optimization%20.ipynb): This research demonstrates the mean-variance approach to asset allocation in modern portfolio theory and shows how to find the efficient frontier. 35 | 36 | - [EMA Cross Strategy Based on VXX](https://github.com/QuantConnect/Research/blob/master/Analysis/04%20EMA%20Cross%20Strategy%20Based%20on%20VXX.ipynb): This research demonstrates how to build a simple EMA cross strategy with Python and how to get the performance statistics of the strategy. 37 | 38 | - [Pairs Trading Strategy Based on Cointegration](https://github.com/QuantConnect/Research/blob/master/Analysis/05%20Pairs%20Trading%20Strategy%20Based%20on%20Cointegration.ipynb): This research goes through the development process step-by-step of a pairs trading strategy and shows how to backtest the strategy. 39 | -------------------------------------------------------------------------------- /Research2Production/CSharp/01 Mean Reversion CSharp.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Mean Reversion\n", 16 | "A number of financial signals can be thought to be mean-reverting (i.e. the spread between two assets prices). There are a number of ways to test for mean-reversion, but in this example we will assume it holds.\n" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "We use the research notebook to calculate the spread between an asset's price and its historical mean (past 30 days). Then, we calculate the standard deviation and determine which assets\n", 24 | "have moved more than one standard deviation below their average price. The theory is that they are due to revert back towards the mean, and so we can take advantage of this by taking \n", 25 | "long positions." 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 1, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "// QuantBook C# Research Environment\n", 35 | "// For more information see https://www.quantconnect.com/docs/research/overview\n", 36 | "#load \"../QuantConnect.csx\"\n", 37 | "\n", 38 | "// Initialize QuantBook\n", 39 | "var qb = new QuantBook();\n", 40 | "\n", 41 | "var assets = new List() {\"SHY\", \"TLT\", \"SHV\", \"TLH\", \"EDV\", \"BIL\",\n", 42 | " \"SPTL\", \"TBT\", \"TMF\", \"TMV\", \"TBF\", \"VGSH\", \"VGIT\",\n", 43 | " \"VGLT\", \"SCHO\", \"SCHR\", \"SPTS\", \"GOVT\"};\n", 44 | "\n", 45 | "// Subscribed to data for given assets\n", 46 | "foreach(var ticker in assets){\n", 47 | " qb.AddEquity(ticker, Resolution.Minute);\n", 48 | "}\n", 49 | "\n", 50 | "// Request past 30 days of historical price data for given assets\n", 51 | "var history = qb.History(qb.Securities.Keys, 30, Resolution.Daily);\n", 52 | "\n", 53 | "\n", 54 | "var closes = new Dictionary>();\n", 55 | "\n", 56 | "// extract close prices for each Symbol from Slice data\n", 57 | "foreach(var slice in history){\n", 58 | " foreach(var symbol in slice.Keys){\n", 59 | " if(!closes.ContainsKey(symbol)){\n", 60 | " closes.Add(symbol, new List());\n", 61 | " }\n", 62 | " closes[symbol].Add(slice.Bars[symbol].Close);\n", 63 | " }\n", 64 | "}\n" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 2, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "var means = new Dictionary();\n", 74 | "\n", 75 | "// Calculate mean price over 30 day period for each symbol\n", 76 | "foreach(var symbol in closes.Keys){\n", 77 | " if(!means.ContainsKey(symbol)){\n", 78 | " var avg = closes[symbol].Average();\n", 79 | " means.Add(symbol, avg);\n", 80 | " }\n", 81 | "}\n", 82 | "\n", 83 | "var std = new Dictionary();\n", 84 | "\n", 85 | "// Calculate standard deviation of prices for each symbol\n", 86 | "foreach(var symbol in closes.Keys){\n", 87 | " if(!std.ContainsKey(symbol)){\n", 88 | " var average = means[symbol];\n", 89 | " var sumOfSquaresOfDifferences = closes[symbol].Select(val => (val - average) * (val - average)).Sum();\n", 90 | " var sd = (Decimal)Math.Sqrt((double) (sumOfSquaresOfDifferences / closes[symbol].Count()));\n", 91 | "\n", 92 | " std.Add(symbol, sd);\n", 93 | " }\n", 94 | "}" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 3, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "var selected = new List();\n", 104 | "\n", 105 | "// Select symbols which are 1 standard deviation below their 30 day mean\n", 106 | "foreach(var symbol in closes.Keys){\n", 107 | " var close_values = closes[symbol];\n", 108 | " var last_close = close_values[close_values.Count - 1];\n", 109 | " var lower_bound = means[symbol] - std[symbol];\n", 110 | " \n", 111 | " if(last_close < lower_bound){\n", 112 | " selected.Add(symbol);\n", 113 | " }\n", 114 | "}\n", 115 | "\n", 116 | "selected.ForEach(x => {Console.WriteLine(x);});" 117 | ] 118 | } 119 | ], 120 | "metadata": { 121 | "kernelspec": { 122 | "display_name": "C#", 123 | "language": "csharp", 124 | "name": "csharp" 125 | }, 126 | "language_info": { 127 | "file_extension": ".cs", 128 | "mimetype": "text/x-csharp", 129 | "name": "C#", 130 | "pygments_lexer": "c#", 131 | "version": "4.0.30319" 132 | }, 133 | "pycharm": { 134 | "stem_cell": { 135 | "cell_type": "raw", 136 | "metadata": { 137 | "collapsed": false 138 | }, 139 | "source": [] 140 | } 141 | } 142 | }, 143 | "nbformat": 4, 144 | "nbformat_minor": 2 145 | } 146 | -------------------------------------------------------------------------------- /Research2Production/CSharp/03 Uncorrelated Assets CSharp.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Uncorrelated Assets\n", 16 | "\n", 17 | "Finding uncorrelated assets allows you to find a portfolio that will, theoretically, be more diversified and resilient to extreme market events. When combined with other indicators and data sources, this can be an important component in building an algorithm that limits drawdown and remains profitable in choppy markets.\n", 18 | "\n", 19 | "The first step is to execute the cell containing our correlation function and then grab the historical data for the securities and use this to fetch returns data." 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 1, 25 | "metadata": { 26 | "scrolled": true 27 | }, 28 | "outputs": [], 29 | "source": [ 30 | "// QuantBook C# Research Environment\n", 31 | "// For more information see https://www.quantconnect.com/docs/research/overview\n", 32 | "#load \"../QuantConnect.csx\"\n", 33 | "using MathNet.Numerics.Statistics;\n", 34 | "\n", 35 | "var qb = new QuantBook();\n", 36 | "\n", 37 | "var tickers = new List {\"SQQQ\", \"TQQQ\", \"TVIX\", \"VIXY\", \"SPLV\",\n", 38 | " \"SVXY\", \"UVXY\", \"EEMV\", \"EFAV\", \"USMV\"};\n", 39 | "\n", 40 | "foreach(var ticker in tickers){\n", 41 | " qb.AddEquity(ticker, Resolution.Minute);\n", 42 | "}\n", 43 | "\n", 44 | "// Fetch history\n", 45 | "var history = qb.History(qb.Securities.Keys, 150, Resolution.Hour);" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 2, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "// Calculates the % returns of our symbols over the historical data period\n", 55 | "public Dictionary> GetReturns(IEnumerable history){\n", 56 | " var returns = new Dictionary>();\n", 57 | " var last = new Dictionary();\n", 58 | " foreach(var slice in history){\n", 59 | " foreach(var symbol in slice.Bars.Keys){\n", 60 | " if(!returns.ContainsKey(symbol)){\n", 61 | " returns.Add(symbol, new List());\n", 62 | " last.Add(symbol, (Double)slice.Bars[symbol].Close);\n", 63 | " }\n", 64 | " var change = (Double) ((Double)slice.Bars[symbol].Close - last[symbol])/last[symbol];\n", 65 | " returns[symbol].Add(change);\n", 66 | " } \n", 67 | " }\n", 68 | " return returns;\n", 69 | "}\n", 70 | "\n", 71 | "// Calculates the net absolute correlation between a given security and the remaining securities\n", 72 | "public Dictionary GetCorrelations(Dictionary> returns){\n", 73 | " \n", 74 | " var correlations = new Dictionary();\n", 75 | " \n", 76 | " foreach(var symbol in returns.Keys){\n", 77 | " if(!correlations.ContainsKey(symbol)){\n", 78 | " correlations.Add(symbol, 0);\n", 79 | " }\n", 80 | " \n", 81 | " foreach(var symbol2 in returns.Keys){\n", 82 | " if(symbol == symbol2) {\n", 83 | " continue;\n", 84 | " }\n", 85 | " var corr = Correlation.Pearson(returns[symbol], returns[symbol2]);\n", 86 | " correlations[symbol] += Math.Abs(corr);\n", 87 | " } \n", 88 | " \n", 89 | " } \n", 90 | " \n", 91 | " return correlations;\n", 92 | "}\n" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "Then, we calculate the correlation of the returns, which gives us a correlation matrix. In the GetUncorrelatedAssets function, we figure out which symbols have the lowest overall correlation with the rest of the symbols as a whole -- we want to find the five assets with the lowest average absolute correlation so that we can trade them without fearing that any pair are too highly correlated." 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 3, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "// Get hourly returns\n", 109 | "var returns = GetReturns(history);\n", 110 | "\n", 111 | "// Get correlations\n", 112 | "var corr = GetCorrelations(returns);\n", 113 | " \n", 114 | "// Get 5 assets with least overall correlation\n", 115 | "var uncorrelatedAssets = corr.OrderBy(x => x.Value).Take(5); \n", 116 | " \n", 117 | "\n", 118 | "foreach(var kvp in uncorrelatedAssets){\n", 119 | " Console.WriteLine(kvp.Key + \" , \" + kvp.Value);\n", 120 | "}" 121 | ] 122 | } 123 | ], 124 | "metadata": { 125 | "kernelspec": { 126 | "display_name": "C#", 127 | "language": "csharp", 128 | "name": "csharp" 129 | }, 130 | "language_info": { 131 | "file_extension": ".cs", 132 | "mimetype": "text/x-csharp", 133 | "name": "C#", 134 | "pygments_lexer": "c#", 135 | "version": "4.0.30319" 136 | }, 137 | "pycharm": { 138 | "stem_cell": { 139 | "cell_type": "raw", 140 | "metadata": { 141 | "collapsed": false 142 | }, 143 | "source": [] 144 | } 145 | } 146 | }, 147 | "nbformat": 4, 148 | "nbformat_minor": 2 149 | } 150 | -------------------------------------------------------------------------------- /Research2Production/CSharp/04 Kalman Filters and Pairs Trading CSharp.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Kalman Filters and Pairs Trading\n", 16 | " \n", 17 | "There are a few Python packages out there for Kalman filters, but we're adapting this example and the Kalman filter class code from [this article](https://www.quantstart.com/articles/kalman-filter-based-pairs-trading-strategy-in-qstrader) and demonstrating how you can implement similar ideas using QuantConnect!,\n", 18 | " \n", 19 | "Briefly, a Kalman filter is a [state-space model](https://en.wikipedia.org/wiki/State-space_representation) applicable to linear dynamic systems -- systems whose state is time-dependent and state variations are represented linearly. The model is used to estimate unknown states of a variable based on a series of past values. The procedure is two-fold: a prediction (estimate) is made by the filter of the current state of a variable and the uncertainty of the estimate itself. When new data is available, these estimates are updated. There is a lot of information available about Kalman filters, and the variety of their applications is pretty astounding, but for now, we're going to use a Kalman filter to estimate the hedge ratio between a pair of equities.\n", 20 | " \n", 21 | "The idea behind the strategy is pretty straightforward: take two equities that are cointegrated and create a long-short portfolio. The premise of this is that the spread between the value of our two positions should be mean-reverting. Anytime the spread deviates from its expected value, one of the assets moved in an unexpected direction and is due to revert back. When the spread diverges, you can take advantage of this by going long or short on the spread.\n", 22 | " \n", 23 | "To illustrate, imagine you have a long position in AAPL worth \\\\\\\\$2000 and a short position in IBM worth \\\\\\\\$2000. This gives you a net spread of \\\\\\\\$0. Since you expected AAPL and IBM to move together, then if the spread increases significantly above \\\\\\\\$0, you would short the spread in the expectation that it will return to \\\\\\\\$0, it's natural equilibrium. Similarly, if the value drops significantly below \\\\\\\\$0, you would long the spread and capture the profits as its value returns to \\\\\\\\$0. In our application, the Kalman filter will be used to track the hedging ratio between our equities to ensure that the portfolio value is stationary, which means it will continue to exhibit mean-reversion behavior." 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 7, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "// QuantBook C# Research Environment\n", 33 | "// For more information see https://www.quantconnect.com/docs/research/overview\n", 34 | "#load \"../QuantConnect.csx\"\n", 35 | "var qb = new QuantBook();\n", 36 | "var via = qb.AddEquity(\"VIA\", Resolution.Daily).Symbol;\n", 37 | "var viab = qb.AddEquity(\"VIAB\", Resolution.Daily).Symbol;" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 8, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "public class KalmanFilter\n", 47 | "{\n", 48 | " \n", 49 | " private decimal delta = 0.0001M;\n", 50 | " private decimal[,] wt = new decimal[,] {{0.0001M / (1 - 0.0001M), 0}, {0 , 0.0001M / (1 - 0.0001M)}};\n", 51 | " private decimal vt = 0.001M;\n", 52 | " private decimal[] theta = new decimal[2] {0, 0};\n", 53 | " private decimal[,] P = new decimal[,] {{0, 0}, {0, 0}};\n", 54 | " private decimal[,] R = null;\n", 55 | " private decimal qty = 2000;\n", 56 | " private decimal[,] C = new decimal[2,2];\n", 57 | " \n", 58 | " \n", 59 | " public List Update(decimal priceOne, decimal priceTwo)\n", 60 | " {\n", 61 | "// Create the observation matrix of the latest prices\n", 62 | "// of TLT and the intercept value (1.0)\n", 63 | " decimal[] F = new decimal[2] {priceOne, 1.0M};\n", 64 | " decimal y = priceTwo;\n", 65 | " \n", 66 | " \n", 67 | "// The prior value of the states \\theta_t is\n", 68 | "// distributed as a multivariate Gaussian with\n", 69 | "// mean a_t and variance-covariance R_t\n", 70 | " if(R == null){\n", 71 | " R = new decimal[,] {{0, 0}, {0, 0}};\n", 72 | " }else{ \n", 73 | " for(int k = 0; k <= 1; k++){\n", 74 | " for(int j = 0; j <= 1; j++){\n", 75 | " R[k,j] = C[k,j] + wt[k,j];\n", 76 | " }\n", 77 | " }\n", 78 | " }\n", 79 | " \n", 80 | "// Calculate the Kalman Filter update\n", 81 | "// ----------------------------------\n", 82 | "// Calculate prediction of new observation\n", 83 | "// as well as forecast error of that prediction\n", 84 | " decimal yhat = F[0] * theta[0] + F[1] * theta[1];\n", 85 | " decimal et = y - yhat;\n", 86 | " \n", 87 | "// Q_t is the variance of the prediction of\n", 88 | "// observations and hence \\sqrt{Q_t} is the\n", 89 | "// standard deviation of the predictions\n", 90 | " decimal[] FR = new decimal[2];\n", 91 | " FR[0] = F[0] * R[0,0] + F[0] * R[0,1];\n", 92 | " FR[1] = F[1] * R[1,0] + F[1] * R[1,1];\n", 93 | " \n", 94 | " decimal Qt = FR[0] * F[0] + FR[1] * F[1] + vt;\n", 95 | " decimal sqrtQt = (decimal)Math.Sqrt(Convert.ToDouble(Qt));\n", 96 | "\n", 97 | " \n", 98 | "// The posterior value of the states \\theta_t is\n", 99 | "// distributed as a multivariate Gaussian with mean\n", 100 | "// m_t and variance-covariance C_t\n", 101 | " decimal[] At = new decimal[2] {FR[0] / Qt, FR[1] / Qt};\n", 102 | " theta[0] = theta[0] + At[0] * et;\n", 103 | " theta[1] = theta[1] + At[1] * et;\n", 104 | " C[0,0] = R[0,0] - At[0] * FR[0];\n", 105 | " C[0,1] = R[0,1] - At[0] * FR[1];\n", 106 | " C[1,0] = R[1,0] - At[1] * FR[0];\n", 107 | " C[1,1] = R[1,1] - At[1] * FR[1];\n", 108 | " decimal hedgeQuantity = Math.Floor(qty * theta[0]);\n", 109 | " \n", 110 | " return new List() {et, sqrtQt, hedgeQuantity}; \n", 111 | " } \n", 112 | " \n", 113 | " \n", 114 | "\n", 115 | "}" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "Now, we initialize the Kalman Filter, grab our data, and then run the Kalman Filter update process over the data.\n", 123 | "\n", 124 | "In an algorithm, the kf.qty variable is the number of shares to invested in VIAB, and hedge_quantity is the amount to trade in the opposite direction for VIA" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 9, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "var kf = new KalmanFilter();\n", 134 | "\n", 135 | "\n", 136 | "var history = qb.History(qb.Securities.Keys, new DateTime(2019, 1, 1), new DateTime(2019, 1, 11), Resolution.Daily);\n", 137 | "var prices = new Dictionary>();\n", 138 | "\n", 139 | "foreach(var slice in history){\n", 140 | " \n", 141 | " foreach(var symbol in slice.Keys){ \n", 142 | " if(!prices.ContainsKey(symbol)){\n", 143 | " prices.Add(symbol, new List());\n", 144 | " }\n", 145 | " prices[symbol].Add(slice.Bars[symbol].Close);\n", 146 | " }\n", 147 | "}\n", 148 | "\n", 149 | "for(var k=0; k < prices[via].Count(); k++){ \n", 150 | " var via_price = prices[via][k];\n", 151 | " var viab_price = prices[viab][k];\n", 152 | " var result = kf.Update(via_price, viab_price);\n", 153 | " Console.WriteLine(result[0] + \" : \" + result[1] + \" : \" + result[2]);\n", 154 | "}" 155 | ] 156 | } 157 | ], 158 | "metadata": { 159 | "kernelspec": { 160 | "display_name": "C#", 161 | "language": "csharp", 162 | "name": "csharp" 163 | }, 164 | "language_info": { 165 | "file_extension": ".cs", 166 | "mimetype": "text/x-csharp", 167 | "name": "C#", 168 | "pygments_lexer": "c#", 169 | "version": "4.0.30319" 170 | }, 171 | "pycharm": { 172 | "stem_cell": { 173 | "cell_type": "raw", 174 | "metadata": { 175 | "collapsed": false 176 | }, 177 | "source": [] 178 | } 179 | } 180 | }, 181 | "nbformat": 4, 182 | "nbformat_minor": 2 183 | } 184 | -------------------------------------------------------------------------------- /Research2Production/CSharp/05 Stationary Processes and Z-Scores CSharp.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "// QuantBook C# Research Environment\n", 18 | "// For more information see https://www.quantconnect.com/docs/research/overview\n", 19 | "#load \"../QuantConnect.csx\" \n", 20 | "using QuantConnect.Data.UniverseSelection;\n", 21 | "\n", 22 | "var qb = new QuantBook();\n", 23 | "var symbols = LiquidETFUniverse.SP500Sectors;\n", 24 | "\n", 25 | "var history = qb.History(symbols, 360, Resolution.Daily);" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 2, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "// Calculates the % returns of our symbols over the historical data period\n", 35 | "var returns = new Dictionary>();\n", 36 | "var last = new Dictionary();\n", 37 | "foreach(var slice in history){\n", 38 | " foreach(var symbol in slice.Bars.Keys){\n", 39 | " if(!returns.ContainsKey(symbol)){\n", 40 | " returns.Add(symbol, new List());\n", 41 | " last.Add(symbol, (decimal)slice.Bars[symbol].Close);\n", 42 | " }\n", 43 | " var change = (decimal) ((decimal)slice.Bars[symbol].Close - last[symbol])/last[symbol];\n", 44 | " returns[symbol].Add(change);\n", 45 | " } \n", 46 | "}\n", 47 | "\n", 48 | "\n", 49 | "\n", 50 | "var means = new Dictionary();\n", 51 | "\n", 52 | "// Calculate mean price over 30 day period for each symbol\n", 53 | "foreach(var symbol in returns.Keys){\n", 54 | " if(!means.ContainsKey(symbol)){\n", 55 | " var avg = returns[symbol].Average();\n", 56 | " means.Add(symbol, avg);\n", 57 | " }\n", 58 | "}\n", 59 | "\n", 60 | "var std = new Dictionary();\n", 61 | "\n", 62 | "// Calculate standard deviation of prices for each symbol\n", 63 | "foreach(var symbol in returns.Keys){\n", 64 | " if(!std.ContainsKey(symbol)){\n", 65 | " var average = means[symbol];\n", 66 | " var sumOfSquaresOfDifferences = returns[symbol].Select(val => (val - average) * (val - average)).Sum();\n", 67 | " var sd = (Decimal)Math.Sqrt((double) (sumOfSquaresOfDifferences / returns[symbol].Count()));\n", 68 | "\n", 69 | " std.Add(symbol, sd);\n", 70 | " }\n", 71 | "}\n" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 3, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "var ZScores = new Dictionary>();\n", 81 | "\n", 82 | "// Calculate the Z-Score for each return in the time series\n", 83 | "foreach(var symbol in returns.Keys){\n", 84 | " \n", 85 | " if(!ZScores.ContainsKey(symbol)){\n", 86 | " ZScores.Add(symbol, new List());\n", 87 | " }\n", 88 | " \n", 89 | " foreach(var ret in returns[symbol]){\n", 90 | " var score = (decimal)((ret - means[symbol])/std[symbol]);\n", 91 | " ZScores[symbol].Add(score); \n", 92 | " }\n", 93 | "}\n", 94 | "\n", 95 | "foreach(var symbol in ZScores.Keys){\n", 96 | " Console.WriteLine(symbol + \", \" + ZScores[symbol].Count);\n", 97 | "}" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [] 106 | } 107 | ], 108 | "metadata": { 109 | "kernelspec": { 110 | "display_name": "C#", 111 | "language": "csharp", 112 | "name": "csharp" 113 | }, 114 | "language_info": { 115 | "file_extension": ".cs", 116 | "mimetype": "text/x-csharp", 117 | "name": "C#", 118 | "pygments_lexer": "c#", 119 | "version": "4.0.30319" 120 | }, 121 | "pycharm": { 122 | "stem_cell": { 123 | "cell_type": "raw", 124 | "metadata": { 125 | "collapsed": false 126 | }, 127 | "source": [] 128 | } 129 | } 130 | }, 131 | "nbformat": 4, 132 | "nbformat_minor": 2 133 | } 134 | -------------------------------------------------------------------------------- /Research2Production/Python/01 Mean Reversion.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Mean Reversion\n", 16 | "A number of financial signals can be thought to be mean-reverting (i.e. the spread between two assets prices). There are a number of ways to test for mean-reversion, but in this example we will assume it holds.\n", 17 | "\n", 18 | "We use the research notebook to calculate the spread between an asset's price and its historical mean (past 30 days). Then, we calculate the standard deviation and determine which assets have moved more than one standard deviation below their average price. The theory is that they are due to revert back towards the mean, and so we can take advantage of this by taking long positions." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "outputs": [ 26 | { 27 | "name": "stdout", 28 | "output_type": "stream", 29 | "text": [ 30 | "Symbol: BIL TT1EBZ21QWKL, Magnitude: 0.013359453149532978\n", 31 | "Symbol: EDV TYCF240SL9PH, Magnitude: 3.3265208673173503\n", 32 | "Symbol: GOVT V45XL2BVKU3P, Magnitude: 0.11821631840615553\n", 33 | "Symbol: SHV TP8J6Z7L419H, Magnitude: 0.03615110356200481\n", 34 | "Symbol: TLO TT1EBZ21QWKL, Magnitude: 0.6459442217433491\n", 35 | "Symbol: TLT SGNKIKYGE9NP, Magnitude: 2.3672705126087825\n", 36 | "Symbol: TMF UBTUG7D0B7TX, Magnitude: 1.7547680856690884\n", 37 | "Symbol: VGLT UHVG8V7B7YAT, Magnitude: 1.3637322489563317\n" 38 | ] 39 | } 40 | ], 41 | "source": [ 42 | "# QuantBook Analysis Tool \n", 43 | "# For more information see [https://www.quantconnect.com/docs/research/overview]\n", 44 | "import numpy as np\n", 45 | "qb = QuantBook()\n", 46 | "\n", 47 | "symbols = {}\n", 48 | "assets = [\"SHY\", \"TLT\", \"SHV\", \"TLH\", \"EDV\", \"BIL\",\n", 49 | " \"SPTL\", \"TBT\", \"TMF\", \"TMV\", \"TBF\", \"VGSH\", \"VGIT\",\n", 50 | " \"VGLT\", \"SCHO\", \"SCHR\", \"SPTS\", \"GOVT\"]\n", 51 | "\n", 52 | "for i in range(len(assets)):\n", 53 | " symbols[assets[i]] = qb.AddEquity(assets[i],Resolution.Minute).Symbol\n", 54 | "\n", 55 | "# Fetch history on our universe\n", 56 | "df = qb.History(qb.Securities.Keys, 30, Resolution.Daily)\n", 57 | "# Make all of them into a single time index.\n", 58 | "df = df.close.unstack(level=0)\n", 59 | "# Calculate the truth value of the most recent price being less than 1 std away from the mean\n", 60 | "classifier = df.le(df.mean().subtract(df.std())).tail(1)\n", 61 | "# Get indexes of the True values\n", 62 | "classifier_indexes = np.where(classifier)[1]\n", 63 | "# Get the Symbols for the True values\n", 64 | "classifier = classifier.transpose().iloc[classifier_indexes].index.values\n", 65 | "# Get the std values for the True values (used for Insight magnitude)\n", 66 | "magnitude = df.std().transpose()[classifier_indexes].values\n", 67 | "# Zip together to iterate over later\n", 68 | "selected = zip(classifier, magnitude)\n", 69 | "\n", 70 | "for x, y in selected:\n", 71 | " print(f'Symbol: {x}, Magnitude: {y}')" 72 | ] 73 | } 74 | ], 75 | "metadata": { 76 | "kernelspec": { 77 | "display_name": "Python 3", 78 | "language": "python", 79 | "name": "python3" 80 | }, 81 | "language_info": { 82 | "codemirror_mode": { 83 | "name": "ipython", 84 | "version": 3 85 | }, 86 | "file_extension": ".py", 87 | "mimetype": "text/x-python", 88 | "name": "python", 89 | "nbconvert_exporter": "python", 90 | "pygments_lexer": "ipython3", 91 | "version": "3.6.8" 92 | } 93 | }, 94 | "nbformat": 4, 95 | "nbformat_minor": 2 96 | } 97 | -------------------------------------------------------------------------------- /Research2Production/Python/02 Random Forest Regression.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Random Forest Regression\n", 16 | "\n", 17 | "For another installment of our \"mini-series\" of examples on how to move your work from the research environment and into production, we've shown how you can implement a basic random forest regression model using the sklearn RandomForestRegressor. Briefly, random forests is a supervised learning algorithm that we here use specifically for regression in order to identify important features of our dataset and create weights to build a tradeable portfolio. \n", 18 | "\n", 19 | "To start, we continue to use the US Treasuries ETF basket and get the historical data we want. We'll use the most recent 1000 hours of historical data to create our train / test data sets." 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 1, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "# QuantBook Analysis Tool \n", 29 | "# For more information see [https://www.quantconnect.com/docs/research/overview]\n", 30 | "from sklearn.ensemble import RandomForestRegressor\n", 31 | "from sklearn.model_selection import train_test_split\n", 32 | "qb = QuantBook()\n", 33 | "qb\n", 34 | "\n", 35 | "symbols = {}\n", 36 | "assets = [\"SHY\", \"TLT\", \"SHV\", \"TLH\", \"EDV\", \"BIL\",\n", 37 | " \"SPTL\", \"TBT\", \"TMF\", \"TMV\", \"TBF\", \"VGSH\", \"VGIT\",\n", 38 | " \"VGLT\", \"SCHO\", \"SCHR\", \"SPTS\", \"GOVT\"]\n", 39 | "\n", 40 | "for i in range(len(assets)):\n", 41 | " symbols[assets[i]] = qb.AddEquity(assets[i],Resolution.Minute).Symbol\n", 42 | "\n", 43 | "#Copy Paste Region For Backtesting.\n", 44 | "#==========================================\n", 45 | "# Set up classifier\n", 46 | "# Initialize instance of Random Forest Regressor\n", 47 | "regressor = RandomForestRegressor(n_estimators=100, min_samples_split=5, random_state = 1990)\n", 48 | "\n", 49 | "# Fetch history on our universe\n", 50 | "df = qb.History(qb.Securities.Keys, 500, Resolution.Hour)\n", 51 | "\n", 52 | "# Get train/test data\n", 53 | "returns = df.unstack(level=1).close.transpose().pct_change().dropna()\n", 54 | "X = returns\n", 55 | "# use real portfolio value in algo: y = [x for x in qb.portfolioValue][-X.shape[0]:]\n", 56 | "y = np.random.normal(100000, 5, X.shape[0])\n", 57 | "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 1990)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "Once we have our data and have initialized our regressor, we fit the model and then can determine the importance of each feature, which in this case are the different symbols.\n", 65 | "\n", 66 | "Our final variable selected is a zip of symbol-weight tuples to be used in building our portfolio." 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 2, 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "name": "stdout", 76 | "output_type": "stream", 77 | "text": [ 78 | "Symbol: BIL TT1EBZ21QWKL, Weight: 0.06860311608980395\n", 79 | "Symbol: EDV TYCF240SL9PH, Weight: 0.06236283531890688\n", 80 | "Symbol: GOVT V45XL2BVKU3P, Weight: 0.04368632766949955\n", 81 | "Symbol: SCHO UOVIOSUIT3DX, Weight: 0.061877890201449244\n", 82 | "Symbol: SCHR UOVIOSUIT3DX, Weight: 0.07772532787888427\n", 83 | "Symbol: SHV TP8J6Z7L419H, Weight: 0.06571655997142939\n", 84 | "Symbol: SHY SGNKIKYGE9NP, Weight: 0.0637328714858151\n", 85 | "Symbol: SST V2245V5VOQQT, Weight: 0.09435425484418379\n", 86 | "Symbol: TBF UF9WRZG9YA1X, Weight: 0.043968112419911415\n", 87 | "Symbol: TBT U297ZHBXJ5NP, Weight: 0.035527396871039806\n", 88 | "Symbol: TLH TP8J6Z7L419H, Weight: 0.07438852724550572\n", 89 | "Symbol: TLO TT1EBZ21QWKL, Weight: 0.04383431314645853\n", 90 | "Symbol: TLT SGNKIKYGE9NP, Weight: 0.02775756426489062\n", 91 | "Symbol: TMF UBTUG7D0B7TX, Weight: 0.029645002255879502\n", 92 | "Symbol: TMV UBTUG7D0B7TX, Weight: 0.03533909176068473\n", 93 | "Symbol: VGIT UHVG8V7B7YAT, Weight: 0.06269298525092254\n", 94 | "Symbol: VGLT UHVG8V7B7YAT, Weight: 0.03941532098553043\n", 95 | "Symbol: VGSH UHVG8V7B7YAT, Weight: 0.06937250233920446\n" 96 | ] 97 | } 98 | ], 99 | "source": [ 100 | "\n", 101 | "# Fit regressor\n", 102 | "regressor.fit(X_train, y_train)\n", 103 | "\n", 104 | "# Get long-only predictions\n", 105 | "weights = regressor.feature_importances_\n", 106 | "symbols = returns.columns[np.where(weights)]\n", 107 | "selected = zip(symbols, weights)\n", 108 | "for x, y in selected:\n", 109 | " print(f'Symbol: {x}, Weight: {y}')" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": null, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [] 118 | } 119 | ], 120 | "metadata": { 121 | "kernelspec": { 122 | "display_name": "Python 3", 123 | "language": "python", 124 | "name": "python3" 125 | }, 126 | "language_info": { 127 | "codemirror_mode": { 128 | "name": "ipython", 129 | "version": 3 130 | }, 131 | "file_extension": ".py", 132 | "mimetype": "text/x-python", 133 | "name": "python", 134 | "nbconvert_exporter": "python", 135 | "pygments_lexer": "ipython3", 136 | "version": "3.6.8" 137 | } 138 | }, 139 | "nbformat": 4, 140 | "nbformat_minor": 2 141 | } 142 | -------------------------------------------------------------------------------- /Research2Production/Python/03 Uncorrelated Assets.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Uncorrelated Assets\n", 16 | "Finding uncorrelated assets allows you to find a portfolio that will, theoretically, be more diversified and resilient to extreme market events. When combined with other indicators and data sources, this can be an important component in building an algorithm that limits drawdown and remains profitable in choppy markets.\n", 17 | "\n", 18 | "The first step is to execute the cell containing our correlation function and then grab the historical data for the securities and use this to fetch returns data." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# This cell is the function we use to get correlation ranks\n", 28 | "def GetUncorrelatedAssets(returns, num_assets):\n", 29 | " # Get correlation\n", 30 | " correlation = returns.corr()\n", 31 | " \n", 32 | " # Find assets with lowest mean correlation, scaled by STD\n", 33 | " selected = []\n", 34 | " for index, row in correlation.iteritems():\n", 35 | " corr_rank = row.abs().mean()/row.abs().std()\n", 36 | " selected.append((index, corr_rank))\n", 37 | "\n", 38 | " # Sort and take the top num_assets\n", 39 | " selected = sorted(selected, key = lambda x: x[1])[:num_assets]\n", 40 | " \n", 41 | " return selected" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "# Import custom functions\n", 51 | "qb = QuantBook()\n", 52 | "\n", 53 | "tickers = [\"SQQQ\", \"TQQQ\", \"TVIX\", \"VIXY\", \"SPLV\",\n", 54 | " \"SVXY\", \"UVXY\", \"EEMV\", \"EFAV\", \"USMV\"]\n", 55 | "symbols = [qb.AddEquity(x, Resolution.Minute) for x in tickers]\n", 56 | "\n", 57 | "# Fetch history\n", 58 | "history = qb.History(qb.Securities.Keys, 150, Resolution.Hour)\n", 59 | "\n", 60 | "# Get hourly returns\n", 61 | "returns = history.unstack(level = 1).close.transpose().pct_change().dropna()" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "Then, we calculate the correlation of the returns, which gives us a correlation matrix. In the GetUncorrelatedAssets function, we figure out which symbols have the lowest overall correlation with the rest of the symbols as a whole -- we want to find the five assets with the lowest average absolute correlation so that we can trade them without fearing that any pair are too highly correlated." 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 3, 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "data": { 78 | "text/plain": [ 79 | "[('VIXY UT076X30D0MD', 6.908714329573668),\n", 80 | " ('UVXY V0H08FY38ZFP', 6.976330405968959),\n", 81 | " ('SVXY V0H08FY38ZFP', 7.041772632696755),\n", 82 | " ('SPLV UWBCA8CFMB1H', 7.109323797061953),\n", 83 | " ('TVIX US1QJNA3OM05', 7.113355127827484)]" 84 | ] 85 | }, 86 | "execution_count": 3, 87 | "metadata": {}, 88 | "output_type": "execute_result" 89 | } 90 | ], 91 | "source": [ 92 | "# Get 5 assets with least overall correlation\n", 93 | "selected = GetUncorrelatedAssets(returns, 5)\n", 94 | "selected" 95 | ] 96 | } 97 | ], 98 | "metadata": { 99 | "kernelspec": { 100 | "display_name": "Python 3", 101 | "language": "python", 102 | "name": "python3" 103 | }, 104 | "language_info": { 105 | "codemirror_mode": { 106 | "name": "ipython", 107 | "version": 3 108 | }, 109 | "file_extension": ".py", 110 | "mimetype": "text/x-python", 111 | "name": "python", 112 | "nbconvert_exporter": "python", 113 | "pygments_lexer": "ipython3", 114 | "version": "3.6.8" 115 | } 116 | }, 117 | "nbformat": 4, 118 | "nbformat_minor": 2 119 | } -------------------------------------------------------------------------------- /Research2Production/Python/04 Kalman Filters and Pairs Trading.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Kalman Filters and Pairs Trading\n", 16 | "\n", 17 | "There are a few Python packages out there for Kalman filters, but we're adapting this example and the Kalman filter class code from [this article](https://www.quantstart.com/articles/kalman-filter-based-pairs-trading-strategy-in-qstrader) and demonstrating how you can implement similar ideas using QuantConnect!\n", 18 | "\n", 19 | "Briefly, a Kalman filter is a [state-space model](https://en.wikipedia.org/wiki/State-space_representation) applicable to linear dynamic systems -- systems whose state is time-dependent and state variations are represented linearly. The model is used to estimate unknown states of a variable based on a series of past values. The procedure is two-fold: a prediction (estimate) is made by the filter of the current state of a variable and the uncertainty of the estimate itself. When new data is available, these estimates are updated. There is a lot of information available about Kalman filters, and the variety of their applications is pretty astounding, but for now, we're going to use a Kalman filter to estimate the hedge ratio between a pair of equities.\n", 20 | "\n", 21 | "The idea behind the strategy is pretty straightforward: take two equities that are cointegrated and create a long-short portfolio. The premise of this is that the spread between the value of our two positions should be mean-reverting. Anytime the spread deviates from its expected value, one of the assets moved in an unexpected direction and is due to revert back. When the spread diverges, you can take advantage of this by going long or short on the spread.\n", 22 | "\n", 23 | "To illustrate, imagine you have a long position in AAPL worth \\\\$2000 and a short position in IBM worth \\\\$2000. This gives you a net spread of \\\\$0. Since you expected AAPL and IBM to move together, then if the spread increases significantly above \\\\$0, you would short the spread in the expectation that it will return to \\\\$0, it's natural equilibrium. Similarly, if the value drops significantly below \\\\$0, you would long the spread and capture the profits as its value returns to \\\\$0. In our application, the Kalman filter will be used to track the hedging ratio between our equities to ensure that the portfolio value is stationary, which means it will continue to exhibit mean-reversion behavior." 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "##### Note: Run the final cell first so the remaining cells will execute" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 2, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "# QuantBook Analysis Tool \n", 40 | "# For more information see [https://www.quantconnect.com/docs/research/overview]\n", 41 | "import numpy as np\n", 42 | "from math import floor\n", 43 | "import matplotlib.pyplot as plt\n", 44 | "from KalmanFilter import KalmanFilter\n", 45 | "\n", 46 | "qb = QuantBook()\n", 47 | "symbols = [qb.AddEquity(x).Symbol for x in ['VIA', 'VIAB']]" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "Now, we initialize the Kalman Filter, grab our data, and then run the Kalman Filter update process over the data." 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 3, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "[26.19257561] :: [[0.03162278]] :: 0\n", 67 | "[25.84020912] :: [[0.28741791]] :: 1786\n", 68 | "[0.13404521] :: [[0.29639079]] :: 1795\n", 69 | "[-0.42714423] :: [[0.31514705]] :: 1768\n", 70 | "[0.24662073] :: [[0.31807877]] :: 1783\n", 71 | "[0.38152379] :: [[0.31687043]] :: 1807\n", 72 | "[0.12279125] :: [[0.31913823]] :: 1815\n" 73 | ] 74 | } 75 | ], 76 | "source": [ 77 | "kf = KalmanFilter()\n", 78 | "history = qb.History(qb.Securities.Keys, datetime(2019, 1, 1), datetime(2019, 1, 11), Resolution.Daily)\n", 79 | "prices = history.unstack(level=1).close.transpose()\n", 80 | "for index, row in prices.iterrows():\n", 81 | " via = row.loc[str(symbols[0].ID)]\n", 82 | " viab = row.loc[str(symbols[1].ID)]\n", 83 | " forecast_error, prediction_std_dev, hedge_quantity = kf.update(via, viab)\n", 84 | " print(f'{forecast_error} :: {prediction_std_dev} :: {hedge_quantity}')" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": {}, 90 | "source": [ 91 | "In an algorithm, the kf.qty variable is the number of shares to invested in VIAB, and hedge_quantity is the amount to trade in the opposite direction for VIA" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "##### Code for the Kalman Filter" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": 1, 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "import numpy as np\n", 108 | "from math import floor\n", 109 | "\n", 110 | "class KalmanFilter:\n", 111 | " def __init__(self):\n", 112 | " self.delta = 1e-4\n", 113 | " self.wt = self.delta / (1 - self.delta) * np.eye(2)\n", 114 | " self.vt = 1e-3\n", 115 | " self.theta = np.zeros(2)\n", 116 | " self.P = np.zeros((2, 2))\n", 117 | " self.R = None\n", 118 | " self.qty = 2000\n", 119 | "\n", 120 | " def update(self, price_one, price_two):\n", 121 | " # Create the observation matrix of the latest prices\n", 122 | " # of TLT and the intercept value (1.0)\n", 123 | " F = np.asarray([price_one, 1.0]).reshape((1, 2))\n", 124 | " y = price_two\n", 125 | "\n", 126 | " # The prior value of the states \\theta_t is\n", 127 | " # distributed as a multivariate Gaussian with\n", 128 | " # mean a_t and variance-covariance R_t\n", 129 | " if self.R is not None:\n", 130 | " self.R = self.C + self.wt\n", 131 | " else:\n", 132 | " self.R = np.zeros((2, 2))\n", 133 | "\n", 134 | " # Calculate the Kalman Filter update\n", 135 | " # ----------------------------------\n", 136 | " # Calculate prediction of new observation\n", 137 | " # as well as forecast error of that prediction\n", 138 | " yhat = F.dot(self.theta)\n", 139 | " et = y - yhat\n", 140 | "\n", 141 | " # Q_t is the variance of the prediction of\n", 142 | " # observations and hence \\sqrt{Q_t} is the\n", 143 | " # standard deviation of the predictions\n", 144 | " Qt = F.dot(self.R).dot(F.T) + self.vt\n", 145 | " sqrt_Qt = np.sqrt(Qt)\n", 146 | "\n", 147 | " # The posterior value of the states \\theta_t is\n", 148 | " # distributed as a multivariate Gaussian with mean\n", 149 | " # m_t and variance-covariance C_t\n", 150 | " At = self.R.dot(F.T) / Qt\n", 151 | " self.theta = self.theta + At.flatten() * et\n", 152 | " self.C = self.R - At * F.dot(self.R)\n", 153 | " hedge_quantity = int(floor(self.qty*self.theta[0]))\n", 154 | " \n", 155 | " return et, sqrt_Qt, hedge_quantity" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [] 164 | } 165 | ], 166 | "metadata": { 167 | "kernelspec": { 168 | "display_name": "Python 3", 169 | "language": "python", 170 | "name": "python3" 171 | }, 172 | "language_info": { 173 | "codemirror_mode": { 174 | "name": "ipython", 175 | "version": 3 176 | }, 177 | "file_extension": ".py", 178 | "mimetype": "text/x-python", 179 | "name": "python", 180 | "nbconvert_exporter": "python", 181 | "pygments_lexer": "ipython3", 182 | "version": "3.6.8" 183 | } 184 | }, 185 | "nbformat": 4, 186 | "nbformat_minor": 2 187 | } 188 | -------------------------------------------------------------------------------- /Research2Production/Python/06 Principle Compenent Analysis.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "### Principle Component Analysis\n", 16 | "Briefly stated, collinearity is the state of two independent variables being highly correlated. Unfortunately, collinearity can cause trouble when using regression models. One of the fundamental assumptions of Ordinary Least Squares regression is that the variables can't have a linear relationship. This problem is especially tricky when dealing with multiple variables in regression models where multicollinearity occurs. The presence of multicollinearity is a problem because the linear regression model cannot distinguish between the co-lineated variables. It may cause some variables to appear to be insignificant in the relationship when they really are - in other words, their \"weight\" is transferred to another, correlated value.\n", 17 | "\n", 18 | "Principal Component Analysis (PCA) a way of mapping the existing dataset into a new \"space\", where the dimensions of the new data are linearly-independent, orthogonal vectors. PCA eliminates the problem of multicollinearity. In addition to this, PCA gives us a way of identifying the dimensions of the data that contribute most to the variance of the data, meaning we can also use PCA to reduce the number of input variables in our regression model. We can use the PCA-transformed data to build a regression model, make predictions, and then map those predictions back into the original data \"space\" so that we have applicable predictions." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "import numpy as np\n", 28 | "import statsmodels.api as sm\n", 29 | "from sklearn.decomposition import PCA\n", 30 | "from sklearn.ensemble import RandomForestRegressor\n", 31 | "from sklearn.linear_model import Ridge, LinearRegression, LogisticRegression, HuberRegressor, Lasso\n", 32 | "from sklearn.model_selection import cross_val_score, GridSearchCV\n", 33 | "\n", 34 | "# Import the Liquid ETF Universe helper methods\n", 35 | "from QuantConnect.Data.UniverseSelection import *\n", 36 | "from QuantConnect.Data.Custom.USTreasury import *\n", 37 | "\n", 38 | "# Initialize QuantBook and the US Treasuries ETFs\n", 39 | "qb = QuantBook()\n", 40 | "yieldCurve = qb.AddData(USTreasuryYieldCurveRate, \"USTYCR\", Resolution.Daily).Symbol" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": {}, 47 | "outputs": [ 48 | { 49 | "data": { 50 | "text/html": [ 51 | "
\n", 52 | "\n", 65 | "\n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | "
fiveyearonemonthoneyearsevenyearsixmonthtenyearthirtyyearthreemonththreeyeartwentyyeartwomonthtwoyear
time
2020-01-280.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.000000
2020-01-29-0.040816-0.006536-0.013072-0.032051-0.006329-0.030303-0.023810-0.006369-0.041379-0.030769-0.012739-0.020690
2020-01-30-0.0141840.046053-0.019868-0.0132450.000000-0.018750-0.0048780.006410-0.014388-0.0052910.019355-0.007042
2020-01-31-0.050360-0.018868-0.020270-0.046980-0.019108-0.038217-0.024510-0.012739-0.051095-0.026596-0.006329-0.056738
2020-02-030.0227270.0000000.0068970.0211270.0129870.0198680.0100500.0129030.0307690.0054640.0000000.022556
\n", 176 | "
" 177 | ], 178 | "text/plain": [ 179 | " fiveyear onemonth oneyear sevenyear sixmonth tenyear \\\n", 180 | "time \n", 181 | "2020-01-28 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 \n", 182 | "2020-01-29 -0.040816 -0.006536 -0.013072 -0.032051 -0.006329 -0.030303 \n", 183 | "2020-01-30 -0.014184 0.046053 -0.019868 -0.013245 0.000000 -0.018750 \n", 184 | "2020-01-31 -0.050360 -0.018868 -0.020270 -0.046980 -0.019108 -0.038217 \n", 185 | "2020-02-03 0.022727 0.000000 0.006897 0.021127 0.012987 0.019868 \n", 186 | "\n", 187 | " thirtyyear threemonth threeyear twentyyear twomonth twoyear \n", 188 | "time \n", 189 | "2020-01-28 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 \n", 190 | "2020-01-29 -0.023810 -0.006369 -0.041379 -0.030769 -0.012739 -0.020690 \n", 191 | "2020-01-30 -0.004878 0.006410 -0.014388 -0.005291 0.019355 -0.007042 \n", 192 | "2020-01-31 -0.024510 -0.012739 -0.051095 -0.026596 -0.006329 -0.056738 \n", 193 | "2020-02-03 0.010050 0.012903 0.030769 0.005464 0.000000 0.022556 " 194 | ] 195 | }, 196 | "execution_count": 2, 197 | "metadata": {}, 198 | "output_type": "execute_result" 199 | } 200 | ], 201 | "source": [ 202 | "# Get history\n", 203 | "history = qb.History(yieldCurve, 100, Resolution.Daily)\n", 204 | "# Get prices and returns\n", 205 | "bonds = history.loc[yieldCurve].pct_change().ffill().fillna(value = 0).replace([np.inf, -np.inf], np.nan).dropna()\n", 206 | "bonds.head()" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": 3, 212 | "metadata": {}, 213 | "outputs": [], 214 | "source": [ 215 | "# Structure data for modeling - train for building the models, testing to make predictions with\n", 216 | "training = bonds.iloc[:len(bonds)-1].copy()\n", 217 | "testing = bonds.iloc[len(bonds)-1:].copy()" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 4, 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "# The implementation of the function in RegressionFunction.py but with some added plotting features\n", 227 | "def FitRegressionModel(pca, model, training, testing, alpha = False): \n", 228 | " estimators = []\n", 229 | " Y_pred = []\n", 230 | " Y_actual = []\n", 231 | " X_training_proj = pca.transform(training)\n", 232 | "\n", 233 | " # Iterate over all principle components:\n", 234 | " for i in range(pca.n_components_):\n", 235 | "\n", 236 | " # Here, we lag X compared to Y. X will be one time period behind Y\n", 237 | " X = X_training_proj[:-1, i]\n", 238 | " Y = X_training_proj[1:, i]\n", 239 | " \n", 240 | " Y_actual.append(Y)\n", 241 | " X = sm.add_constant(X)\n", 242 | " \n", 243 | " if alpha:\n", 244 | " test_range = np.arange(5)\n", 245 | " param_grid = {\"alpha\": test_range}\n", 246 | " grid_search = GridSearchCV(model,param_grid)\n", 247 | " grid_search.fit(X, Y)\n", 248 | " best_params = grid_search.best_params_\n", 249 | " \n", 250 | " est = model.fit(X, Y)\n", 251 | " estimators.append(est)\n", 252 | " Y_pred.append(model.predict(X))\n", 253 | " print(\"Estimator {}: R2 = {:.3f}\\n\".format(i, model.score(X,Y)))\n", 254 | " \n", 255 | " Y_pred = np.array(Y_pred).transpose()\n", 256 | " Y_actual = np.array(Y_actual).transpose()\n", 257 | " \n", 258 | " Y_actual_original_space = pca.inverse_transform(Y_actual)\n", 259 | " Y_pred_original_space = pca.inverse_transform(Y_pred)\n", 260 | "\n", 261 | " # Compute sum of squared error:\n", 262 | " train_sse = np.sum((Y_pred_original_space - Y_actual_original_space)** 2)\n", 263 | " print(f'Sum of squared error: {train_sse}\\n')\n", 264 | " testing_proj = pca.transform(testing)\n", 265 | " \n", 266 | " testing_prediction = []\n", 267 | " for i in range(pca.n_components_):\n", 268 | " # Create a data row - remember our estimators have a constant, so we need that\n", 269 | " row = [1, testing_proj[:,i]]\n", 270 | " print(row)\n", 271 | " row = np.reshape(row, (1, 2))\n", 272 | " # Predict this row\n", 273 | " p = model.predict(row)\n", 274 | " # Transofrm the (1, 1) result into just a (1,), and append to our predictions\n", 275 | "\n", 276 | " # Potential error here:\n", 277 | " testing_prediction.append(p[0]) # If this errors, try p[0][0]\n", 278 | " \n", 279 | " predictions = pca.inverse_transform(testing_prediction)\n", 280 | " pred_sse = np.sum((predictions - testing.values)** 2)\n", 281 | " actual_pred = {'Predicted':predictions, 'Actual':[x[0] for x in testing.transpose().values]}\n", 282 | " actual_pred = pd.DataFrame(actual_pred, index = testing.columns)\n", 283 | " print(actual_pred)\n", 284 | " print(f'\\nPrediction sum of squared error: {pred_sse}\\n')\n", 285 | " return actual_pred['Predicted']" 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "metadata": {}, 291 | "source": [ 292 | "Instead of telling the PCA model how many components we wanted to keep, we decided instead to have it return the number of components it took to explain 95% of the variance of the data, which in the case of treasury yield changes was 4." 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 5, 298 | "metadata": {}, 299 | "outputs": [ 300 | { 301 | "name": "stdout", 302 | "output_type": "stream", 303 | "text": [ 304 | "PCA Explained Variance: [0.76330253 0.08974696 0.0675421 0.04663491]\n", 305 | "\n", 306 | "PCA No. Components: 4\n", 307 | "\n" 308 | ] 309 | } 310 | ], 311 | "source": [ 312 | "# Initialize the PCA model\n", 313 | "pca = PCA(n_components=0.95) # Forces it to explain >99% of variance\n", 314 | "# Fit the PCA model\n", 315 | "pca.fit(training)\n", 316 | "print(f'PCA Explained Variance: {pca.explained_variance_ratio_}\\n')\n", 317 | "print(f'PCA No. Components: {pca.n_components_}\\n')" 318 | ] 319 | }, 320 | { 321 | "cell_type": "markdown", 322 | "metadata": {}, 323 | "source": [ 324 | "We fit two different regression models -- Linean and Random Forest" 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": 6, 330 | "metadata": {}, 331 | "outputs": [ 332 | { 333 | "name": "stdout", 334 | "output_type": "stream", 335 | "text": [ 336 | "Estimator 0: R2 = 0.008\n", 337 | "\n", 338 | "Estimator 1: R2 = 0.043\n", 339 | "\n", 340 | "Estimator 2: R2 = 0.023\n", 341 | "\n", 342 | "Estimator 3: R2 = 0.000\n", 343 | "\n", 344 | "Sum of squared error: 67.20596541646557\n", 345 | "\n", 346 | "[1, array([-0.10059391])]\n", 347 | "[1, array([-0.15233152])]\n", 348 | "[1, array([-0.01927581])]\n", 349 | "[1, array([-0.0198384])]\n", 350 | " Predicted Actual\n", 351 | "fiveyear -0.009274 0.027778\n", 352 | "onemonth 0.008572 -0.111111\n", 353 | "oneyear -0.015760 0.000000\n", 354 | "sevenyear -0.007291 0.075472\n", 355 | "sixmonth 0.049502 0.066667\n", 356 | "tenyear -0.005246 0.090909\n", 357 | "thirtyyear -0.002515 0.068182\n", 358 | "threemonth -0.001092 -0.076923\n", 359 | "threeyear -0.014679 0.000000\n", 360 | "twentyyear -0.003784 0.084112\n", 361 | "twomonth -0.022414 -0.090909\n", 362 | "twoyear -0.019232 -0.105263\n", 363 | "\n", 364 | "Prediction sum of squared error: 0.06311788675635607\n", 365 | "\n" 366 | ] 367 | } 368 | ], 369 | "source": [ 370 | "# Initialize a standard OLS model\n", 371 | "model = LinearRegression()\n", 372 | "results = FitRegressionModel(pca, model, training, testing)" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": 7, 378 | "metadata": {}, 379 | "outputs": [ 380 | { 381 | "name": "stdout", 382 | "output_type": "stream", 383 | "text": [ 384 | "Estimator 0: R2 = 0.814\n", 385 | "\n", 386 | "Estimator 1: R2 = 0.835\n", 387 | "\n", 388 | "Estimator 2: R2 = 0.822\n", 389 | "\n", 390 | "Estimator 3: R2 = 0.816\n", 391 | "\n", 392 | "Sum of squared error: 12.488616430427138\n", 393 | "\n", 394 | "[1, array([-0.10059391])]\n", 395 | "[1, array([-0.15233152])]\n", 396 | "[1, array([-0.01927581])]\n", 397 | "[1, array([-0.0198384])]\n", 398 | " Predicted Actual\n", 399 | "fiveyear 0.075139 0.027778\n", 400 | "onemonth -0.118144 -0.111111\n", 401 | "oneyear 0.050716 0.000000\n", 402 | "sevenyear 0.058816 0.075472\n", 403 | "sixmonth 0.226376 0.066667\n", 404 | "tenyear 0.065140 0.090909\n", 405 | "thirtyyear 0.042342 0.068182\n", 406 | "threemonth -0.016886 -0.076923\n", 407 | "threeyear 0.069739 0.000000\n", 408 | "twentyyear 0.047773 0.084112\n", 409 | "twomonth -0.138771 -0.090909\n", 410 | "twoyear 0.053890 -0.105263\n", 411 | "\n", 412 | "Prediction sum of squared error: 0.06938996854271938\n", 413 | "\n" 414 | ] 415 | } 416 | ], 417 | "source": [ 418 | "# Initialize Randome Forest Regression model\n", 419 | "model = RandomForestRegressor(random_state=0, n_estimators = 100)\n", 420 | "results = FitRegressionModel(pca, model, training, testing)" 421 | ] 422 | }, 423 | { 424 | "cell_type": "markdown", 425 | "metadata": {}, 426 | "source": [ 427 | "Random Forest Regression has the highest R-squared values and the prediction SSE is approximately the same as Linear Regression, so RFR is the model we would want to use in an algorithm." 428 | ] 429 | } 430 | ], 431 | "metadata": { 432 | "kernelspec": { 433 | "display_name": "Python 3", 434 | "language": "python", 435 | "name": "python3" 436 | }, 437 | "language_info": { 438 | "codemirror_mode": { 439 | "name": "ipython", 440 | "version": 3 441 | }, 442 | "file_extension": ".py", 443 | "mimetype": "text/x-python", 444 | "name": "python", 445 | "nbconvert_exporter": "python", 446 | "pygments_lexer": "ipython3", 447 | "version": "3.6.8" 448 | } 449 | }, 450 | "nbformat": 4, 451 | "nbformat_minor": 2 452 | } -------------------------------------------------------------------------------- /Research2Production/Python/07 Hidden Markov Models.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "![QuantConnect Logo](https://cdn.quantconnect.com/web/i/icon.png)\n", 8 | "
" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "## Hidden Markov Models" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "One of the most important aspects of trading is risk management. Knowing when the market is going to move against you and acting before this happens is the best way to avoid large drawdowns and maximize your Sharpe ratio. The ultimate strategy is buy-low, sell-high. The trouble with this, however, is knowing when low is low and high is high. Hindsight is 20/20, but can we find a way to understand when markets are about to shift from bull to bear?\n", 23 | "\n", 24 | "One possibility is to use a Hidden Markov Model (HMM). These are Markov models where the system is being modeled as a Markov process but whose states are unobserved, or hidden. (Briefly, a Markov process is a stochastic process where the possibility of switching to another state depends only on the current state of the model -- it is history-independent, or memoryless). In a regular Markov model, the state is observable by the user and so the only parameters are the state transition probabilities. For example, in a two-state Markov model, the user is able to know which state the system being modeled is in, and so the only model parameters to be characterized are the probabilities of attaining each state.\n", 25 | "\n", 26 | "As with Kalman filters, there are three main aspects of interest to us:\n", 27 | "\n", 28 | "* Prediction - forecasting future values of the process\n", 29 | "* Filtering - estimating the current state of the model\n", 30 | "* Smoothing - estimating past states of the model\n", 31 | "\n", 32 | "The salient feature here is prediction as our ultimate goal is to predict the market state. To experiment with this, we used the research notebook to get historical data for SPY and fit a Gaussian, two-state Hidden Markov Model to the data. We built a few functions to build, fit, and predict from our Gaussian HMM." 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 1, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "# Import necessary libraries and get historical data\n", 42 | "import numpy as np\n", 43 | "from hmmlearn.hmm import GaussianHMM\n", 44 | "qb = QuantBook()\n", 45 | "\n", 46 | "symbol = qb.AddEquity('SPY', Resolution.Daily).Symbol\n", 47 | "\n", 48 | "# Fetch history and returns\n", 49 | "history = qb.History(symbol, 500, Resolution.Hour)\n", 50 | "returns = history.close.pct_change().dropna()" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 2, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "# Define Hidden Markov Model functions\n", 60 | "def CreateHMM(algorithm, symbol):\n", 61 | " history = algorithm.History([symbol], 900, Resolution.Daily)\n", 62 | " returns = np.array(history.loc[symbol].close.pct_change().dropna())\n", 63 | " # Reshape returns\n", 64 | " returns = np.array(returns).reshape((len(returns),1))\n", 65 | " # Initialize Gaussian Hidden Markov Model\n", 66 | " model = GaussianHMM(n_components=2, covariance_type=\"full\", n_iter=1000).fit(returns)\n", 67 | " print(model.score(returns))\n", 68 | " return model\n", 69 | " \n", 70 | "def RefitModel(algorithm, symbol, model):\n", 71 | " history = algorithm.History([symbol], 900, Resolution.Daily)\n", 72 | " returns = np.array(history.loc[symbol].close.pct_change().dropna())\n", 73 | " # Reshape returns\n", 74 | " returns = np.array(returns).reshape((len(returns),1))\n", 75 | " return model.fit(returns)\n", 76 | "\n", 77 | "def PlotStates(algorithm, symbol, model):\n", 78 | " history = algorithm.History([symbol], 900, Resolution.Daily).loc[symbol]\n", 79 | " returns = history.close.pct_change().dropna()\n", 80 | "\n", 81 | " hidden_states = model.predict(np.array(returns).reshape((len(returns),1)))\n", 82 | " hidden_states = pd.Series(hidden_states, index = returns.index)\n", 83 | " hidden_states.name = 'hidden'\n", 84 | " \n", 85 | " bull = hidden_states.loc[hidden_states.values == 0]\n", 86 | " bear = hidden_states.loc[hidden_states.values == 1]\n", 87 | " plt.figure()\n", 88 | " ax = plt.gca()\n", 89 | " ax.plot(bull.index, bull.values, \".\", linestyle='none', c = 'b', label = \"Bull Market\")\n", 90 | " ax.plot(bear.index, bear.values, \".\", linestyle='none', c = 'r', label = \"Bear Market\")\n", 91 | " plt.title('Hidden States')\n", 92 | " ax.legend()\n", 93 | " plt.show()\n", 94 | " \n", 95 | " df = history.join(hidden_states, how = 'inner')\n", 96 | " df = df[['close', 'hidden']]\n", 97 | " up = pd.Series()\n", 98 | " down = pd.Series()\n", 99 | " mid = pd.Series()\n", 100 | " for tuple in df.itertuples():\n", 101 | " if tuple.hidden == 0:\n", 102 | " x = pd.Series(tuple.close, index = [tuple.Index])\n", 103 | " up = up.append(x)\n", 104 | " else:\n", 105 | " x = pd.Series(tuple.close, index = [tuple.Index])\n", 106 | " down = down.append(x)\n", 107 | " up = up.sort_index()\n", 108 | " down = down.sort_index()\n", 109 | " plt.figure()\n", 110 | " ax = plt.gca()\n", 111 | " ax.plot(up.index, up.values, \".\", linestyle='none', c = 'b', label = \"Bull Market\")\n", 112 | " ax.plot(down.index, down.values, \".\", linestyle='none', c = 'r', label = \"Bear Market\")\n", 113 | " plt.title('SPY')\n", 114 | " ax.legend()\n", 115 | " plt.show()" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 3, 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "name": "stdout", 125 | "output_type": "stream", 126 | "text": [ 127 | "3078.595332747756\n" 128 | ] 129 | }, 130 | { 131 | "name": "stderr", 132 | "output_type": "stream", 133 | "text": [ 134 | "/opt/miniconda3/lib/python3.6/site-packages/pandas/plotting/_matplotlib/converter.py:103: FutureWarning: Using an implicitly registered datetime converter for a matplotlib plotting method. The converter was registered by pandas on import. Future versions of pandas will require you to explicitly register matplotlib converters.\n", 135 | "\n", 136 | "To register the converters:\n", 137 | "\t>>> from pandas.plotting import register_matplotlib_converters\n", 138 | "\t>>> register_matplotlib_converters()\n", 139 | " warnings.warn(msg, FutureWarning)\n" 140 | ] 141 | }, 142 | { 143 | "data": { 144 | "image/png": "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ogujd7kJE6IttIMHo7+Z3j1ZXV3Nnj17aGhooLCwkMMJB846+33/z372s9x8881H7Ya65557OOWUU3j55Zc5fPgwJSUlLdN69+4dtO3Xrx+DBw/m97//fUswuDs/+tGPmJT0VbjafD6YJyJ5qVufK6m6GlasgNtui/5mevfkpk2baGpqoqysjCFDhrBhwwY+/PBD9u3bx4oVKzrV51VXXUVNTc1Rb+D79u3j1FNPpUePHjz44INtHmguLi5m6dKlPPDAAy0HlidNmsSPf/xjGhsbAdiyZQvvv/8+ffv25d133+1UrSJyfMqrU2J0RnV1ZgNh//79VFZWAtGn8EWLFlFQUMDgwYO5+uqrGTlyJMOGDWP06NGd6r9v3758O8UPtG666SamTZvG448/zoUXXnjUVkKy3r1785vf/IZPf/rT9O7dm+uvv56dO3cyZswY3J3y8nKWLl3KyJEjKSwsZNSoUcyaNYuvf/3rnapbRI4fFu0J6npVVVWefKGejRs3cuaZZ+akHonoNRDJb2a2zt2rsjmPbr0rSUREMk/BICIiAQWDiIgEFAwiIhJQMIiISEDBICIiAQVDks6cdrszZs2aRWlpafDjs69+9auYWcrTdreltVN9t+YHP/hBh/oXkeOLgiFJtk67neqXzB//+Mf51a9+BcDhw4dZuXIlAwemvJRFh/ptj4JBRNrS/YMhi+fdTjztNsDdd9/dclrr73znOy3jr7zySsaOHcuIESNYmHDZxT59+nDLLbdw9tlnszpFfTNmzGg5DXdtbS3nnnsuhYWFne53//79TJ48mZ/+9KcAPPTQQ4wfP57KykpuvPFGmpqamDt3bsuvu6+99toMLCUR+cjJ9ulbWxsycmnPLJx3u7XTbi9btsxvuOEGP3z4sDc1Nflll13mzz33nLu77927193dP/jgAx8xYoTv2bPH3d0Bf/TRR1POZ+bMmf7444/72Wef7W+++aZff/31XltbG5y2O91+hwwZ4jt27PCJEyf6okWL3D1alpdffrkfPHjQ3d2/9KUvtUzr3bt3q89fp90WyW/kw2m381oWzrvd2mm3ly9fzvLly1vOkfTee++xdetWzj//fO69916eeuopAHbt2sXWrVspKyujoKCAae1caWvq1KksXryYF154oeXiOs060u+UKVOoqalp2QpYsWIF69atY9y4cUC0NXHyyScf07IRkeND9w6GLJ93O/G02+7OvHnzuPHGG4M2tbW1/Pa3v2X16tWUlpYyYcKEllNyl5SUUJDqUpYJpk+fzpgxY5g5cyY9Ei572NF+zz33XJ599lmuueYazAx3Z+bMmdxxxx2ZWBQichzp3scYsnze7cTTbk+aNIn77ruP9957D4DXX3+d3bt3s2/fPvr3709paSmbNm1izZo1HZrH6aefzu23385NN90UjO9ov7feeitlZWUt/UycOJEnnniC3bt3A/Dmm2/y2muvAVBUVNRyem4RkWTde4sBMn7e7dZOu33xxRezceNGquN59enTh4ceeojJkyfzk5/8hJEjR3LGGWdwzjnndHieyVshQKf6/eEPf8jnP/95ampquOuuu/j+97/PxRdfzOHDhykqKmL+/PkMGTKE2bNnM3LkSMaMGcPDDz/c4XpF5KNNp92WgF4Dkfym026LiEiXUzCIiEgg74IhV7u2RMteRCJ5FQwlJSXs3btXb1A54O7s3buXkpKSXJciIjmWV99KGjRoEPX19TQ0NOS6lONSSUkJgwYNynUZIpJjeRUMRUVFDB06NNdliIgc1/JqV5KIiOSegkFERAIKBhERCSgYREQkoGAQEZGAgkFERAIKBhERCaQVDGY22cw2m9k2M5ubYvrpZrbSzF40s1fM7NLMlyoiIl2h3WAwswJgPnAJMByYYWbDk5r9E/CYu48GpgP/nulCRUSka6SzxTAe2Obu2939ILAYmJLUxoET4tv9gDcyV6KIiHSldIJhILAr4X59PC7Rd4HrzKweeAb4cqqOzGy2mdWZWZ3OhyQikp/SCQZLMS759KczgPvdfRBwKfCgmR3Vt7svdPcqd68qLy/veLUiIpJ16QRDPTA44f4gjt5V9AXgMQB3Xw2UAAMyUaCIiHStdIJhLTDMzIaaWTHRweWnk9r8CZgIYGZnEgWD9hWJiHRD7QaDux8C5gDLgI1E3z5ab2a3mtkVcbNvADeY2cvAI8As19V2RES6pbSux+DuzxAdVE4cd0vC7Q3AuZktTUREckG/fBYRkYCCQUREAgoGEREJKBhERCSgYBARkYCCQUREAgoGEREJKBhERCSgYBARkYCCQUREAgoGEREJKBhERCSgYBARkYCCQUREAgoGEREJKBhERCSgYBARkYCCQUREAgoGEREJKBhERCSgYBARkYCCQURE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\n", 145 | "text/plain": [ 146 | "
" 147 | ] 148 | }, 149 | "metadata": { 150 | "needs_background": "light" 151 | }, 152 | "output_type": "display_data" 153 | }, 154 | { 155 | "data": { 156 | "image/png": "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RqO8/vrrXdvGjh2blLuwYcMGJk6cyL333suBBx4IwMKFC3nxxRe55557ANi4cSOrV69mwIABBZHdMPojjz6a/HrwYLj0Upg2rW/kKRVVoySam90Kwl9JNDcX9vhNTU289957bNiwgZqaGjo7O7u29TSf4Nvf/jbnnntumqnq+uuv59Of/jQvvPACnZ2d1NfXd23bfvvtk+buvPPODBkyhGeeeaZLSagqN910E8cee2zS3Fi1e9gMo0h4notoCrLXXpBi9a1KqsYn0dTkTEz/9V/FCUl79dVX6ejooKGhgX322YeVK1eyZcsWNm7cyBNPPNGjY5500knMmjUr7WK+ceNG9txzTyKRCHfccUdWJ/WAAQNYsGABra2tXU7pY489lltuuYVt27YB8Prrr/N///d/7Ljjjnz00Uc9ktUw+jOxmItmCrJ8uTNxV3uuRNWsJMAphkIqB98nAe7ufP78+USjUYYMGcK3vvUtRo0axf7778/oTMbKbthxxx25+OKL08bPOeccTj75ZP74xz/yxS9+MW31kMr222/Pww8/zNFHH83222/PmWeeydq1azn44INRVQYNGsSCBQsYNWoUNTU1HHTQQZx++ulccMEFPZLbMKodz3OKoaEB2tpcBFMwJVjEKY1CmbbLGelhMnRBGTNmjKY2HVq1ahXDhw/vI4kMsN/A6J/4QTCbNrnXIhCJOH+nTzTq/hYqSKaniMhzqjqmmO9RVSsJwzCM3hKLJRQEuBVEqsX3q1+FsWOd77OaVxFgSsIwDCOJhob0MRGoqXHJdAMGwKxZ1a8cfExJGIZhBGhrSx877TTYcUf3fMqU/qMgwJSEYRhGEs3NrnDfli3udWMj3HNPYhUxZUqfildyqiYE1jAMoxAsWOCilnxWrHAKI5io25+wlYRhGEaciy92pcAzUVNT+ETdcsdWElnoSanwnnD66aez3XbbJSW6nX/++YhIaKnxbGQqT56JK664Iq/jG0a14nlw7bXZ54wb17/8EWBKIivFKhUelkG933778cADDwDQ2dnJk08+yd57793r43aHKQnDcIRlVe+2W/Lr/tjRt7qURBFrhQdLhQNcc801XaW4f/KTn3SNT5o0iUMOOYQDDzyQlpaWrvEddtiBH//4x4wbNw4vRL5TTz21q3R4LBbj8MMPp6ampsfH3bRpE8cddxy//vWvAfjtb3/L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\n", 157 | "text/plain": [ 158 | "
" 159 | ] 160 | }, 161 | "metadata": { 162 | "needs_background": "light" 163 | }, 164 | "output_type": "display_data" 165 | } 166 | ], 167 | "source": [ 168 | "# Build the model and plot good/bad regime states\n", 169 | "model = CreateHMM(qb, symbol)\n", 170 | "PlotStates(qb, symbol, model)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "metadata": {}, 176 | "source": [ 177 | "##### (The specific application of an HMM to SPY returns and as use in risk management is thanks in part to articles on HMM and trading found [here](https://www.quantstart.com/articles/market-regime-detection-using-hidden-markov-models-in-qstrader/) and [here](https://www.quantstart.com/articles/hidden-markov-models-for-regime-detection-using-r/).)" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [] 186 | } 187 | ], 188 | "metadata": { 189 | "kernelspec": { 190 | "display_name": "Python 3", 191 | "language": "python", 192 | "name": "python3" 193 | }, 194 | "language_info": { 195 | "codemirror_mode": { 196 | "name": "ipython", 197 | "version": 3 198 | }, 199 | "file_extension": ".py", 200 | "mimetype": "text/x-python", 201 | "name": "python", 202 | "nbconvert_exporter": "python", 203 | "pygments_lexer": "ipython3", 204 | "version": "3.6.8" 205 | } 206 | }, 207 | "nbformat": 4, 208 | "nbformat_minor": 2 209 | } -------------------------------------------------------------------------------- /flask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuantConnect/Research/d89cf67bc37bc032f0de47089e16d825bcd8a65f/flask.png --------------------------------------------------------------------------------