From 8d5701dd0a48931842d11042730f3c6f7674eea7 Mon Sep 17 00:00:00 2001 From: Rick Gentry Date: Sat, 24 Apr 2021 10:09:55 -0600 Subject: [PATCH 01/12] reorganize and moving avg --- .gitignore | 3 + done_data.csv => data/done_data.csv | 0 environment.yml | 107 ++++++ notebooks/run_pipeline.ipynb | 338 ++++++++++++++++++ sentiment_analysis/__init__.py | 0 sentiment_analysis/score.py | 123 +++++++ .../A2C_30k_dow_126.zip | Bin 0 -> 163867 bytes 7 files changed, 571 insertions(+) rename done_data.csv => data/done_data.csv (100%) create mode 100644 environment.yml create mode 100644 notebooks/run_pipeline.ipynb create mode 100644 sentiment_analysis/__init__.py create mode 100644 sentiment_analysis/score.py create mode 100644 trained_models/2021-03-31 10:30:20.968779/A2C_30k_dow_126.zip diff --git a/.gitignore b/.gitignore index b816aaa06..3771e9f19 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ results/ # remove DS_Store **/.DS_Store +# Remove vs stuff +.vscode + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/done_data.csv b/data/done_data.csv similarity index 100% rename from done_data.csv rename to data/done_data.csv diff --git a/environment.yml b/environment.yml new file mode 100644 index 000000000..a5e5165fc --- /dev/null +++ b/environment.yml @@ -0,0 +1,107 @@ +name: bdrl +channels: + - conda-forge + - defaults +dependencies: + - appnope=0.1.2=py38h50d1736_1 + - backcall=0.2.0=pyh9f0ad1d_0 + - backports=1.0=py_2 + - backports.functools_lru_cache=1.6.3=pyhd8ed1ab_0 + - ca-certificates=2020.12.5=h033912b_0 + - certifi=2020.12.5=py38h50d1736_1 + - decorator=4.4.2=py_0 + - ipykernel=5.5.3=py38h6c79ece_0 + - ipython=7.22.0=py38h6c79ece_0 + - ipython_genutils=0.2.0=py_1 + - jedi=0.18.0=py38h50d1736_2 + - jupyter_client=6.1.12=pyhd8ed1ab_0 + - jupyter_core=4.7.1=py38h50d1736_0 + - libcxx=11.1.0=habf9029_0 + - libffi=3.3=h046ec9c_2 + - libsodium=1.0.18=hbcb3906_1 + - ncurses=6.2=h2e338ed_4 + - openssl=1.1.1k=h0d85af4_0 + - parso=0.8.2=pyhd8ed1ab_0 + - pexpect=4.8.0=pyh9f0ad1d_2 + - pickleshare=0.7.5=py_1003 + - pip=21.0.1=pyhd8ed1ab_0 + - prompt-toolkit=3.0.18=pyha770c72_0 + - ptyprocess=0.7.0=pyhd3deb0d_0 + - pygments=2.8.1=pyhd8ed1ab_0 + - python=3.8.8=h4e93d89_0_cpython + - python-dateutil=2.8.1=py_0 + - python_abi=3.8=1_cp38 + - pyzmq=22.0.3=py38hd3b92b6_1 + - readline=8.1=h05e3726_0 + - setuptools=49.6.0=py38h50d1736_3 + - six=1.15.0=pyh9f0ad1d_0 + - sqlite=3.35.3=h44b9ce1_0 + - tk=8.6.10=h0419947_1 + - tornado=6.1=py38h5406a74_1 + - traitlets=5.0.5=py_0 + - wcwidth=0.2.5=pyh9f0ad1d_2 + - wheel=0.36.2=pyhd3deb0d_0 + - xz=5.2.5=haf1e3a3_1 + - zeromq=4.3.4=h1c7c35f_0 + - zlib=1.2.11=h7795811_1010 + - pip: + - absl-py==0.12.0 + - atari-py==0.2.6 + - attrs==20.3.0 + - cachetools==4.2.1 + - chardet==4.0.0 + - cloudpickle==1.6.0 + - cycler==0.10.0 + - empyrical==0.5.5 + - finrl==0.3.0 + - future==0.18.2 + - google-auth==1.28.0 + - google-auth-oauthlib==0.4.4 + - grpcio==1.36.1 + - gym==0.18.0 + - idna==2.10 + - iniconfig==1.1.1 + - int-date==0.1.8 + - ipython-genutils==0.2.0 + - joblib==1.0.1 + - kiwisolver==1.3.1 + - lxml==4.6.3 + - markdown==3.3.4 + - matplotlib==3.4.1 + - multitasking==0.0.9 + - numpy==1.20.2 + - oauthlib==3.1.0 + - opencv-python==4.5.1.48 + - packaging==20.9 + - pandas==1.2.3 + - pandas-datareader==0.9.0 + - pillow==7.2.0 + - pluggy==0.13.1 + - protobuf==3.15.6 + - psutil==5.8.0 + - py==1.10.0 + - pyasn1==0.4.8 + - pyasn1-modules==0.2.8 + - pyfolio==0.9.2 + - pyglet==1.5.0 + - pyparsing==2.4.7 + - pytest==6.2.2 + - pytz==2021.1 + - requests==2.25.1 + - requests-oauthlib==1.3.0 + - rsa==4.7.2 + - scikit-learn==0.24.1 + - scipy==1.6.2 + - seaborn==0.11.1 + - stable-baselines3==1.0 + - stockstats==0.3.2 + - tensorboard==2.4.1 + - tensorboard-plugin-wit==1.8.0 + - threadpoolctl==2.1.0 + - toml==0.10.2 + - torch==1.8.1 + - typing-extensions==3.7.4.3 + - urllib3==1.26.4 + - werkzeug==1.0.1 + - yfinance==0.1.59 +prefix: /Users/rickgentry/opt/anaconda3/envs/bdrl diff --git a/notebooks/run_pipeline.ipynb b/notebooks/run_pipeline.ipynb new file mode 100644 index 000000000..e11eb07fd --- /dev/null +++ b/notebooks/run_pipeline.ipynb @@ -0,0 +1,338 @@ +{ + "metadata": { + "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.8.8" + }, + "orig_nbformat": 2, + "kernelspec": { + "name": "python3", + "display_name": "Python 3.8.8 64-bit ('bdrl': conda)", + "metadata": { + "interpreter": { + "hash": "884827d7ddaa858276f89104a03cd002b38877c13fae0f667c5d4e67e7e2a66a" + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "from sklearn import preprocessing\n", + "\n", + "matplotlib.use(\"Agg\")\n", + "import datetime\n", + "\n", + "from finrl.config import config\n", + "from finrl.marketdata.yahoodownloader import YahooDownloader\n", + "from finrl.preprocessing.preprocessors import FeatureEngineer\n", + "from finrl.preprocessing.data import data_split\n", + "from finrl.env.env_stocktrading import StockTradingEnv\n", + "from finrl.model.models import DRLAgent\n", + "from finrl.trade.backtest import backtest_stats, backtest_plot, get_daily_return, get_baseline\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "['AAPL',\n", + " 'MSFT',\n", + " 'JPM',\n", + " 'V',\n", + " 'RTX',\n", + " 'PG',\n", + " 'GS',\n", + " 'NKE',\n", + " 'DIS',\n", + " 'AXP',\n", + " 'HD',\n", + " 'INTC',\n", + " 'WMT',\n", + " 'IBM',\n", + " 'MRK',\n", + " 'UNH',\n", + " 'KO',\n", + " 'CAT',\n", + " 'TRV',\n", + " 'JNJ',\n", + " 'CVX',\n", + " 'MCD',\n", + " 'VZ',\n", + " 'CSCO',\n", + " 'XOM',\n", + " 'BA',\n", + " 'MMM',\n", + " 'PFE',\n", + " 'WBA',\n", + " 'DD']" + ] + }, + "metadata": {}, + "execution_count": 15 + } + ], + "source": [ + "config.DOW_30_TICKER" + ] + }, + { + "source": [ + "## Fetch Data" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "# Loads numerical data from csv if filepath is specified, otherwise downloads it from yfinance for specified dates and tickers\n", + "def get_numerical_data(filepath='',start_date='2020-01-01',end_date='2021-01-01',ticker_list=config.DOW_30_TICKER):\n", + " if filepath:\n", + " df = data.load_dataset(filepath)\n", + " else:\n", + " df = YahooDownloader(start_date=start_date,end_date=end_date,ticker_list=ticker_list).fetch_data()\n", + " return df" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "# Need to pull textual data from sources\n", + "def get_textual_data():\n", + " pass\n", + "\n", + "# Run through sentiment analysis model to get the sentiment\n", + "def analyze_textual_data():\n", + " pass\n", + "\n", + "# Compute sentiment score. This needs to be computed for every ticker and day based on the sentiment analysis models output for text related to that day.\n", + "def compute_score():\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "Shape of DataFrame: (7590, 8)\n" + ] + } + ], + "source": [ + "numerical_df = get_numerical_data()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " date open high low close volume \\\n", + "0 2020-01-02 74.059998 75.150002 73.797501 74.333511 135480400 \n", + "1 2020-01-02 124.660004 126.269997 124.230003 123.637741 2708000 \n", + "2 2020-01-02 328.549988 333.350006 327.700012 331.348572 4544400 \n", + "3 2020-01-02 149.000000 150.550003 147.979996 145.354584 3311900 \n", + "4 2020-01-02 48.060001 48.419998 47.880001 46.776043 16708100 \n", + "\n", + " tic day \n", + "0 AAPL 3 \n", + "1 AXP 3 \n", + "2 BA 3 \n", + "3 CAT 3 \n", + "4 CSCO 3 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticday
02020-01-0274.05999875.15000273.79750174.333511135480400AAPL3
12020-01-02124.660004126.269997124.230003123.6377412708000AXP3
22020-01-02328.549988333.350006327.700012331.3485724544400BA3
32020-01-02149.000000150.550003147.979996145.3545843311900CAT3
42020-01-0248.06000148.41999847.88000146.77604316708100CSCO3
\n
" + }, + "metadata": {}, + "execution_count": 21 + } + ], + "source": [ + "numerical_df.head()" + ] + }, + { + "source": [ + "## Preprocess data" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def join_data(numerical_df,sentiment_df):\n", + " return numerical_df.merge(sentiment_df,on=['datadate','tic'])\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def preprocess_data(numerical_df,sentiment_df,use_turbulence=False):\n", + " fe = FeatureEngineer(use_turbulence=use_turbulence)\n", + " numerical_df = fe.preprocess_data(numerical_df)\n", + " df = join_data(numerical_df,sentiment_df)\n", + " return df" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Successfully added technical indicators\n" + ] + } + ], + "source": [ + "fe = FeatureEngineer()\n", + "numerical_df = fe.preprocess_data(numerical_df)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " date open high low close volume \\\n", + "0 2020-01-02 74.059998 75.150002 73.797501 74.333511 135480400 \n", + "1 2020-01-02 124.660004 126.269997 124.230003 123.637741 2708000 \n", + "2 2020-01-02 328.549988 333.350006 327.700012 331.348572 4544400 \n", + "3 2020-01-02 149.000000 150.550003 147.979996 145.354584 3311900 \n", + "4 2020-01-02 48.060001 48.419998 47.880001 46.776043 16708100 \n", + "\n", + " tic day macd boll_ub boll_lb rsi_30 cci_30 \\\n", + "0 AAPL 3 0.000000 74.994187 72.950164 0.000000 -66.666667 \n", + "1 AXP 3 -0.016214 74.994187 72.950164 0.000000 -66.666667 \n", + "2 BA 3 -0.002471 74.815289 73.279208 45.641442 -100.000000 \n", + "3 CAT 3 -0.008758 74.655408 73.339686 35.632520 81.412298 \n", + "4 CSCO 3 0.035281 75.295238 73.115391 63.681143 166.666667 \n", + "\n", + " dx_30 close_30_sma close_60_sma \n", + "0 100.000000 74.333511 74.333511 \n", + "1 100.000000 73.972176 73.972176 \n", + "2 100.000000 74.047249 74.047249 \n", + "3 57.734339 73.997547 73.997547 \n", + "4 14.772271 74.205315 74.205315 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticdaymacdboll_ubboll_lbrsi_30cci_30dx_30close_30_smaclose_60_sma
02020-01-0274.05999875.15000273.79750174.333511135480400AAPL30.00000074.99418772.9501640.000000-66.666667100.00000074.33351174.333511
12020-01-02124.660004126.269997124.230003123.6377412708000AXP3-0.01621474.99418772.9501640.000000-66.666667100.00000073.97217673.972176
22020-01-02328.549988333.350006327.700012331.3485724544400BA3-0.00247174.81528973.27920845.641442-100.000000100.00000074.04724974.047249
32020-01-02149.000000150.550003147.979996145.3545843311900CAT3-0.00875874.65540873.33968635.63252081.41229857.73433973.99754773.997547
42020-01-0248.06000148.41999847.88000146.77604316708100CSCO30.03528175.29523873.11539163.681143166.66666714.77227174.20531574.205315
\n
" + }, + "metadata": {}, + "execution_count": 23 + } + ], + "source": [ + "numerical_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "output_type": "error", + "ename": "NameError", + "evalue": "name 'training' is not defined", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mtraining\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain_one\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'training' is not defined" + ] + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ] +} \ No newline at end of file diff --git a/sentiment_analysis/__init__.py b/sentiment_analysis/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sentiment_analysis/score.py b/sentiment_analysis/score.py new file mode 100644 index 000000000..b084da755 --- /dev/null +++ b/sentiment_analysis/score.py @@ -0,0 +1,123 @@ +import numpy as np + + +def calc_alpha(window, weight_proportion): + ''' + Calculate alpha parameter for exponentially weighted moving average + + :param window: number of values that weights will add up to the weight_proportion + :param weight_proportion: float [0,1], gives the amount of cumulative weight given to last window values + :return: alpha parameter for ewma + ''' + return 1 - np.exp(np.log(1-weight_proportion)/window) + + +def get_weights(alpha,num_steps): + ''' + Shows weights of last num_steps values + ''' + return [alpha] + [alpha*(1-alpha)**i for i in range(1,num_steps)] + +def update_ewma(prev_stat, data_point, alpha): + ''' + Updates the exponentially weighted moving average given a new data_point and parameter alpha + ''' + return data_point*alpha + (1-alpha) * prev_stat + +# This can be used for calculating the ewma given a vector as a start. Safe for large sizes of input +def ewma_vectorized(data, alpha, offset=None, dtype=None, order='C', out=None): + """ + Calculates the exponential moving average over a vector. + Will fail for large inputs. + Params: + :param data: Input data + :param alpha: scalar float in range (0,1) + The alpha parameter for the moving average. + :param offset: optional + The offset for the moving average, scalar. Defaults to data[0]. + :param dtype: optional + Data type used for calculations. Defaults to float64 unless + data.dtype is float32, then it will use float32. + :param order: {'C', 'F', 'A'}, optional + Order to use when flattening the data. Defaults to 'C'. + :param out: ndarray, or None, optional + A location into which the result is stored. If provided, it must have + the same shape as the input. If not provided or `None`, + a freshly-allocated array is returned. + :return out + """ + data = np.array(data, copy=False) + + if dtype is None: + if data.dtype == np.float32: + dtype = np.float32 + else: + dtype = np.float64 + else: + dtype = np.dtype(dtype) + + if data.ndim > 1: + # flatten input + data = data.reshape(-1, order) + + if out is None: + out = np.empty_like(data, dtype=dtype) + else: + assert out.shape == data.shape + assert out.dtype == dtype + + if data.size < 1: + # empty input, return empty array + return out + + if offset is None: + offset = data[0] + + alpha = np.array(alpha, copy=False).astype(dtype, copy=False) + + # scaling_factors -> 0 as len(data) gets large + # this leads to divide-by-zeros below + scaling_factors = np.power(1. - alpha, np.arange(data.size + 1, dtype=dtype), + dtype=dtype) + # create cumulative sum array + np.multiply(data, (alpha * scaling_factors[-2]) / scaling_factors[:-1], + dtype=dtype, out=out) + np.cumsum(out, dtype=dtype, out=out) + + # cumsums / scaling + out /= scaling_factors[-2::-1] + + if offset != 0: + offset = np.array(offset, copy=False).astype(dtype, copy=False) + # add offsets + out += offset * scaling_factors[1:] + + return out + + + initial_avg = vals[0] + get_weights(alpha,len(vals)) + + + +def test_calc_alpha(): + alpha = (calc_alpha(7,.99)) + weights = get_weights(alpha, 10) + print(alpha) + print(weights) + print(sum(weights)) + +def test_update_ewma(): + alpha = (calc_alpha(10,0.9)) + scores = np.array([-1,-1,-1,0,0,0,0,1,1,1,1,1,1,1,1]) + avg = scores[0] + ewmas = [avg] + for i in range(1,len(scores)): + avg = (update_ewma(avg,scores[i],alpha)) + ewmas.append(avg) + print(ewmas) + + + +if __name__ == "__main__": + test_update_ewma() \ No newline at end of file diff --git a/trained_models/2021-03-31 10:30:20.968779/A2C_30k_dow_126.zip b/trained_models/2021-03-31 10:30:20.968779/A2C_30k_dow_126.zip new file mode 100644 index 0000000000000000000000000000000000000000..c0331727395b3b5ccb4f8c720ba0923d69f95182 GIT binary patch literal 163867 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 literal 0 HcmV?d00001 From 05b460638fb86c190d915b4c45268283b073974d Mon Sep 17 00:00:00 2001 From: Aman Satya Date: Mon, 26 Apr 2021 23:36:11 -0600 Subject: [PATCH 02/12] Add files via upload get_sentiment_score(sentence,stock): return dic --- Sentiment_model.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 Sentiment_model.py diff --git a/Sentiment_model.py b/Sentiment_model.py new file mode 100644 index 000000000..1bb7eaf66 --- /dev/null +++ b/Sentiment_model.py @@ -0,0 +1,64 @@ + +from transformers import AutoTokenizer, AutoModelForSequenceClassification +from transformers import pipeline +import torch +import numpy as np + + + +model_name = "ProsusAI/finbert" +model = AutoModelForSequenceClassification.from_pretrained(model_name) +tokenizer = AutoTokenizer.from_pretrained(model_name) +classifier = pipeline('sentiment-analysis', model=model, tokenizer=tokenizer) + +dic={'MMM':0,'AXP':0,'AMGN':0,'AAPL':0,'BA':0,'CAT':0,'CVX':0,'CSCO':0,'KO':0,'DIS':0,'DOW':0,'GS':0,'HD':0,'HON':0,'IBM':0,'INTC':0,'JNJ':0,'JPM':0,'MCD':0,'MRK':0,'MSFT':0,'NKE':0,'PG':0,'CRM':0,'TRV':0,'UNH':0,'VZ':0,'V':0,'WBA':0,'WMT':0} + + + + +def get_sentiment_score(sentence, stock): + out= classifier(sentence) + # print(out) + pos=0 + neg=0 + neutral=0 + sentiment_score=0 + for i in out: + # print(i['label']) + if(i['label']=='POSITIVE'): + pos=i['score'] + # print(pos) + elif(i['label']=='NEGATIVE'): + neg=i['score'] + # print(neg) + else: + neutral= i['score'] + + if(pos!=0 or neg!=0): + sentiment_score= pos-neg + else: + sentiment_score=neutral + + if(dic[stock]==0): + avg= sentiment_score + dic[stock]=avg + + else: + alpha = (calc_alpha(10,0.9)) + avg= dic[stock] + res = update_ewma(avg,sentiment_score,alpha) + dic[stock]=res + + return dic + +def calc_alpha(window, weight_proportion): + + return 1 - np.exp(np.log(1-weight_proportion)/window) + + +def update_ewma(prev_stat, data_point, alpha): + + return data_point*alpha + (1-alpha) * prev_stat + + + From 44414b04e21881d85dfbf20fc30f5437b1e13b10 Mon Sep 17 00:00:00 2001 From: Rick Gentry Date: Tue, 27 Apr 2021 18:36:43 -0600 Subject: [PATCH 03/12] RL prediction and data processing --- .gitignore | 3 + model/online_stock_prediction.py | 131 +++++ notebooks/run_pipeline.ipynb | 896 +++++++++++++++++++++++++++++-- preprocessing/data_processor.py | 82 +++ requirements.txt | 2 +- 5 files changed, 1056 insertions(+), 58 deletions(-) create mode 100644 model/online_stock_prediction.py create mode 100644 preprocessing/data_processor.py diff --git a/.gitignore b/.gitignore index 3771e9f19..a6297371d 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ dmypy.json # Pyre type checker .pyre/ + +# FinRL library +src \ No newline at end of file diff --git a/model/online_stock_prediction.py b/model/online_stock_prediction.py new file mode 100644 index 000000000..2e48b338a --- /dev/null +++ b/model/online_stock_prediction.py @@ -0,0 +1,131 @@ +import pandas as pd +import numpy as np +import datetime +from finrl.config import config +from finrl.marketdata.yahoodownloader import YahooDownloader +from finrl.preprocessing.preprocessors import FeatureEngineer +from finrl.preprocessing.data import data_split +from finrl.env.env_stocktrading import StockTradingEnv +from finrl.env.env_stocks import StockEnv +from finrl.env.env_onlinestocktrading import OnlineStockTradingEnv +from finrl.model.models import DRLAgent +from finrl.trade.backtest import backtest_stats, backtest_plot, get_daily_return, get_baseline + + + + +class OnlineStockPrediction: + + def __init__(self, e_trade_gym, model): + self.e_trade_gym = e_trade_gym + self.env_trade, self.cur_obs = self.e_trade_gym.get_sb_env() + self.model = model + #self.env_trade.reset() + + + # def _get_numerical_data(self,all=True,col_list=['date','open', 'high', 'low', 'close', 'volume', 'tic','day']): + # if not all: + # window = 30 + # if len(self.e_trade_gym.df.index.unique() > window): + # indices = self.e_trade_gym.df.unique()[-window:] + # prev_numerical_df = self.e_trade_gym.df.loc[indices][col_list] + # else: + # prev_numerical_df = e_trade_gym.df[col_list] + # return prev_numerical_df + + # def process_data(self,numerical_df,sentiment_df): + # full_numerical_df = self.compute_technical_indicators(numerical_df) + # new_df = full_numerical_df.merge(sentiment_df,on=['date','tic']) + # self.add_data(new_df) + # return new_df + + # def compute_technical_indicators(self,numerical_df): + # prev_numerical_df = self._get_numerical_data(all=False) + # full_df = prev_numerical_df.append(numerical_df) + # full_df = self.feature_engineer.preprocess_data(full_df) + # #TODO Might need to just take the indices corresponding to the new numerical data + # return full_df + + def add_data(self,df): + self.e_trade_gym._update_data(df) + + + def predict(self): + #print("CURRENT OBSERVATION:" , self.cur_obs) + action, states = self.model.predict(self.cur_obs) + next_obs, rewards, done, info = self.e_trade_gym.step(action.squeeze()) + #print("NEXT OBSERVATION:", next_obs) + self.cur_obs = next_obs + return action,states, next_obs, rewards + + def run(self): + pass + + + + +def generate_sentiment_scores(start_date,end_date,tickers=config.DOW_30_TICKER,time_fmt="%Y-%m-%d"): + dates = pd.date_range(start_date,end_date).to_pydatetime() + dates = np.array([datetime.datetime.strftime(r,time_fmt) for r in dates]) + data = np.array(np.meshgrid(dates,tickers)).T.reshape(-1,2) + scores = np.random.uniform(low=-1.0,high=1.0,size=(len(data),1)) + df = pd.DataFrame(data,columns=['date','tic']) + df['sentiment'] = scores + return df + +def get_initial_data(numerical_df,sentiment_df,use_turbulence=False): + fe = FeatureEngineer(use_turbulence=use_turbulence) + numerical_df = fe.preprocess_data(numerical_df) + df = numerical_df.merge(sentiment_df,on=["date","tic"],how="left") + df.fillna(0) + return df + +def main(): + start_date = '2020-01-01' + trade_start_date='2020-12-01' + end_date='2021-01-01' + ticker_list=config.DOW_30_TICKER + numerical_df = YahooDownloader(start_date=start_date,end_date=end_date,ticker_list=ticker_list).fetch_data() + sentiment_df = generate_sentiment_scores(start_date,end_date) + initial_data = get_initial_data(numerical_df,sentiment_df) + train_data = data_split(initial_data,start_date,trade_start_date) + trade_data = data_split(initial_data,trade_start_date,end_date) + indicator_list = config.TECHNICAL_INDICATORS_LIST + ['sentiment'] + stock_dimension = len(trade_data.tic.unique()) + state_space = 1 + 2*stock_dimension + len(indicator_list)*stock_dimension + env_kwargs = { + "hmax": 100, + "initial_amount": 1000000, + "buy_cost_pct": 0.001, + "sell_cost_pct": 0.001, + "state_space": state_space, + "stock_dim": stock_dimension, + "tech_indicator_list": indicator_list, + "action_space": stock_dimension, + "reward_scaling": 1e-4, + "print_verbosity":5 + } + e_train_gym = StockTradingEnv(df = train_data, **env_kwargs) + env_train, _ = e_train_gym.get_sb_env() + # print(train_data.index) + # print(trade_data.index) + # print(trade_data.loc[0]) + e_trade_gym = OnlineStockTradingEnv(trade_data.loc[0], **env_kwargs) + training_agent = DRLAgent(env=env_train) + model_a2c = training_agent.get_model("a2c") + # print(train_data.index) + # print(trade_data.index) + #trained_a2c = agent.train_model(model=model_a2c, tb_log_name='a2c',total_timesteps=10000) + feature_engineer = FeatureEngineer() + online_stock_pred = OnlineStockPrediction(e_trade_gym,model_a2c,feature_engineer) + for i in range(1,trade_data.index.unique().max()): + print(trade_data.loc[i]) + online_stock_pred.add_data(trade_data.loc[i]) + action,states, next_obs, rewards = online_stock_pred.predict() + print("Action:" ,action) + print("States: ", states) + print("Next observation: ", next_obs) + print("Rewards: ", rewards) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/notebooks/run_pipeline.ipynb b/notebooks/run_pipeline.ipynb index e11eb07fd..75f13b2db 100644 --- a/notebooks/run_pipeline.ipynb +++ b/notebooks/run_pipeline.ipynb @@ -14,13 +14,8 @@ }, "orig_nbformat": 2, "kernelspec": { - "name": "python3", - "display_name": "Python 3.8.8 64-bit ('bdrl': conda)", - "metadata": { - "interpreter": { - "hash": "884827d7ddaa858276f89104a03cd002b38877c13fae0f667c5d4e67e7e2a66a" - } - } + "name": "python388jvsc74a57bd0884827d7ddaa858276f89104a03cd002b38877c13fae0f667c5d4e67e7e2a66a", + "display_name": "Python 3.8.8 64-bit ('bdrl': conda)" } }, "nbformat": 4, @@ -28,9 +23,17 @@ "cells": [ { "cell_type": "code", - "execution_count": 14, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/Users/rickgentry/opt/anaconda3/envs/bdrl/lib/python3.8/site-packages/pyfolio/pos.py:26: UserWarning: Module \"zipline.assets\" not found; multipliers will not be applied to position notionals.\n warnings.warn(\n" + ] + } + ], "source": [ "import pandas as pd\n", "import numpy as np\n", @@ -52,7 +55,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -92,7 +95,7 @@ ] }, "metadata": {}, - "execution_count": 15 + "execution_count": 3 } ], "source": [ @@ -108,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -123,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -142,7 +145,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -189,7 +192,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -198,10 +201,10 @@ "text/plain": [ " date open high low close volume \\\n", "0 2020-01-02 74.059998 75.150002 73.797501 74.333511 135480400 \n", - "1 2020-01-02 124.660004 126.269997 124.230003 123.637741 2708000 \n", + "1 2020-01-02 124.660004 126.269997 124.230003 123.267235 2708000 \n", "2 2020-01-02 328.549988 333.350006 327.700012 331.348572 4544400 \n", - "3 2020-01-02 149.000000 150.550003 147.979996 145.354584 3311900 \n", - "4 2020-01-02 48.060001 48.419998 47.880001 46.776043 16708100 \n", + "3 2020-01-02 149.000000 150.550003 147.979996 144.700500 3311900 \n", + "4 2020-01-02 48.060001 48.419998 47.880001 46.443089 16708100 \n", "\n", " tic day \n", "0 AAPL 3 \n", @@ -210,16 +213,121 @@ "3 CAT 3 \n", "4 CSCO 3 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticday
02020-01-0274.05999875.15000273.79750174.333511135480400AAPL3
12020-01-02124.660004126.269997124.230003123.6377412708000AXP3
22020-01-02328.549988333.350006327.700012331.3485724544400BA3
32020-01-02149.000000150.550003147.979996145.3545843311900CAT3
42020-01-0248.06000148.41999847.88000146.77604316708100CSCO3
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticday
02020-01-0274.05999875.15000273.79750174.333511135480400AAPL3
12020-01-02124.660004126.269997124.230003123.2672352708000AXP3
22020-01-02328.549988333.350006327.700012331.3485724544400BA3
32020-01-02149.000000150.550003147.979996144.7005003311900CAT3
42020-01-0248.06000148.41999847.88000146.44308916708100CSCO3
\n
" }, "metadata": {}, - "execution_count": 21 + "execution_count": 7 } ], "source": [ "numerical_df.head()" ] }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "7586 2020-12-31\n", + "7587 2020-12-31\n", + "7588 2020-12-31\n", + "7589 2020-12-31\n", + "Name: date, dtype: object" + ] + }, + "metadata": {}, + "execution_count": 84 + } + ], + "source": [ + "numerical_df[-4:]['date']" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "# time_fmt = \"%Y-%m-%d\"\n", + "# dates = pd.date_range('2020-01-01','2021-01-01').to_pydatetime()\n", + "# dates = np.array([datetime.strftime(r,time_fmt) for r in dates])\n", + "# tickers = np.array(config.DOW_30_TICKER)\n", + "# data = np.array(np.meshgrid(dates,tickers)).T.reshape(-1,2)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "\n", + "def generate_sentiment_scores(start_date,end_date,tickers=config.DOW_30_TICKER,time_fmt=\"%Y-%m-%d\"):\n", + " dates = pd.date_range(start_date,end_date).to_pydatetime()\n", + " dates = np.array([datetime.strftime(r,time_fmt) for r in dates])\n", + " data = np.array(np.meshgrid(dates,tickers)).T.reshape(-1,2)\n", + " scores = np.random.uniform(low=-1.0,high=1.0,size=(len(data),1))\n", + " data = np.concatenate((data,scores),axis=1)\n", + " df = pd.DataFrame(data,columns=['date','tic','sentiment'])\n", + " return df\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "sentiment_df = generate_sentiment_scores('2020-01-02','2021-01-01')" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " date tic sentiment\n", + "0 2020-01-02 AAPL 0.2833926324598415\n", + "1 2020-01-02 MSFT 0.35161072063542154\n", + "2 2020-01-02 JPM -0.5808555250854983\n", + "3 2020-01-02 V -0.39750294456355584\n", + "4 2020-01-02 RTX -0.2170646506354239\n", + ".. ... ... ...\n", + "56 2020-01-03 MMM 0.05423965692744859\n", + "57 2020-01-03 PFE 0.2665994090757775\n", + "58 2020-01-03 WBA -0.8667574036009138\n", + "59 2020-01-03 DD 0.27551254408626114\n", + "60 2020-01-04 AAPL -0.1348328790150064\n", + "\n", + "[61 rows x 3 columns]" + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateticsentiment
02020-01-02AAPL0.2833926324598415
12020-01-02MSFT0.35161072063542154
22020-01-02JPM-0.5808555250854983
32020-01-02V-0.39750294456355584
42020-01-02RTX-0.2170646506354239
............
562020-01-03MMM0.05423965692744859
572020-01-03PFE0.2665994090757775
582020-01-03WBA-0.8667574036009138
592020-01-03DD0.27551254408626114
602020-01-04AAPL-0.1348328790150064
\n

61 rows × 3 columns

\n
" + }, + "metadata": {}, + "execution_count": 47 + } + ], + "source": [ + "sentiment_df[:61]" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [] + }, { "source": [ "## Preprocess data" @@ -229,17 +337,17 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def join_data(numerical_df,sentiment_df):\n", - " return numerical_df.merge(sentiment_df,on=['datadate','tic'])\n" + " return numerical_df.merge(sentiment_df,on=['date','tic'])\n" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -252,7 +360,233 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Successfully added technical indicators\n" + ] + } + ], + "source": [ + "# Single sample\n", + "\n", + "two_day_numerical = numerical_df.loc[:60]\n", + "two_day_sentiment = sentiment_df.loc[:60]\n", + "two_day_data = preprocess_data(single_day_numerical,single_day_sentiment)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " date open high low close volume \\\n", + "0 2020-01-02 74.059998 75.150002 73.797501 74.333511 135480400 \n", + "1 2020-01-02 124.660004 126.269997 124.230003 123.267235 2708000 \n", + "2 2020-01-02 328.549988 333.350006 327.700012 331.348572 4544400 \n", + "3 2020-01-02 149.000000 150.550003 147.979996 144.700500 3311900 \n", + "4 2020-01-02 48.060001 48.419998 47.880001 46.443089 16708100 \n", + "5 2020-01-02 120.809998 121.629997 120.769997 113.316681 5205000 \n", + "6 2020-01-02 64.800003 65.160004 63.480000 61.819908 5967300 \n", + "7 2020-01-02 145.289993 148.199997 145.100006 148.199997 9502100 \n", + "8 2020-01-02 231.000000 234.639999 230.160004 227.913971 3736300 \n", + "9 2020-01-02 219.080002 219.759995 217.839996 213.260651 3935700 \n", + "10 2020-01-02 135.000000 135.919998 134.770004 126.975204 3148600 \n", + "11 2020-01-02 60.240002 60.970001 60.220001 59.005745 18056000 \n", + "12 2020-01-02 145.869995 146.020004 145.080002 141.226059 5777000 \n", + "13 2020-01-02 139.789993 141.100006 139.259995 134.380966 10803700 \n", + "14 2020-01-02 55.320000 55.430000 54.759998 52.731567 11867700 \n", + "15 2020-01-02 198.000000 200.800003 197.809998 194.704422 3554200 \n", + "16 2020-01-02 177.679993 180.009995 177.139999 172.119888 3601700 \n", + "17 2020-01-02 91.080002 92.139999 90.370003 88.430603 7873500 \n", + "18 2020-01-02 158.779999 160.729996 158.330002 158.571075 22622100 \n", + "19 2020-01-02 101.360001 102.209999 101.019997 101.029839 5644100 \n", + "20 2020-01-02 37.286530 37.333965 36.888046 35.293362 16514072 \n", + "21 2020-01-02 124.500000 124.730003 122.940002 118.952316 8130800 \n", + "22 2020-01-02 94.235367 96.425423 94.235367 93.120224 4451584 \n", + "23 2020-01-02 137.520004 137.740005 136.139999 133.839127 1117300 \n", + "24 2020-01-02 293.980011 295.700012 289.790009 286.745422 2543400 \n", + "25 2020-01-02 189.000000 191.139999 188.720001 189.656342 8733000 \n", + "26 2020-01-02 61.380001 61.450001 60.810001 57.256145 11447900 \n", + "27 2020-01-02 59.279999 59.590000 58.700001 56.046688 5700500 \n", + "28 2020-01-02 118.860001 119.889999 118.699997 116.500679 6764900 \n", + "29 2020-01-02 70.239998 71.019997 70.239998 64.560120 12456400 \n", + "30 2020-01-03 74.287498 75.144997 74.125000 73.610840 146322800 \n", + "31 2020-01-03 124.320000 125.099998 123.940002 122.042877 2090600 \n", + "32 2020-01-03 330.630005 334.890015 330.299988 330.791901 3875900 \n", + "33 2020-01-03 148.770004 149.960007 147.449997 142.691452 3100600 \n", + "34 2020-01-03 47.910000 48.139999 47.480000 45.685341 15577400 \n", + "35 2020-01-03 121.779999 122.720001 120.739998 112.924751 6360900 \n", + "36 2020-01-03 62.750000 62.950001 61.880001 60.515362 6005300 \n", + "37 2020-01-03 146.399994 147.899994 146.050003 146.500000 7320200 \n", + "38 2020-01-03 231.600006 232.610001 230.300003 225.248886 2274500 \n", + "39 2020-01-03 217.139999 219.679993 216.750000 212.551910 3423200 \n", + "40 2020-01-03 133.570007 134.860001 133.559998 125.962540 2373700 \n", + "41 2020-01-03 59.810001 60.700001 59.810001 58.288052 15293900 \n", + "42 2020-01-03 143.500000 145.369995 143.000000 139.590973 5752400 \n", + "43 2020-01-03 137.500000 139.229996 137.080002 132.607590 10386800 \n", + "44 2020-01-03 54.320000 54.990002 54.090000 52.443882 11354500 \n", + "45 2020-01-03 199.389999 200.550003 198.850006 194.015961 2767600 \n", + "46 2020-01-03 177.020004 178.660004 175.630005 170.637741 2466900 \n", + "47 2020-01-03 90.680000 92.070000 90.510002 87.671577 5633300 \n", + "48 2020-01-03 158.320007 159.949997 158.059998 156.596588 21116200 \n", + "49 2020-01-03 100.589996 102.000000 100.309998 100.753044 4541800 \n", + "50 2020-01-03 36.736244 37.229603 36.688805 35.104000 14922848 \n", + "51 2020-01-03 122.160004 123.529999 121.860001 118.152298 7970500 \n", + "52 2020-01-03 94.984268 97.325363 94.719948 93.247925 4969756 \n", + "53 2020-01-03 136.550003 137.369995 136.350006 133.362213 927300 \n", + "54 2020-01-03 287.269989 291.880005 284.359985 283.843658 2711400 \n", + "55 2020-01-03 188.410004 190.960007 187.919998 188.147980 4899700 \n", + "56 2020-01-03 60.590000 60.790001 60.070000 56.646542 13263200 \n", + "57 2020-01-03 58.540001 59.349998 58.180000 56.046688 4892300 \n", + "58 2020-01-03 118.269997 118.790001 117.589996 115.472214 5399200 \n", + "59 2020-01-03 71.339996 71.370003 70.160004 64.041092 17386900 \n", + "\n", + " tic day macd boll_ub boll_lb rsi_30 cci_30 dx_30 \\\n", + "0 AAPL 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "1 AXP 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "2 BA 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "3 CAT 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "4 CSCO 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "5 CVX 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "6 DD 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "7 DIS 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "8 GS 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "9 HD 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "10 IBM 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "11 INTC 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "12 JNJ 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "13 JPM 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "14 KO 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "15 MCD 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "16 MMM 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "17 MRK 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "18 MSFT 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "19 NKE 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "20 PFE 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "21 PG 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "22 RTX 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "23 TRV 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "24 UNH 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "25 V 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "26 VZ 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "27 WBA 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "28 WMT 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "29 XOM 3 0.000000 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "30 AAPL 4 -0.016214 74.994187 72.950164 0.0 -66.666667 100.0 \n", + "31 AXP 4 -0.027470 124.386559 120.923553 0.0 -66.666667 100.0 \n", + "32 BA 4 -0.012489 331.857488 330.282984 0.0 66.666667 100.0 \n", + "33 CAT 4 -0.045075 146.537200 140.854753 0.0 -66.666667 100.0 \n", + "34 CSCO 4 -0.017001 47.135832 44.992598 0.0 -66.666667 100.0 \n", + "35 CVX 4 -0.008793 113.674988 112.566444 0.0 66.666667 100.0 \n", + "36 DD 4 -0.029269 63.012542 59.322728 0.0 -66.666667 100.0 \n", + "37 DIS 4 -0.038141 149.754157 144.945840 0.0 -66.666667 100.0 \n", + "38 GS 4 -0.059794 230.350428 222.812429 0.0 -66.666667 100.0 \n", + "39 HD 4 -0.015901 213.908591 211.903970 0.0 -66.666667 100.0 \n", + "40 IBM 4 -0.022720 127.900996 125.036748 0.0 -66.666667 100.0 \n", + "41 INTC 4 -0.016102 59.661870 57.631927 0.0 -66.666667 100.0 \n", + "42 JNJ 4 -0.036685 142.720877 138.096155 0.0 -66.666667 100.0 \n", + "43 JPM 4 -0.039787 136.002211 130.986345 0.0 -66.666667 100.0 \n", + "44 KO 4 -0.006454 52.994573 52.180876 0.0 -66.666667 100.0 \n", + "45 MCD 4 -0.015446 195.333823 193.386560 0.0 66.666667 100.0 \n", + "46 MMM 4 -0.033253 173.474887 169.282742 0.0 -66.666667 100.0 \n", + "47 MRK 4 -0.017029 89.124515 86.977666 0.0 -66.666667 100.0 \n", + "48 MSFT 4 -0.044299 160.376179 154.791485 0.0 -66.666667 100.0 \n", + "49 NKE 4 -0.006210 101.282888 100.499995 0.0 -66.666667 100.0 \n", + "50 PFE 4 -0.004248 35.466479 34.930883 0.0 -66.666667 100.0 \n", + "51 PG 4 -0.017949 119.683704 117.420910 0.0 -66.666667 100.0 \n", + "52 RTX 4 0.002865 93.364671 93.003478 100.0 66.666667 100.0 \n", + "53 TRV 4 -0.010700 134.275127 132.926212 0.0 -66.666667 100.0 \n", + "54 UNH 4 -0.065104 289.398254 281.190827 0.0 -66.666667 100.0 \n", + "55 V 4 -0.033841 191.035306 186.769015 0.0 -66.666667 100.0 \n", + "56 VZ 4 -0.013677 57.813454 56.089233 0.0 -66.666667 100.0 \n", + "57 WBA 4 0.000000 56.046688 56.046688 0.0 -66.666667 100.0 \n", + "58 WMT 4 -0.023075 117.440916 114.531977 0.0 -66.666667 100.0 \n", + "59 XOM 4 -0.011645 65.034622 63.566590 0.0 -66.666667 100.0 \n", + "\n", + " close_30_sma close_60_sma sentiment \n", + "0 74.333511 74.333511 0.2833926324598415 \n", + "1 123.267235 123.267235 -0.11115165541267769 \n", + "2 331.348572 331.348572 0.29541394800696064 \n", + "3 144.700500 144.700500 0.8798273908339771 \n", + "4 46.443089 46.443089 -0.07381205009223857 \n", + "5 113.316681 113.316681 -0.4464516834557266 \n", + "6 61.819908 61.819908 -0.957457220229198 \n", + "7 148.199997 148.199997 0.7801606410796214 \n", + "8 227.913971 227.913971 -0.9926437559956203 \n", + "9 213.260651 213.260651 -0.7517608373429434 \n", + "10 126.975204 126.975204 -0.9518127988808116 \n", + "11 59.005745 59.005745 -0.7295197689489716 \n", + "12 141.226059 141.226059 -0.368885697305978 \n", + "13 134.380966 134.380966 -0.5808555250854983 \n", + "14 52.731567 52.731567 -0.3168016119684709 \n", + "15 194.704422 194.704422 0.9544598341934427 \n", + "16 172.119888 172.119888 -0.45637313484623276 \n", + "17 88.430603 88.430603 -0.8483626960013035 \n", + "18 158.571075 158.571075 0.35161072063542154 \n", + "19 101.029839 101.029839 0.33235868699960736 \n", + "20 35.293362 35.293362 -0.21671163807774896 \n", + "21 118.952316 118.952316 -0.2930800244388918 \n", + "22 93.120224 93.120224 -0.2170646506354239 \n", + "23 133.839127 133.839127 -0.7729591574297954 \n", + "24 286.745422 286.745422 -0.07014709176506329 \n", + "25 189.656342 189.656342 -0.39750294456355584 \n", + "26 57.256145 57.256145 0.5056281510451979 \n", + "27 56.046688 56.046688 -0.7445853388654293 \n", + "28 116.500679 116.500679 -0.3850887915612753 \n", + "29 64.560120 64.560120 -0.9017768038998397 \n", + "30 73.972176 73.972176 0.49483270328122275 \n", + "31 122.655056 122.655056 0.42247144916316404 \n", + "32 331.070236 331.070236 -0.1438780409884277 \n", + "33 143.695976 143.695976 0.16136106311291898 \n", + "34 46.064215 46.064215 -0.6097691029467514 \n", + "35 113.120716 113.120716 -0.09582076137347384 \n", + "36 61.167635 61.167635 0.27551254408626114 \n", + "37 147.349998 147.349998 -0.2684184623789383 \n", + "38 226.581429 226.581429 -0.0666785697971437 \n", + "39 212.906281 212.906281 0.4502175044140855 \n", + "40 126.468872 126.468872 -0.3713517376463129 \n", + "41 58.646898 58.646898 0.9328784644234758 \n", + "42 140.408516 140.408516 -0.36031163722297554 \n", + "43 133.494278 133.494278 0.4659316067482131 \n", + "44 52.587725 52.587725 0.8255035548375986 \n", + "45 194.360191 194.360191 -0.6470241167267008 \n", + "46 171.378815 171.378815 0.05423965692744859 \n", + "47 88.051090 88.051090 -0.14301320432544107 \n", + "48 157.583832 157.583832 -0.35755855471695863 \n", + "49 100.891441 100.891441 -0.4964396444392536 \n", + "50 35.198681 35.198681 0.2665994090757775 \n", + "51 118.552307 118.552307 -0.4740679943893711 \n", + "52 93.184074 93.184074 -0.3879245811768506 \n", + "53 133.600670 133.600670 0.8920625605656347 \n", + "54 285.294540 285.294540 0.17691277602586508 \n", + "55 188.902161 188.902161 0.23339625131239883 \n", + "56 56.951344 56.951344 0.03492856441277947 \n", + "57 56.046688 56.046688 -0.8667574036009138 \n", + "58 115.986446 115.986446 0.3016037669797418 \n", + "59 64.300606 64.300606 0.6014761207460211 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticdaymacdboll_ubboll_lbrsi_30cci_30dx_30close_30_smaclose_60_smasentiment
02020-01-0274.05999875.15000273.79750174.333511135480400AAPL30.00000074.99418772.9501640.0-66.666667100.074.33351174.3335110.2833926324598415
12020-01-02124.660004126.269997124.230003123.2672352708000AXP30.00000074.99418772.9501640.0-66.666667100.0123.267235123.267235-0.11115165541267769
22020-01-02328.549988333.350006327.700012331.3485724544400BA30.00000074.99418772.9501640.0-66.666667100.0331.348572331.3485720.29541394800696064
32020-01-02149.000000150.550003147.979996144.7005003311900CAT30.00000074.99418772.9501640.0-66.666667100.0144.700500144.7005000.8798273908339771
42020-01-0248.06000148.41999847.88000146.44308916708100CSCO30.00000074.99418772.9501640.0-66.666667100.046.44308946.443089-0.07381205009223857
52020-01-02120.809998121.629997120.769997113.3166815205000CVX30.00000074.99418772.9501640.0-66.666667100.0113.316681113.316681-0.4464516834557266
62020-01-0264.80000365.16000463.48000061.8199085967300DD30.00000074.99418772.9501640.0-66.666667100.061.81990861.819908-0.957457220229198
72020-01-02145.289993148.199997145.100006148.1999979502100DIS30.00000074.99418772.9501640.0-66.666667100.0148.199997148.1999970.7801606410796214
82020-01-02231.000000234.639999230.160004227.9139713736300GS30.00000074.99418772.9501640.0-66.666667100.0227.913971227.913971-0.9926437559956203
92020-01-02219.080002219.759995217.839996213.2606513935700HD30.00000074.99418772.9501640.0-66.666667100.0213.260651213.260651-0.7517608373429434
102020-01-02135.000000135.919998134.770004126.9752043148600IBM30.00000074.99418772.9501640.0-66.666667100.0126.975204126.975204-0.9518127988808116
112020-01-0260.24000260.97000160.22000159.00574518056000INTC30.00000074.99418772.9501640.0-66.666667100.059.00574559.005745-0.7295197689489716
122020-01-02145.869995146.020004145.080002141.2260595777000JNJ30.00000074.99418772.9501640.0-66.666667100.0141.226059141.226059-0.368885697305978
132020-01-02139.789993141.100006139.259995134.38096610803700JPM30.00000074.99418772.9501640.0-66.666667100.0134.380966134.380966-0.5808555250854983
142020-01-0255.32000055.43000054.75999852.73156711867700KO30.00000074.99418772.9501640.0-66.666667100.052.73156752.731567-0.3168016119684709
152020-01-02198.000000200.800003197.809998194.7044223554200MCD30.00000074.99418772.9501640.0-66.666667100.0194.704422194.7044220.9544598341934427
162020-01-02177.679993180.009995177.139999172.1198883601700MMM30.00000074.99418772.9501640.0-66.666667100.0172.119888172.119888-0.45637313484623276
172020-01-0291.08000292.13999990.37000388.4306037873500MRK30.00000074.99418772.9501640.0-66.666667100.088.43060388.430603-0.8483626960013035
182020-01-02158.779999160.729996158.330002158.57107522622100MSFT30.00000074.99418772.9501640.0-66.666667100.0158.571075158.5710750.35161072063542154
192020-01-02101.360001102.209999101.019997101.0298395644100NKE30.00000074.99418772.9501640.0-66.666667100.0101.029839101.0298390.33235868699960736
202020-01-0237.28653037.33396536.88804635.29336216514072PFE30.00000074.99418772.9501640.0-66.666667100.035.29336235.293362-0.21671163807774896
212020-01-02124.500000124.730003122.940002118.9523168130800PG30.00000074.99418772.9501640.0-66.666667100.0118.952316118.952316-0.2930800244388918
222020-01-0294.23536796.42542394.23536793.1202244451584RTX30.00000074.99418772.9501640.0-66.666667100.093.12022493.120224-0.2170646506354239
232020-01-02137.520004137.740005136.139999133.8391271117300TRV30.00000074.99418772.9501640.0-66.666667100.0133.839127133.839127-0.7729591574297954
242020-01-02293.980011295.700012289.790009286.7454222543400UNH30.00000074.99418772.9501640.0-66.666667100.0286.745422286.745422-0.07014709176506329
252020-01-02189.000000191.139999188.720001189.6563428733000V30.00000074.99418772.9501640.0-66.666667100.0189.656342189.656342-0.39750294456355584
262020-01-0261.38000161.45000160.81000157.25614511447900VZ30.00000074.99418772.9501640.0-66.666667100.057.25614557.2561450.5056281510451979
272020-01-0259.27999959.59000058.70000156.0466885700500WBA30.00000074.99418772.9501640.0-66.666667100.056.04668856.046688-0.7445853388654293
282020-01-02118.860001119.889999118.699997116.5006796764900WMT30.00000074.99418772.9501640.0-66.666667100.0116.500679116.500679-0.3850887915612753
292020-01-0270.23999871.01999770.23999864.56012012456400XOM30.00000074.99418772.9501640.0-66.666667100.064.56012064.560120-0.9017768038998397
302020-01-0374.28749875.14499774.12500073.610840146322800AAPL4-0.01621474.99418772.9501640.0-66.666667100.073.97217673.9721760.49483270328122275
312020-01-03124.320000125.099998123.940002122.0428772090600AXP4-0.027470124.386559120.9235530.0-66.666667100.0122.655056122.6550560.42247144916316404
322020-01-03330.630005334.890015330.299988330.7919013875900BA4-0.012489331.857488330.2829840.066.666667100.0331.070236331.070236-0.1438780409884277
332020-01-03148.770004149.960007147.449997142.6914523100600CAT4-0.045075146.537200140.8547530.0-66.666667100.0143.695976143.6959760.16136106311291898
342020-01-0347.91000048.13999947.48000045.68534115577400CSCO4-0.01700147.13583244.9925980.0-66.666667100.046.06421546.064215-0.6097691029467514
352020-01-03121.779999122.720001120.739998112.9247516360900CVX4-0.008793113.674988112.5664440.066.666667100.0113.120716113.120716-0.09582076137347384
362020-01-0362.75000062.95000161.88000160.5153626005300DD4-0.02926963.01254259.3227280.0-66.666667100.061.16763561.1676350.27551254408626114
372020-01-03146.399994147.899994146.050003146.5000007320200DIS4-0.038141149.754157144.9458400.0-66.666667100.0147.349998147.349998-0.2684184623789383
382020-01-03231.600006232.610001230.300003225.2488862274500GS4-0.059794230.350428222.8124290.0-66.666667100.0226.581429226.581429-0.0666785697971437
392020-01-03217.139999219.679993216.750000212.5519103423200HD4-0.015901213.908591211.9039700.0-66.666667100.0212.906281212.9062810.4502175044140855
402020-01-03133.570007134.860001133.559998125.9625402373700IBM4-0.022720127.900996125.0367480.0-66.666667100.0126.468872126.468872-0.3713517376463129
412020-01-0359.81000160.70000159.81000158.28805215293900INTC4-0.01610259.66187057.6319270.0-66.666667100.058.64689858.6468980.9328784644234758
422020-01-03143.500000145.369995143.000000139.5909735752400JNJ4-0.036685142.720877138.0961550.0-66.666667100.0140.408516140.408516-0.36031163722297554
432020-01-03137.500000139.229996137.080002132.60759010386800JPM4-0.039787136.002211130.9863450.0-66.666667100.0133.494278133.4942780.4659316067482131
442020-01-0354.32000054.99000254.09000052.44388211354500KO4-0.00645452.99457352.1808760.0-66.666667100.052.58772552.5877250.8255035548375986
452020-01-03199.389999200.550003198.850006194.0159612767600MCD4-0.015446195.333823193.3865600.066.666667100.0194.360191194.360191-0.6470241167267008
462020-01-03177.020004178.660004175.630005170.6377412466900MMM4-0.033253173.474887169.2827420.0-66.666667100.0171.378815171.3788150.05423965692744859
472020-01-0390.68000092.07000090.51000287.6715775633300MRK4-0.01702989.12451586.9776660.0-66.666667100.088.05109088.051090-0.14301320432544107
482020-01-03158.320007159.949997158.059998156.59658821116200MSFT4-0.044299160.376179154.7914850.0-66.666667100.0157.583832157.583832-0.35755855471695863
492020-01-03100.589996102.000000100.309998100.7530444541800NKE4-0.006210101.282888100.4999950.0-66.666667100.0100.891441100.891441-0.4964396444392536
502020-01-0336.73624437.22960336.68880535.10400014922848PFE4-0.00424835.46647934.9308830.0-66.666667100.035.19868135.1986810.2665994090757775
512020-01-03122.160004123.529999121.860001118.1522987970500PG4-0.017949119.683704117.4209100.0-66.666667100.0118.552307118.552307-0.4740679943893711
522020-01-0394.98426897.32536394.71994893.2479254969756RTX40.00286593.36467193.003478100.066.666667100.093.18407493.184074-0.3879245811768506
532020-01-03136.550003137.369995136.350006133.362213927300TRV4-0.010700134.275127132.9262120.0-66.666667100.0133.600670133.6006700.8920625605656347
542020-01-03287.269989291.880005284.359985283.8436582711400UNH4-0.065104289.398254281.1908270.0-66.666667100.0285.294540285.2945400.17691277602586508
552020-01-03188.410004190.960007187.919998188.1479804899700V4-0.033841191.035306186.7690150.0-66.666667100.0188.902161188.9021610.23339625131239883
562020-01-0360.59000060.79000160.07000056.64654213263200VZ4-0.01367757.81345456.0892330.0-66.666667100.056.95134456.9513440.03492856441277947
572020-01-0358.54000159.34999858.18000056.0466884892300WBA40.00000056.04668856.0466880.0-66.666667100.056.04668856.046688-0.8667574036009138
582020-01-03118.269997118.790001117.589996115.4722145399200WMT4-0.023075117.440916114.5319770.0-66.666667100.0115.986446115.9864460.3016037669797418
592020-01-0371.33999671.37000370.16000464.04109217386900XOM4-0.01164565.03462263.5665900.0-66.666667100.064.30060664.3006060.6014761207460211
\n
" + }, + "metadata": {}, + "execution_count": 52 + } + ], + "source": [ + "two_day_data" + ] + }, + { + "cell_type": "code", + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -264,13 +598,359 @@ } ], "source": [ - "fe = FeatureEngineer()\n", - "numerical_df = fe.preprocess_data(numerical_df)" + "processed = preprocess_data(numerical_df,sentiment_df)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "# import itertools\n", + "# list_ticker = processed[\"tic\"].unique().tolist()\n", + "# list_date = list(pd.date_range(processed['date'].min(),processed['date'].max()).astype(str))\n", + "# combination = list(itertools.product(list_date,list_ticker))\n", + "\n", + "# processed_full = pd.DataFrame(combination,columns=[\"date\",\"tic\"]).merge(processed,on=[\"date\",\"tic\"],how=\"left\")\n", + "# processed_full = processed_full[processed_full['date'].isin(processed['date'])]\n", + "# processed_full = processed_full.sort_values(['date','tic'])\n", + "\n", + "\n", + "# processed_full = processed_full.fillna(0)" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " date open high low close volume \\\n", + "3478 2020-06-17 119.860001 120.129997 118.400002 117.619827 6722300 \n", + "3599 2020-06-23 46.900002 47.220001 46.500000 43.970928 18916600 \n", + "5921 2020-10-13 54.270000 54.290001 53.619999 53.118870 20005800 \n", + "1441 2020-03-12 87.660004 89.610001 81.809998 81.815742 12206600 \n", + "4890 2020-08-25 124.697502 125.180000 123.052498 124.424088 211495600 \n", + "\n", + " tic day macd boll_ub boll_lb rsi_30 cci_30 \\\n", + "3478 WMT 2 -1.290810 125.199126 116.131516 46.609719 -138.407024 \n", + "3599 XOM 1 0.368817 50.564067 40.015104 50.305326 0.772453 \n", + "5921 INTC 1 0.846364 53.585163 47.605143 54.869043 165.159351 \n", + "1441 AXP 3 -8.979919 145.336436 84.050319 27.036437 -198.757983 \n", + "4890 AAPL 1 6.845802 128.234868 96.448479 73.723113 127.021210 \n", + "\n", + " dx_30 close_30_sma close_60_sma sentiment \n", + "3478 18.794410 121.310914 121.087573 -0.398418752383233 \n", + "3599 4.928755 44.004824 41.655123 -0.8059696803289789 \n", + "5921 33.459339 50.166393 49.776057 0.18158025813670298 \n", + "1441 66.161600 119.648064 122.248775 -0.9747474568266239 \n", + "4890 68.616624 106.625726 97.585338 0.3361142593207198 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticdaymacdboll_ubboll_lbrsi_30cci_30dx_30close_30_smaclose_60_smasentiment
34782020-06-17119.860001120.129997118.400002117.6198276722300WMT2-1.290810125.199126116.13151646.609719-138.40702418.794410121.310914121.087573-0.398418752383233
35992020-06-2346.90000247.22000146.50000043.97092818916600XOM10.36881750.56406740.01510450.3053260.7724534.92875544.00482441.655123-0.8059696803289789
59212020-10-1354.27000054.29000153.61999953.11887020005800INTC10.84636453.58516347.60514354.869043165.15935133.45933950.16639349.7760570.18158025813670298
14412020-03-1287.66000489.61000181.80999881.81574212206600AXP3-8.979919145.33643684.05031927.036437-198.75798366.161600119.648064122.248775-0.9747474568266239
48902020-08-25124.697502125.180000123.052498124.424088211495600AAPL16.845802128.23486896.44847973.723113127.02121068.616624106.62572697.5853380.3361142593207198
\n
" + }, + "metadata": {}, + "execution_count": 24 + } + ], + "source": [ + "processed.sample(5)" + ] + }, + { + "source": [ + "## Setting up the environment" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 162, + "metadata": {}, + "outputs": [], + "source": [ + "trade = data_split(processed, '2020-12-01','2021-01-01')" + ] + }, + { + "cell_type": "code", + "execution_count": 147, + "metadata": {}, + "outputs": [], + "source": [ + "last_df = trade.loc[21]\n", + "\n", + "trade = trade.drop(21)" + ] + }, + { + "cell_type": "code", + "execution_count": 170, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " date open high low close volume tic \\\n", + "1 2020-12-02 122.019997 123.370003 120.889999 122.896355 89004200 AAPL \n", + "1 2020-12-02 119.279999 122.849998 118.900002 121.537247 3271900 AXP \n", + "1 2020-12-02 213.009995 224.990005 210.300003 223.850006 25912300 BA \n", + "1 2020-12-02 173.259995 174.419998 172.279999 172.171539 1971000 CAT \n", + "1 2020-12-02 43.389999 43.959999 43.349998 43.227016 17422200 CSCO \n", + "1 2020-12-02 87.260002 91.309998 87.099998 88.617355 10509600 CVX \n", + "1 2020-12-02 63.480000 64.290001 63.250000 63.684902 5959500 DD \n", + "1 2020-12-02 149.490005 154.009995 148.339996 153.610001 10601900 DIS \n", + "1 2020-12-02 232.080002 238.130005 231.580002 236.720154 2136300 GS \n", + "1 2020-12-02 273.970001 274.109985 269.570007 269.411774 4168600 HD \n", + "1 2020-12-02 122.849998 124.639999 122.410004 122.976685 3690700 IBM \n", + "1 2020-12-02 49.220001 50.060001 49.180000 49.598938 33753500 INTC \n", + "1 2020-12-02 147.850006 149.710007 147.699997 147.221954 7745500 JNJ \n", + "1 2020-12-02 119.699997 122.309998 119.269997 120.457932 10811300 JPM \n", + "1 2020-12-02 51.900002 52.130001 51.639999 51.679848 14913100 KO \n", + "1 2020-12-02 214.000000 214.399994 209.130005 209.570557 6223700 MCD \n", + "1 2020-12-02 170.259995 172.580002 170.220001 170.445450 2421800 MMM \n", + "1 2020-12-02 81.940002 82.739998 81.290001 80.481735 6837600 MRK \n", + "1 2020-12-02 214.880005 215.470001 212.800003 214.875107 23724500 MSFT \n", + "1 2020-12-02 135.160004 136.320007 134.669998 135.033234 4132700 NKE \n", + "1 2020-12-02 40.470001 41.410000 40.299999 40.360924 84347500 PFE \n", + "1 2020-12-02 139.369995 139.940002 137.490005 136.656113 6963000 PG \n", + "1 2020-12-02 71.190002 72.349998 70.690002 71.500046 5401600 RTX \n", + "1 2020-12-02 132.580002 134.279999 132.419998 133.197662 948400 TRV \n", + "1 2020-12-02 341.769989 351.829987 341.500000 345.088043 2864300 UNH \n", + "1 2020-12-02 211.000000 211.399994 208.479996 209.854202 9728900 V \n", + "1 2020-12-02 61.400002 61.950001 61.009998 60.063873 14169900 VZ \n", + "1 2020-12-02 38.500000 40.099998 38.389999 39.463787 8350800 WBA \n", + "1 2020-12-02 152.000000 152.619995 149.529999 149.348251 7849000 WMT \n", + "1 2020-12-02 38.389999 40.419998 38.340000 39.273056 29369500 XOM \n", + "\n", + " day macd boll_ub boll_lb rsi_30 cci_30 dx_30 \\\n", + "1 2 1.155794 122.805722 113.112443 56.727828 154.383004 18.326964 \n", + "1 2 4.855969 128.749669 97.409153 62.028803 89.511153 27.531221 \n", + "1 2 13.984483 239.745046 151.577955 63.765725 106.310809 46.270265 \n", + "1 2 3.476263 179.658458 158.813556 58.681287 64.315213 17.939864 \n", + "1 2 1.260845 44.596100 35.715147 61.543815 122.453264 49.933659 \n", + "1 2 4.274846 97.089123 68.545152 59.083672 82.819903 25.952298 \n", + "1 2 1.451896 65.811749 57.838978 58.141855 85.679606 31.433742 \n", + "1 2 5.910297 157.873490 125.776510 64.685415 109.627809 52.602178 \n", + "1 2 8.360214 243.926541 196.584662 64.374630 110.967327 43.734589 \n", + "1 2 -0.944513 281.791293 263.140854 48.497968 -63.312473 23.109887 \n", + "1 2 2.134021 125.476211 108.313706 56.223680 134.543842 22.198912 \n", + "1 2 0.211684 48.953709 43.227755 53.657276 127.255083 27.590260 \n", + "1 2 0.617159 150.505035 138.234351 54.479110 94.172556 16.105215 \n", + "1 2 4.618248 126.101105 101.519365 62.619666 89.538776 36.182661 \n", + "1 2 0.588356 54.433406 48.800277 55.922677 26.277790 7.999997 \n", + "1 2 -0.700665 217.831208 208.867687 46.215942 -112.124166 8.580493 \n", + "1 2 2.504897 178.973703 157.544793 54.931979 48.736132 14.206497 \n", + "1 2 0.319381 80.386801 78.151293 53.501224 104.705440 28.901184 \n", + "1 2 0.646495 221.489533 207.514984 52.140060 27.877785 3.735623 \n", + "1 2 2.444884 137.104205 124.671836 63.426064 123.045806 30.844240 \n", + "1 2 1.051314 39.227687 33.270638 66.510171 259.359393 55.141508 \n", + "1 2 -0.464932 142.354259 134.869215 49.645956 -70.492153 25.745587 \n", + "1 2 3.364471 78.241081 56.665192 60.365475 80.804586 23.943590 \n", + "1 2 2.975748 139.320666 125.329705 58.551823 47.673542 9.973399 \n", + "1 2 3.014990 359.988047 326.525271 57.457320 52.263896 23.169929 \n", + "1 2 3.105384 218.624165 196.712163 55.270354 57.102913 8.855146 \n", + "1 2 0.574362 61.005373 56.862147 60.438235 94.456302 35.663662 \n", + "1 2 0.455995 42.535704 34.916657 53.783927 48.518993 9.793683 \n", + "1 2 2.347821 154.586767 141.289839 58.043236 69.502107 23.805558 \n", + "1 2 1.479254 42.005492 31.235070 56.350455 95.687022 26.527777 \n", + "\n", + " close_30_sma close_60_sma sentiment \n", + "1 116.314661 115.369485 -0.984651738354918 \n", + "1 107.265336 104.394359 -0.9176770257026572 \n", + "1 182.422001 172.716168 -0.9572068126980808 \n", + "1 166.248767 158.887985 0.04679674467070649 \n", + "1 38.954216 38.811192 0.21756488479904545 \n", + "1 78.091903 75.165402 -0.5083814301305061 \n", + "1 60.476208 58.747960 0.2728045113638675 \n", + "1 135.727333 131.107500 0.9273346290420486 \n", + "1 211.794794 205.571125 0.6197933298098626 \n", + "1 272.434312 274.191360 0.4380411540759901 \n", + "1 114.370554 116.887375 -0.5538340961143591 \n", + "1 46.183893 48.457200 -0.9702955003015619 \n", + "1 142.673007 144.070394 0.12582628545149088 \n", + "1 108.841823 102.814144 0.6302514692451833 \n", + "1 50.578834 49.800701 0.06716112190767953 \n", + "1 214.861430 217.184572 -0.59283964713772 \n", + "1 166.002745 164.271172 -0.030348809818280698 \n", + "1 78.248804 79.456796 0.3860714757752912 \n", + "1 212.256269 210.551778 -0.9377874625885871 \n", + "1 129.016266 126.211955 0.8946925807909476 \n", + "1 35.520684 34.755356 -0.7348287684964152 \n", + "1 138.565613 137.868746 0.4972821283400899 \n", + "1 63.902177 61.525737 0.44880643926311103 \n", + "1 129.047579 120.059917 -0.4315987907494607 \n", + "1 333.850869 321.935051 0.2944977097406001 \n", + "1 201.521997 201.184046 0.5208271308347097 \n", + "1 57.977235 57.823531 0.45030105246049046 \n", + "1 37.612180 36.468485 0.42852109840948405 \n", + "1 145.548057 142.187921 -0.08145969873327075 \n", + "1 35.068157 34.371995 -0.49465033198191954 " + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticdaymacdboll_ubboll_lbrsi_30cci_30dx_30close_30_smaclose_60_smasentiment
12020-12-02122.019997123.370003120.889999122.89635589004200AAPL21.155794122.805722113.11244356.727828154.38300418.326964116.314661115.369485-0.984651738354918
12020-12-02119.279999122.849998118.900002121.5372473271900AXP24.855969128.74966997.40915362.02880389.51115327.531221107.265336104.394359-0.9176770257026572
12020-12-02213.009995224.990005210.300003223.85000625912300BA213.984483239.745046151.57795563.765725106.31080946.270265182.422001172.716168-0.9572068126980808
12020-12-02173.259995174.419998172.279999172.1715391971000CAT23.476263179.658458158.81355658.68128764.31521317.939864166.248767158.8879850.04679674467070649
12020-12-0243.38999943.95999943.34999843.22701617422200CSCO21.26084544.59610035.71514761.543815122.45326449.93365938.95421638.8111920.21756488479904545
12020-12-0287.26000291.30999887.09999888.61735510509600CVX24.27484697.08912368.54515259.08367282.81990325.95229878.09190375.165402-0.5083814301305061
12020-12-0263.48000064.29000163.25000063.6849025959500DD21.45189665.81174957.83897858.14185585.67960631.43374260.47620858.7479600.2728045113638675
12020-12-02149.490005154.009995148.339996153.61000110601900DIS25.910297157.873490125.77651064.685415109.62780952.602178135.727333131.1075000.9273346290420486
12020-12-02232.080002238.130005231.580002236.7201542136300GS28.360214243.926541196.58466264.374630110.96732743.734589211.794794205.5711250.6197933298098626
12020-12-02273.970001274.109985269.570007269.4117744168600HD2-0.944513281.791293263.14085448.497968-63.31247323.109887272.434312274.1913600.4380411540759901
12020-12-02122.849998124.639999122.410004122.9766853690700IBM22.134021125.476211108.31370656.223680134.54384222.198912114.370554116.887375-0.5538340961143591
12020-12-0249.22000150.06000149.18000049.59893833753500INTC20.21168448.95370943.22775553.657276127.25508327.59026046.18389348.457200-0.9702955003015619
12020-12-02147.850006149.710007147.699997147.2219547745500JNJ20.617159150.505035138.23435154.47911094.17255616.105215142.673007144.0703940.12582628545149088
12020-12-02119.699997122.309998119.269997120.45793210811300JPM24.618248126.101105101.51936562.61966689.53877636.182661108.841823102.8141440.6302514692451833
12020-12-0251.90000252.13000151.63999951.67984814913100KO20.58835654.43340648.80027755.92267726.2777907.99999750.57883449.8007010.06716112190767953
12020-12-02214.000000214.399994209.130005209.5705576223700MCD2-0.700665217.831208208.86768746.215942-112.1241668.580493214.861430217.184572-0.59283964713772
12020-12-02170.259995172.580002170.220001170.4454502421800MMM22.504897178.973703157.54479354.93197948.73613214.206497166.002745164.271172-0.030348809818280698
12020-12-0281.94000282.73999881.29000180.4817356837600MRK20.31938180.38680178.15129353.501224104.70544028.90118478.24880479.4567960.3860714757752912
12020-12-02214.880005215.470001212.800003214.87510723724500MSFT20.646495221.489533207.51498452.14006027.8777853.735623212.256269210.551778-0.9377874625885871
12020-12-02135.160004136.320007134.669998135.0332344132700NKE22.444884137.104205124.67183663.426064123.04580630.844240129.016266126.2119550.8946925807909476
12020-12-0240.47000141.41000040.29999940.36092484347500PFE21.05131439.22768733.27063866.510171259.35939355.14150835.52068434.755356-0.7348287684964152
12020-12-02139.369995139.940002137.490005136.6561136963000PG2-0.464932142.354259134.86921549.645956-70.49215325.745587138.565613137.8687460.4972821283400899
12020-12-0271.19000272.34999870.69000271.5000465401600RTX23.36447178.24108156.66519260.36547580.80458623.94359063.90217761.5257370.44880643926311103
12020-12-02132.580002134.279999132.419998133.197662948400TRV22.975748139.320666125.32970558.55182347.6735429.973399129.047579120.059917-0.4315987907494607
12020-12-02341.769989351.829987341.500000345.0880432864300UNH23.014990359.988047326.52527157.45732052.26389623.169929333.850869321.9350510.2944977097406001
12020-12-02211.000000211.399994208.479996209.8542029728900V23.105384218.624165196.71216355.27035457.1029138.855146201.521997201.1840460.5208271308347097
12020-12-0261.40000261.95000161.00999860.06387314169900VZ20.57436261.00537356.86214760.43823594.45630235.66366257.97723557.8235310.45030105246049046
12020-12-0238.50000040.09999838.38999939.4637878350800WBA20.45599542.53570434.91665753.78392748.5189939.79368337.61218036.4684850.42852109840948405
12020-12-02152.000000152.619995149.529999149.3482517849000WMT22.347821154.586767141.28983958.04323669.50210723.805558145.548057142.187921-0.08145969873327075
12020-12-0238.38999940.41999838.34000039.27305629369500XOM21.47925442.00549231.23507056.35045595.68702226.52777735.06815734.371995-0.49465033198191954
\n
" + }, + "metadata": {}, + "execution_count": 170 + } + ], + "source": [ + "trade[trade.date == '2020-12-02']" + ] + }, + { + "source": [ + "trade[trade.date > '2020-12-02'][['date','open']]" + ], + "cell_type": "code", + "metadata": {}, + "execution_count": 168, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " date open\n", + "2 2020-12-03 123.519997\n", + "2 2020-12-03 122.849998\n", + "2 2020-12-03 228.300003\n", + "2 2020-12-03 173.869995\n", + "2 2020-12-03 43.779999\n", + ".. ... ...\n", + "21 2020-12-31 218.399994\n", + "21 2020-12-31 58.060001\n", + "21 2020-12-31 39.330002\n", + "21 2020-12-31 144.199997\n", + "21 2020-12-31 41.470001\n", + "\n", + "[600 rows x 2 columns]" + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopen
22020-12-03123.519997
22020-12-03122.849998
22020-12-03228.300003
22020-12-03173.869995
22020-12-0343.779999
.........
212020-12-31218.399994
212020-12-3158.060001
212020-12-3139.330002
212020-12-31144.199997
212020-12-3141.470001
\n

600 rows × 2 columns

\n
" + }, + "metadata": {}, + "execution_count": 168 + } + ] + }, + { + "cell_type": "code", + "execution_count": 151, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " date open high low close volume \\\n", + "1 2020-12-02 122.019997 123.370003 120.889999 122.896355 89004200 \n", + "1 2020-12-02 119.279999 122.849998 118.900002 121.537247 3271900 \n", + "1 2020-12-02 213.009995 224.990005 210.300003 223.850006 25912300 \n", + "1 2020-12-02 173.259995 174.419998 172.279999 172.171539 1971000 \n", + "1 2020-12-02 43.389999 43.959999 43.349998 43.227016 17422200 \n", + ".. ... ... ... ... ... ... \n", + "20 2020-12-30 216.000000 220.389999 215.649994 218.021530 8875100 \n", + "20 2020-12-30 58.830002 58.939999 58.060001 56.911888 18259800 \n", + "20 2020-12-30 39.520000 39.730000 39.200001 38.968510 4194300 \n", + "20 2020-12-30 144.880005 145.149994 143.940002 143.580521 6250400 \n", + "20 2020-12-30 41.330002 42.419998 41.270000 40.905334 23807300 \n", + "\n", + " tic day macd boll_ub boll_lb rsi_30 cci_30 \\\n", + "1 AAPL 2 1.155794 122.805722 113.112443 56.727828 154.383004 \n", + "1 AXP 2 4.855969 128.749669 97.409153 62.028803 89.511153 \n", + "1 BA 2 13.984483 239.745046 151.577955 63.765725 106.310809 \n", + "1 CAT 2 3.476263 179.658458 158.813556 58.681287 64.315213 \n", + "1 CSCO 2 1.260845 44.596100 35.715147 61.543815 122.453264 \n", + ".. ... ... ... ... ... ... ... \n", + "20 V 2 1.774089 216.171158 203.211760 59.209227 263.789704 \n", + "20 VZ 2 -0.390335 61.048898 56.699199 43.708731 -189.807648 \n", + "20 WBA 2 0.022185 42.954975 38.082485 50.214539 -34.466659 \n", + "20 WMT 2 -0.837518 149.407225 141.840954 49.369350 -96.017810 \n", + "20 XOM 2 0.840002 43.642135 39.143972 55.195039 30.368622 \n", + "\n", + " dx_30 close_30_sma close_60_sma sentiment \n", + "1 18.326964 116.314661 115.369485 -0.984651738354918 \n", + "1 27.531221 107.265336 104.394359 -0.9176770257026572 \n", + "1 46.270265 182.422001 172.716168 -0.9572068126980808 \n", + "1 17.939864 166.248767 158.887985 0.04679674467070649 \n", + "1 49.933659 38.954216 38.811192 0.21756488479904545 \n", + ".. ... ... ... ... \n", + "20 29.424279 209.395252 204.046652 0.02611872782509783 \n", + "20 17.673111 58.962963 58.125952 0.08300506391087992 \n", + "20 3.350135 39.692671 38.252223 0.8986461664723171 \n", + "20 5.379552 147.089382 144.891253 -0.6932360295904421 \n", + "20 15.401619 40.351737 36.687027 -0.6605489617307765 \n", + "\n", + "[600 rows x 17 columns]" + ], + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticdaymacdboll_ubboll_lbrsi_30cci_30dx_30close_30_smaclose_60_smasentiment
12020-12-02122.019997123.370003120.889999122.89635589004200AAPL21.155794122.805722113.11244356.727828154.38300418.326964116.314661115.369485-0.984651738354918
12020-12-02119.279999122.849998118.900002121.5372473271900AXP24.855969128.74966997.40915362.02880389.51115327.531221107.265336104.394359-0.9176770257026572
12020-12-02213.009995224.990005210.300003223.85000625912300BA213.984483239.745046151.57795563.765725106.31080946.270265182.422001172.716168-0.9572068126980808
12020-12-02173.259995174.419998172.279999172.1715391971000CAT23.476263179.658458158.81355658.68128764.31521317.939864166.248767158.8879850.04679674467070649
12020-12-0243.38999943.95999943.34999843.22701617422200CSCO21.26084544.59610035.71514761.543815122.45326449.93365938.95421638.8111920.21756488479904545
......................................................
202020-12-30216.000000220.389999215.649994218.0215308875100V21.774089216.171158203.21176059.209227263.78970429.424279209.395252204.0466520.02611872782509783
202020-12-3058.83000258.93999958.06000156.91188818259800VZ2-0.39033561.04889856.69919943.708731-189.80764817.67311158.96296358.1259520.08300506391087992
202020-12-3039.52000039.73000039.20000138.9685104194300WBA20.02218542.95497538.08248550.214539-34.4666593.35013539.69267138.2522230.8986461664723171
202020-12-30144.880005145.149994143.940002143.5805216250400WMT2-0.837518149.407225141.84095449.369350-96.0178105.379552147.089382144.891253-0.6932360295904421
202020-12-3041.33000242.41999841.27000040.90533423807300XOM20.84000243.64213539.14397255.19503930.36862215.40161940.35173736.687027-0.6605489617307765
\n

600 rows × 17 columns

\n
" + }, + "metadata": {}, + "execution_count": 151 + } + ], + "source": [ + "trade.loc[trade.index[0]+1:]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "indicator_list = config.TECHNICAL_INDICATORS_LIST + ['sentiment']" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Stock Dimension: 30, State Space: 331\n" + ] + } + ], + "source": [ + "stock_dimension = len(processed_full.tic.unique())\n", + "state_space = 1 + 2*stock_dimension + len(indicator_list)*stock_dimension\n", + "print(f\"Stock Dimension: {stock_dimension}, State Space: {state_space}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [], + "source": [ + "env_kwargs = {\n", + " \"hmax\": 100, \n", + " \"initial_amount\": 1000000, \n", + " \"buy_cost_pct\": 0.001, \n", + " \"sell_cost_pct\": 0.001, \n", + " \"state_space\": state_space, \n", + " \"stock_dim\": stock_dimension, \n", + " \"tech_indicator_list\": indicator_list, \n", + " \"action_space\": stock_dimension, \n", + " \"reward_scaling\": 1e-4,\n", + " \"print_verbosity\":5\n", + " \n", + "}\n", + "\n", + "e_trade_gym = StockTradingEnv(df = trade, **env_kwargs)\n", + "env_trade, _ = e_trade_gym.get_sb_env()" + ] + }, + { + "cell_type": "code", + "execution_count": 75, "metadata": {}, "outputs": [ { @@ -278,54 +958,156 @@ "data": { "text/plain": [ " date open high low close volume \\\n", - "0 2020-01-02 74.059998 75.150002 73.797501 74.333511 135480400 \n", - "1 2020-01-02 124.660004 126.269997 124.230003 123.637741 2708000 \n", - "2 2020-01-02 328.549988 333.350006 327.700012 331.348572 4544400 \n", - "3 2020-01-02 149.000000 150.550003 147.979996 145.354584 3311900 \n", - "4 2020-01-02 48.060001 48.419998 47.880001 46.776043 16708100 \n", + "0 2020-12-01 121.010002 123.470001 120.010002 122.536896 128166800 \n", + "0 2020-12-01 120.320000 122.570000 119.849998 119.152794 3584400 \n", + "0 2020-12-01 214.309998 218.089996 213.000000 213.009995 15805200 \n", + "0 2020-12-01 175.389999 176.570007 172.940002 171.567505 2710200 \n", + "0 2020-12-01 43.009998 44.070000 43.009998 42.882305 23948800 \n", + "0 2020-12-01 89.279999 89.709999 87.070000 86.231079 9915700 \n", + "0 2020-12-01 64.839996 65.230003 63.330002 63.256752 4505200 \n", + "0 2020-12-01 149.570007 151.399994 149.000000 149.440002 8827800 \n", + "0 2020-12-01 231.960007 234.869995 231.350006 231.171967 2581000 \n", + "0 2020-12-01 278.730011 278.950012 275.549988 273.386841 3944900 \n", + "0 2020-12-01 123.900002 125.830002 123.080002 121.535934 5312100 \n", + "0 2020-12-01 48.750000 50.230000 48.709999 49.260990 57778200 \n", + "0 2020-12-01 146.289993 149.130005 145.860001 146.536240 9741300 \n", + "0 2020-12-01 120.339996 121.580002 119.629997 118.187737 12678200 \n", + "0 2020-12-01 52.139999 52.330002 51.790001 51.610428 18969300 \n", + "0 2020-12-01 218.880005 218.929993 215.529999 214.818268 4226300 \n", + "0 2020-12-01 174.220001 175.690002 170.009995 169.126312 3859600 \n", + "0 2020-12-01 80.949997 82.459999 80.820000 80.206345 9701500 \n", + "0 2020-12-01 214.509995 217.320007 213.350006 215.713181 30931300 \n", + "0 2020-12-01 136.440002 136.500000 134.750000 134.893799 3834500 \n", + "0 2020-12-01 39.400002 40.500000 39.009998 38.985886 72660800 \n", + "0 2020-12-01 139.160004 139.539993 138.160004 137.653671 7169700 \n", + "0 2020-12-01 72.720001 72.940002 71.209999 70.794388 6545800 \n", + "0 2020-12-01 132.509995 134.240005 131.990005 132.571625 1218300 \n", + "0 2020-12-01 344.769989 354.100006 339.929993 338.763336 3817700 \n", + "0 2020-12-01 212.130005 213.669998 211.039993 210.872620 8049400 \n", + "0 2020-12-01 60.430000 60.919998 60.279999 59.300350 14283200 \n", + "0 2020-12-01 38.380001 39.200001 38.310001 38.096817 8004500 \n", + "0 2020-12-01 153.600006 153.660004 151.660004 151.451736 7647100 \n", + "0 2020-12-01 38.959999 39.650002 38.470001 37.857101 32503100 \n", "\n", - " tic day macd boll_ub boll_lb rsi_30 cci_30 \\\n", - "0 AAPL 3 0.000000 74.994187 72.950164 0.000000 -66.666667 \n", - "1 AXP 3 -0.016214 74.994187 72.950164 0.000000 -66.666667 \n", - "2 BA 3 -0.002471 74.815289 73.279208 45.641442 -100.000000 \n", - "3 CAT 3 -0.008758 74.655408 73.339686 35.632520 81.412298 \n", - "4 CSCO 3 0.035281 75.295238 73.115391 63.681143 166.666667 \n", + " tic day macd boll_ub boll_lb rsi_30 cci_30 \\\n", + "0 AAPL 1 0.806734 122.766675 111.870384 56.465653 158.903167 \n", + "0 AXP 1 4.740563 128.735444 94.836253 60.587597 92.037957 \n", + "0 BA 1 13.502710 237.931128 146.371872 61.131542 100.686039 \n", + "0 CAT 1 3.661695 179.348927 158.511125 58.336805 76.050401 \n", + "0 CSCO 1 1.188129 44.341458 35.259680 60.649269 130.290847 \n", + "0 CVX 1 4.377220 97.004396 66.739570 57.242696 76.670547 \n", + "0 DD 1 1.486600 65.660502 57.529655 57.490340 98.279086 \n", + "0 DIS 1 5.628248 157.252273 123.438727 62.437060 103.344798 \n", + "0 GS 1 8.068587 242.898599 193.549264 62.155696 104.800845 \n", + "0 HD 1 -0.821353 281.941664 263.464095 50.789610 9.794276 \n", + "0 IBM 1 1.976144 124.757566 107.839123 54.877749 148.657424 \n", + "0 INTC 1 -0.013522 48.275757 43.371563 52.876832 102.259448 \n", + "0 JNJ 1 0.413395 150.711438 136.974939 53.598712 77.664611 \n", + "0 JPM 1 4.596265 125.822278 99.959343 61.128958 88.001113 \n", + "0 KO 1 0.636729 54.590589 48.331295 55.718840 31.880461 \n", + "0 MCD 1 -0.312211 217.704911 209.457038 51.036825 -14.473086 \n", + "0 MMM 1 2.736254 178.808849 156.940039 53.900958 56.935914 \n", + "0 MRK 1 0.223440 80.884380 77.170803 52.865942 93.671027 \n", + "0 MSFT 1 0.572630 222.105612 205.953179 52.648526 47.034706 \n", + "0 NKE 1 2.423039 136.936074 123.745399 63.290424 135.957603 \n", + "0 PFE 1 0.817742 38.419874 33.404417 63.351960 231.913066 \n", + "0 PG 1 -0.400841 142.396651 135.109301 51.280113 -59.457889 \n", + "0 RTX 1 3.498788 78.386926 54.997566 59.650436 88.203718 \n", + "0 TRV 1 3.146130 139.547557 124.337653 58.097045 47.067895 \n", + "0 UNH 1 2.557991 361.838079 322.072880 55.302689 46.423201 \n", + "0 V 1 3.227687 220.609626 192.517796 56.041466 74.856270 \n", + "0 VZ 1 0.531209 61.017065 56.497081 57.477822 64.122408 \n", + "0 WBA 1 0.390607 42.490830 34.673390 51.242226 21.865538 \n", + "0 WMT 1 2.550562 154.726389 140.382241 61.146359 104.402212 \n", + "0 XOM 1 1.463276 41.856731 30.664341 53.975487 84.355622 \n", "\n", - " dx_30 close_30_sma close_60_sma \n", - "0 100.000000 74.333511 74.333511 \n", - "1 100.000000 73.972176 73.972176 \n", - "2 100.000000 74.047249 74.047249 \n", - "3 57.734339 73.997547 73.997547 \n", - "4 14.772271 74.205315 74.205315 " + " dx_30 close_30_sma close_60_sma sentiment \n", + "0 18.326964 116.122535 115.195506 -0.6353855836609985 \n", + "0 31.349381 106.629814 104.078234 0.6005440884823487 \n", + "0 41.044077 180.535001 171.670001 0.4103206537661357 \n", + "0 20.330658 166.053826 158.454731 -0.5389422727751916 \n", + "0 49.933659 38.800245 38.741337 -0.5830288155235768 \n", + "0 21.597130 77.459857 74.967448 -0.01776743709505868 \n", + "0 32.254347 60.318391 58.625112 -0.5216685812161448 \n", + "0 48.584574 134.772000 130.784000 -0.9259336746217759 \n", + "0 39.014700 210.773879 204.969028 -0.5188158197824897 \n", + "0 10.655830 272.877826 274.136699 0.3704807831218777 \n", + "0 26.069621 114.076892 116.802797 0.7529713903986068 \n", + "0 27.590260 46.288067 48.434949 0.8539701717421719 \n", + "0 13.681658 142.521038 144.038986 0.5682866582278816 \n", + "0 34.500062 108.128854 102.435144 -0.42503771747413355 \n", + "0 6.226137 50.504422 49.749719 -0.9479379522530356 \n", + "0 13.648295 215.366526 217.208724 0.8792847797333472 \n", + "0 14.206497 165.924518 164.104555 0.11072713391558753 \n", + "0 26.952431 78.132093 79.471521 -0.8812162075634768 \n", + "0 2.171538 212.213684 210.331632 0.36688784032438826 \n", + "0 31.371592 128.780886 125.832491 0.42112365937297436 \n", + "0 51.173878 35.336263 34.638991 0.72625802410846 \n", + "0 22.114259 138.675353 137.816553 0.6270024530508409 \n", + "0 27.283244 63.533057 61.308451 -0.6990874391243174 \n", + "0 9.827462 128.519917 119.709633 0.7552892014347639 \n", + "0 23.169929 333.051594 321.247131 -0.8165491584577971 \n", + "0 16.239753 201.096742 201.011629 -0.502620224611146 \n", + "0 21.124652 57.843128 57.790385 0.6591081344198431 \n", + "0 3.172934 37.520049 36.399741 -0.5569183988036852 \n", + "0 43.725999 145.329107 141.988320 -0.3113203205386308 \n", + "0 21.828008 34.837608 34.328411 -0.14096433016302212 " ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticdaymacdboll_ubboll_lbrsi_30cci_30dx_30close_30_smaclose_60_sma
02020-01-0274.05999875.15000273.79750174.333511135480400AAPL30.00000074.99418772.9501640.000000-66.666667100.00000074.33351174.333511
12020-01-02124.660004126.269997124.230003123.6377412708000AXP3-0.01621474.99418772.9501640.000000-66.666667100.00000073.97217673.972176
22020-01-02328.549988333.350006327.700012331.3485724544400BA3-0.00247174.81528973.27920845.641442-100.000000100.00000074.04724974.047249
32020-01-02149.000000150.550003147.979996145.3545843311900CAT3-0.00875874.65540873.33968635.63252081.41229857.73433973.99754773.997547
42020-01-0248.06000148.41999847.88000146.77604316708100CSCO30.03528175.29523873.11539163.681143166.66666714.77227174.20531574.205315
\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
dateopenhighlowclosevolumeticdaymacdboll_ubboll_lbrsi_30cci_30dx_30close_30_smaclose_60_smasentiment
02020-12-01121.010002123.470001120.010002122.536896128166800AAPL10.806734122.766675111.87038456.465653158.90316718.326964116.122535115.195506-0.6353855836609985
02020-12-01120.320000122.570000119.849998119.1527943584400AXP14.740563128.73544494.83625360.58759792.03795731.349381106.629814104.0782340.6005440884823487
02020-12-01214.309998218.089996213.000000213.00999515805200BA113.502710237.931128146.37187261.131542100.68603941.044077180.535001171.6700010.4103206537661357
02020-12-01175.389999176.570007172.940002171.5675052710200CAT13.661695179.348927158.51112558.33680576.05040120.330658166.053826158.454731-0.5389422727751916
02020-12-0143.00999844.07000043.00999842.88230523948800CSCO11.18812944.34145835.25968060.649269130.29084749.93365938.80024538.741337-0.5830288155235768
02020-12-0189.27999989.70999987.07000086.2310799915700CVX14.37722097.00439666.73957057.24269676.67054721.59713077.45985774.967448-0.01776743709505868
02020-12-0164.83999665.23000363.33000263.2567524505200DD11.48660065.66050257.52965557.49034098.27908632.25434760.31839158.625112-0.5216685812161448
02020-12-01149.570007151.399994149.000000149.4400028827800DIS15.628248157.252273123.43872762.437060103.34479848.584574134.772000130.784000-0.9259336746217759
02020-12-01231.960007234.869995231.350006231.1719672581000GS18.068587242.898599193.54926462.155696104.80084539.014700210.773879204.969028-0.5188158197824897
02020-12-01278.730011278.950012275.549988273.3868413944900HD1-0.821353281.941664263.46409550.7896109.79427610.655830272.877826274.1366990.3704807831218777
02020-12-01123.900002125.830002123.080002121.5359345312100IBM11.976144124.757566107.83912354.877749148.65742426.069621114.076892116.8027970.7529713903986068
02020-12-0148.75000050.23000048.70999949.26099057778200INTC1-0.01352248.27575743.37156352.876832102.25944827.59026046.28806748.4349490.8539701717421719
02020-12-01146.289993149.130005145.860001146.5362409741300JNJ10.413395150.711438136.97493953.59871277.66461113.681658142.521038144.0389860.5682866582278816
02020-12-01120.339996121.580002119.629997118.18773712678200JPM14.596265125.82227899.95934361.12895888.00111334.500062108.128854102.435144-0.42503771747413355
02020-12-0152.13999952.33000251.79000151.61042818969300KO10.63672954.59058948.33129555.71884031.8804616.22613750.50442249.749719-0.9479379522530356
02020-12-01218.880005218.929993215.529999214.8182684226300MCD1-0.312211217.704911209.45703851.036825-14.47308613.648295215.366526217.2087240.8792847797333472
02020-12-01174.220001175.690002170.009995169.1263123859600MMM12.736254178.808849156.94003953.90095856.93591414.206497165.924518164.1045550.11072713391558753
02020-12-0180.94999782.45999980.82000080.2063459701500MRK10.22344080.88438077.17080352.86594293.67102726.95243178.13209379.471521-0.8812162075634768
02020-12-01214.509995217.320007213.350006215.71318130931300MSFT10.572630222.105612205.95317952.64852647.0347062.171538212.213684210.3316320.36688784032438826
02020-12-01136.440002136.500000134.750000134.8937993834500NKE12.423039136.936074123.74539963.290424135.95760331.371592128.780886125.8324910.42112365937297436
02020-12-0139.40000240.50000039.00999838.98588672660800PFE10.81774238.41987433.40441763.351960231.91306651.17387835.33626334.6389910.72625802410846
02020-12-01139.160004139.539993138.160004137.6536717169700PG1-0.400841142.396651135.10930151.280113-59.45788922.114259138.675353137.8165530.6270024530508409
02020-12-0172.72000172.94000271.20999970.7943886545800RTX13.49878878.38692654.99756659.65043688.20371827.28324463.53305761.308451-0.6990874391243174
02020-12-01132.509995134.240005131.990005132.5716251218300TRV13.146130139.547557124.33765358.09704547.0678959.827462128.519917119.7096330.7552892014347639
02020-12-01344.769989354.100006339.929993338.7633363817700UNH12.557991361.838079322.07288055.30268946.42320123.169929333.051594321.247131-0.8165491584577971
02020-12-01212.130005213.669998211.039993210.8726208049400V13.227687220.609626192.51779656.04146674.85627016.239753201.096742201.011629-0.502620224611146
02020-12-0160.43000060.91999860.27999959.30035014283200VZ10.53120961.01706556.49708157.47782264.12240821.12465257.84312857.7903850.6591081344198431
02020-12-0138.38000139.20000138.31000138.0968178004500WBA10.39060742.49083034.67339051.24222621.8655383.17293437.52004936.399741-0.5569183988036852
02020-12-01153.600006153.660004151.660004151.4517367647100WMT12.550562154.726389140.38224161.146359104.40221243.725999145.329107141.988320-0.3113203205386308
02020-12-0138.95999939.65000238.47000137.85710132503100XOM11.46327641.85673130.66434153.97548784.35562221.82800834.83760834.328411-0.14096433016302212
\n
" }, "metadata": {}, - "execution_count": 23 + "execution_count": 75 } ], "source": [ - "numerical_df.head()" + "e_trade_gym.data" + ] + }, + { + "source": [ + "## Trading" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize agent\n", + "agent = DRLAgent(env_trade)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 63, "metadata": {}, "outputs": [ { - "output_type": "error", - "ename": "NameError", - "evalue": "name 'training' is not defined", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mtraining\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain_one\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mNameError\u001b[0m: name 'training' is not defined" + "output_type": "stream", + "name": "stdout", + "text": [ + "{'n_steps': 5, 'ent_coef': 0.01, 'learning_rate': 0.0007}\nUsing cpu device\n" ] } ], - "source": [] + "source": [ + "# Initialize Model\n", + "model_a2c = agent.get_model(\"a2c\")" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [], + "source": [ + "# Load or train\n", + "path_to_saved_model = ''\n", + "model_a2c.load(path_to_saved_model)" + ] }, { "cell_type": "code", diff --git a/preprocessing/data_processor.py b/preprocessing/data_processor.py new file mode 100644 index 000000000..14562d746 --- /dev/null +++ b/preprocessing/data_processor.py @@ -0,0 +1,82 @@ +from __future__ import division, absolute_import, print_function +import numpy as np +import pandas as pd +import datetime +from finrl.config import config +from finrl.marketdata.yahoodownloader import YahooDownloader +from finrl.preprocessing.preprocessors import FeatureEngineer +from finrl.preprocessing.data import data_split + + +class DataProcessor: + + def __init__(self,feature_engineer, initial_data, buffer_size=30,numerical_cols = ['date','open', 'high', 'low', 'close', 'volume', 'tic','day']): + self.numerical_cols = numerical_cols + self.fe = feature_engineer + self.buffer_size = buffer_size + initial_indices = initial_data.index.unique() + initial_data.index = initial_data.date.factorize()[0] + self.numerical_data_history = initial_data[self.numerical_cols] + if len(initial_indices) > buffer_size: + self.numerical_data_history = self.numerical_data_history.loc[initial_indices[-buffer_size:]] + + def _add_new(self,df): + if len(self.numerical_data_history.index.unique()) >= self.buffer_size: + self.numerical_data_history.drop(self.numerical_data_history.index[0],inplace=True) + df.set_index(pd.Index(np.full((len(df),),self.numerical_data_history.index[-1]+1)),inplace=True) + self.numerical_data_history = self.numerical_data_history.append(df) + return self.numerical_data_history + + def process_data(self,numerical_df,sentiment_df): + new_feature_df = self.compute_technical_indicators(numerical_df) + new_df = new_feature_df.reset_index().merge(sentiment_df,on=['date','tic']).set_index('index') + return new_df + + def compute_technical_indicators(self,numerical_df): + full_df = self._add_new(numerical_df) + feature_df = self.fe.preprocess_data(full_df) + feature_df.index = feature_df.date.factorize()[0] + new_feature_df = feature_df.loc[feature_df.index[-1]] + return new_feature_df + + def save_to_database(self): + pass + + + +def get_initial_data(numerical_df,sentiment_df,use_turbulence=False): + fe = FeatureEngineer(use_turbulence=use_turbulence) + numerical_df = fe.preprocess_data(numerical_df) + df = numerical_df.merge(sentiment_df,on=["date","tic"],how="left") + df.fillna(0) + return df + +def generate_sentiment_scores(start_date,end_date,tickers=config.DOW_30_TICKER,time_fmt="%Y-%m-%d"): + dates = pd.date_range(start_date,end_date).to_pydatetime() + dates = np.array([datetime.datetime.strftime(r,time_fmt) for r in dates]) + data = np.array(np.meshgrid(dates,tickers)).T.reshape(-1,2) + scores = np.random.uniform(low=-1.0,high=1.0,size=(len(data),1)) + df = pd.DataFrame(data,columns=['date','tic']) + df['sentiment'] = scores + return df + +def test_process_data(): + start_date = '2020-11-01' + end_date='2021-01-01' + ticker_list=config.DOW_30_TICKER + numerical_df = YahooDownloader(start_date=start_date,end_date=end_date,ticker_list=ticker_list).fetch_data() + sentiment_df = generate_sentiment_scores(start_date,end_date) + initial_data = get_initial_data(numerical_df,sentiment_df) + trade_data = data_split(initial_data,start_date,'2020-12-01') + numerical_feed_data = numerical_df[numerical_df.date > '2020-12-01'] + sentiment_feed_data = sentiment_df[sentiment_df.date > '2020-12-01'] + data_processor = DataProcessor(FeatureEngineer(),trade_data) + for date in numerical_feed_data.date.unique(): + + new_numerical = numerical_feed_data[numerical_feed_data.date==date] + new_sentiment = sentiment_feed_data.loc[sentiment_feed_data.date==date] + new_df=data_processor.process_data(new_numerical,new_sentiment) + print(new_df) + +if __name__ == "__main__": + test_process_data() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index def79b22b..92cc820a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Model Building Requirements -numpy==1.16.4 +numpy pandas==1.0.3 stockstats scikit-learn==0.21.0 From 914166aba3745724774e91fa0ff9158316cc7584 Mon Sep 17 00:00:00 2001 From: Rick Gentry Date: Tue, 27 Apr 2021 21:43:26 -0600 Subject: [PATCH 04/12] cleaning up --- .gitignore | 5 +- env/EnvMultipleStock_trade.py | 255 ------------ env/EnvMultipleStock_train.py | 197 --------- env/EnvMultipleStock_validation.py | 225 ---------- env/env_onlinestocktrading.py | 66 +++ env/env_stocks.py | 388 ++++++++++++++++++ model/online_stock_prediction.py | 39 +- preprocessing/preprocessors.py | 161 -------- .../Sentiment_model.py | 0 9 files changed, 464 insertions(+), 872 deletions(-) delete mode 100644 env/EnvMultipleStock_trade.py delete mode 100644 env/EnvMultipleStock_train.py delete mode 100644 env/EnvMultipleStock_validation.py create mode 100644 env/env_onlinestocktrading.py create mode 100644 env/env_stocks.py delete mode 100644 preprocessing/preprocessors.py rename Sentiment_model.py => sentiment_analysis/Sentiment_model.py (100%) diff --git a/.gitignore b/.gitignore index a6297371d..e53cf95df 100644 --- a/.gitignore +++ b/.gitignore @@ -114,9 +114,8 @@ celerybeat.pid # Environments .env .venv -env/ venv/ -ENV/ +#ENV/ env.bak/ venv.bak/ @@ -138,5 +137,3 @@ dmypy.json # Pyre type checker .pyre/ -# FinRL library -src \ No newline at end of file diff --git a/env/EnvMultipleStock_trade.py b/env/EnvMultipleStock_trade.py deleted file mode 100644 index 38abff51c..000000000 --- a/env/EnvMultipleStock_trade.py +++ /dev/null @@ -1,255 +0,0 @@ -import numpy as np -import pandas as pd -from gym.utils import seeding -import gym -from gym import spaces -import matplotlib -matplotlib.use('Agg') -import matplotlib.pyplot as plt -import pickle - -# shares normalization factor -# 100 shares per trade -HMAX_NORMALIZE = 100 -# initial amount of money we have in our account -INITIAL_ACCOUNT_BALANCE=1000000 -# total number of stocks in our portfolio -STOCK_DIM = 30 -# transaction fee: 1/1000 reasonable percentage -TRANSACTION_FEE_PERCENT = 0.001 - -# turbulence index: 90-150 reasonable threshold -#TURBULENCE_THRESHOLD = 140 -REWARD_SCALING = 1e-4 - -class StockEnvTrade(gym.Env): - """A stock trading environment for OpenAI gym""" - metadata = {'render.modes': ['human']} - - def __init__(self, df,day = 0,turbulence_threshold=140 - ,initial=True, previous_state=[], model_name='', iteration=''): - #super(StockEnv, self).__init__() - #money = 10 , scope = 1 - self.day = day - self.df = df - self.initial = initial - self.previous_state = previous_state - # action_space normalization and shape is STOCK_DIM - self.action_space = spaces.Box(low = -1, high = 1,shape = (STOCK_DIM,)) - # Shape = 181: [Current Balance]+[prices 1-30]+[owned shares 1-30] - # +[macd 1-30]+ [rsi 1-30] + [cci 1-30] + [adx 1-30] - self.observation_space = spaces.Box(low=0, high=np.inf, shape = (181,)) - # load data from a pandas dataframe - self.data = self.df.loc[self.day,:] - self.terminal = False - self.turbulence_threshold = turbulence_threshold - # initalize state - self.state = [INITIAL_ACCOUNT_BALANCE] + \ - self.data.adjcp.values.tolist() + \ - [0]*STOCK_DIM + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - # initialize reward - self.reward = 0 - self.turbulence = 0 - self.cost = 0 - self.trades = 0 - # memorize all the total balance change - self.asset_memory = [INITIAL_ACCOUNT_BALANCE] - self.rewards_memory = [] - #self.reset() - self._seed() - self.model_name=model_name - self.iteration=iteration - - - def _sell_stock(self, index, action): - # perform sell action based on the sign of the action - if self.turbulence 0: - #update balance - self.state[0] += \ - self.state[index+1]*min(abs(action),self.state[index+STOCK_DIM+1]) * \ - (1- TRANSACTION_FEE_PERCENT) - - self.state[index+STOCK_DIM+1] -= min(abs(action), self.state[index+STOCK_DIM+1]) - self.cost +=self.state[index+1]*min(abs(action),self.state[index+STOCK_DIM+1]) * \ - TRANSACTION_FEE_PERCENT - self.trades+=1 - else: - pass - else: - # if turbulence goes over threshold, just clear out all positions - if self.state[index+STOCK_DIM+1] > 0: - #update balance - self.state[0] += self.state[index+1]*self.state[index+STOCK_DIM+1]* \ - (1- TRANSACTION_FEE_PERCENT) - self.state[index+STOCK_DIM+1] =0 - self.cost += self.state[index+1]*self.state[index+STOCK_DIM+1]* \ - TRANSACTION_FEE_PERCENT - self.trades+=1 - else: - pass - - def _buy_stock(self, index, action): - # perform buy action based on the sign of the action - if self.turbulence< self.turbulence_threshold: - available_amount = self.state[0] // self.state[index+1] - # print('available_amount:{}'.format(available_amount)) - - #update balance - self.state[0] -= self.state[index+1]*min(available_amount, action)* \ - (1+ TRANSACTION_FEE_PERCENT) - - self.state[index+STOCK_DIM+1] += min(available_amount, action) - - self.cost+=self.state[index+1]*min(available_amount, action)* \ - TRANSACTION_FEE_PERCENT - self.trades+=1 - else: - # if turbulence goes over threshold, just stop buying - pass - - def step(self, actions): - # print(self.day) - self.terminal = self.day >= len(self.df.index.unique())-1 - # print(actions) - - if self.terminal: - plt.plot(self.asset_memory,'r') - plt.savefig('results/account_value_trade_{}_{}.png'.format(self.model_name, self.iteration)) - plt.close() - df_total_value = pd.DataFrame(self.asset_memory) - df_total_value.to_csv('results/account_value_trade_{}_{}.csv'.format(self.model_name, self.iteration)) - end_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - print("previous_total_asset:{}".format(self.asset_memory[0])) - - print("end_total_asset:{}".format(end_total_asset)) - print("total_reward:{}".format(self.state[0]+sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)]))- self.asset_memory[0] )) - print("total_cost: ", self.cost) - print("total trades: ", self.trades) - - df_total_value.columns = ['account_value'] - df_total_value['daily_return']=df_total_value.pct_change(1) - sharpe = (4**0.5)*df_total_value['daily_return'].mean()/ \ - df_total_value['daily_return'].std() - print("Sharpe: ",sharpe) - - df_rewards = pd.DataFrame(self.rewards_memory) - df_rewards.to_csv('results/account_rewards_trade_{}_{}.csv'.format(self.model_name, self.iteration)) - - # print('total asset: {}'.format(self.state[0]+ sum(np.array(self.state[1:29])*np.array(self.state[29:])))) - #with open('obs.pkl', 'wb') as f: - # pickle.dump(self.state, f) - - return self.state, self.reward, self.terminal,{} - - else: - # print(np.array(self.state[1:29])) - - actions = actions * HMAX_NORMALIZE - #actions = (actions.astype(int)) - if self.turbulence>=self.turbulence_threshold: - actions=np.array([-HMAX_NORMALIZE]*STOCK_DIM) - - begin_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - #print("begin_total_asset:{}".format(begin_total_asset)) - - argsort_actions = np.argsort(actions) - - sell_index = argsort_actions[:np.where(actions < 0)[0].shape[0]] - buy_index = argsort_actions[::-1][:np.where(actions > 0)[0].shape[0]] - - for index in sell_index: - # print('take sell action'.format(actions[index])) - self._sell_stock(index, actions[index]) - - for index in buy_index: - # print('take buy action: {}'.format(actions[index])) - self._buy_stock(index, actions[index]) - - self.day += 1 - self.data = self.df.loc[self.day,:] - self.turbulence = self.data['turbulence'].values[0] - #print(self.turbulence) - #load next state - # print("stock_shares:{}".format(self.state[29:])) - self.state = [self.state[0]] + \ - self.data.adjcp.values.tolist() + \ - list(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)]) + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - - end_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - self.asset_memory.append(end_total_asset) - #print("end_total_asset:{}".format(end_total_asset)) - - self.reward = end_total_asset - begin_total_asset - # print("step_reward:{}".format(self.reward)) - self.rewards_memory.append(self.reward) - - self.reward = self.reward*REWARD_SCALING - - - return self.state, self.reward, self.terminal, {} - - def reset(self): - if self.initial: - self.asset_memory = [INITIAL_ACCOUNT_BALANCE] - self.day = 0 - self.data = self.df.loc[self.day,:] - self.turbulence = 0 - self.cost = 0 - self.trades = 0 - self.terminal = False - #self.iteration=self.iteration - self.rewards_memory = [] - #initiate state - self.state = [INITIAL_ACCOUNT_BALANCE] + \ - self.data.adjcp.values.tolist() + \ - [0]*STOCK_DIM + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - else: - previous_total_asset = self.previous_state[0]+ \ - sum(np.array(self.previous_state[1:(STOCK_DIM+1)])*np.array(self.previous_state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - self.asset_memory = [previous_total_asset] - #self.asset_memory = [self.previous_state[0]] - self.day = 0 - self.data = self.df.loc[self.day,:] - self.turbulence = 0 - self.cost = 0 - self.trades = 0 - self.terminal = False - #self.iteration=iteration - self.rewards_memory = [] - #initiate state - #self.previous_state[(STOCK_DIM+1):(STOCK_DIM*2+1)] - #[0]*STOCK_DIM + \ - - self.state = [ self.previous_state[0]] + \ - self.data.adjcp.values.tolist() + \ - self.previous_state[(STOCK_DIM+1):(STOCK_DIM*2+1)]+ \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - - return self.state - - def render(self, mode='human',close=False): - return self.state - - - def _seed(self, seed=None): - self.np_random, seed = seeding.np_random(seed) - return [seed] \ No newline at end of file diff --git a/env/EnvMultipleStock_train.py b/env/EnvMultipleStock_train.py deleted file mode 100644 index c4096e0d5..000000000 --- a/env/EnvMultipleStock_train.py +++ /dev/null @@ -1,197 +0,0 @@ -import numpy as np -import pandas as pd -from gym.utils import seeding -import gym -from gym import spaces -import matplotlib -matplotlib.use('Agg') -import matplotlib.pyplot as plt -import pickle - -# shares normalization factor -# 100 shares per trade -HMAX_NORMALIZE = 100 -# initial amount of money we have in our account -INITIAL_ACCOUNT_BALANCE=1000000 -# total number of stocks in our portfolio -STOCK_DIM = 30 -# transaction fee: 1/1000 reasonable percentage -TRANSACTION_FEE_PERCENT = 0.001 -REWARD_SCALING = 1e-4 - -class StockEnvTrain(gym.Env): - """A stock trading environment for OpenAI gym""" - metadata = {'render.modes': ['human']} - - def __init__(self, df,day = 0): - #super(StockEnv, self).__init__() - #money = 10 , scope = 1 - self.day = day - self.df = df - - # action_space normalization and shape is STOCK_DIM - self.action_space = spaces.Box(low = -1, high = 1,shape = (STOCK_DIM,)) - # Shape = 181: [Current Balance]+[prices 1-30]+[owned shares 1-30] - # +[macd 1-30]+ [rsi 1-30] + [cci 1-30] + [adx 1-30] - self.observation_space = spaces.Box(low=0, high=np.inf, shape = (181,)) - # load data from a pandas dataframe - self.data = self.df.loc[self.day,:] - self.terminal = False - # initalize state - self.state = [INITIAL_ACCOUNT_BALANCE] + \ - self.data.adjcp.values.tolist() + \ - [0]*STOCK_DIM + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - # initialize reward - self.reward = 0 - self.cost = 0 - # memorize all the total balance change - self.asset_memory = [INITIAL_ACCOUNT_BALANCE] - self.rewards_memory = [] - self.trades = 0 - #self.reset() - self._seed() - - - def _sell_stock(self, index, action): - # perform sell action based on the sign of the action - if self.state[index+STOCK_DIM+1] > 0: - #update balance - self.state[0] += \ - self.state[index+1]*min(abs(action),self.state[index+STOCK_DIM+1]) * \ - (1- TRANSACTION_FEE_PERCENT) - - self.state[index+STOCK_DIM+1] -= min(abs(action), self.state[index+STOCK_DIM+1]) - self.cost +=self.state[index+1]*min(abs(action),self.state[index+STOCK_DIM+1]) * \ - TRANSACTION_FEE_PERCENT - self.trades+=1 - else: - pass - - - def _buy_stock(self, index, action): - # perform buy action based on the sign of the action - available_amount = self.state[0] // self.state[index+1] - # print('available_amount:{}'.format(available_amount)) - - #update balance - self.state[0] -= self.state[index+1]*min(available_amount, action)* \ - (1+ TRANSACTION_FEE_PERCENT) - - self.state[index+STOCK_DIM+1] += min(available_amount, action) - - self.cost+=self.state[index+1]*min(available_amount, action)* \ - TRANSACTION_FEE_PERCENT - self.trades+=1 - - def step(self, actions): - # print(self.day) - self.terminal = self.day >= len(self.df.index.unique())-1 - # print(actions) - - if self.terminal: - plt.plot(self.asset_memory,'r') - plt.savefig('results/account_value_train.png') - plt.close() - end_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - - #print("end_total_asset:{}".format(end_total_asset)) - df_total_value = pd.DataFrame(self.asset_memory) - df_total_value.to_csv('results/account_value_train.csv') - #print("total_reward:{}".format(self.state[0]+sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):61]))- INITIAL_ACCOUNT_BALANCE )) - #print("total_cost: ", self.cost) - #print("total_trades: ", self.trades) - df_total_value.columns = ['account_value'] - df_total_value['daily_return']=df_total_value.pct_change(1) - sharpe = (252**0.5)*df_total_value['daily_return'].mean()/ \ - df_total_value['daily_return'].std() - #print("Sharpe: ",sharpe) - #print("=================================") - df_rewards = pd.DataFrame(self.rewards_memory) - #df_rewards.to_csv('results/account_rewards_train.csv') - - # print('total asset: {}'.format(self.state[0]+ sum(np.array(self.state[1:29])*np.array(self.state[29:])))) - #with open('obs.pkl', 'wb') as f: - # pickle.dump(self.state, f) - - return self.state, self.reward, self.terminal,{} - - else: - # print(np.array(self.state[1:29])) - - actions = actions * HMAX_NORMALIZE - #actions = (actions.astype(int)) - - begin_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - #print("begin_total_asset:{}".format(begin_total_asset)) - - argsort_actions = np.argsort(actions) - - sell_index = argsort_actions[:np.where(actions < 0)[0].shape[0]] - buy_index = argsort_actions[::-1][:np.where(actions > 0)[0].shape[0]] - - for index in sell_index: - # print('take sell action'.format(actions[index])) - self._sell_stock(index, actions[index]) - - for index in buy_index: - # print('take buy action: {}'.format(actions[index])) - self._buy_stock(index, actions[index]) - - self.day += 1 - self.data = self.df.loc[self.day,:] - #load next state - # print("stock_shares:{}".format(self.state[29:])) - self.state = [self.state[0]] + \ - self.data.adjcp.values.tolist() + \ - list(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)]) + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - - end_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - self.asset_memory.append(end_total_asset) - #print("end_total_asset:{}".format(end_total_asset)) - - self.reward = end_total_asset - begin_total_asset - # print("step_reward:{}".format(self.reward)) - self.rewards_memory.append(self.reward) - - self.reward = self.reward*REWARD_SCALING - - - - return self.state, self.reward, self.terminal, {} - - def reset(self): - self.asset_memory = [INITIAL_ACCOUNT_BALANCE] - self.day = 0 - self.data = self.df.loc[self.day,:] - self.cost = 0 - self.trades = 0 - self.terminal = False - self.rewards_memory = [] - #initiate state - self.state = [INITIAL_ACCOUNT_BALANCE] + \ - self.data.adjcp.values.tolist() + \ - [0]*STOCK_DIM + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - # iteration += 1 - return self.state - - def render(self, mode='human'): - return self.state - - def _seed(self, seed=None): - self.np_random, seed = seeding.np_random(seed) - return [seed] \ No newline at end of file diff --git a/env/EnvMultipleStock_validation.py b/env/EnvMultipleStock_validation.py deleted file mode 100644 index 390d46310..000000000 --- a/env/EnvMultipleStock_validation.py +++ /dev/null @@ -1,225 +0,0 @@ -import numpy as np -import pandas as pd -from gym.utils import seeding -import gym -from gym import spaces -import matplotlib -matplotlib.use('Agg') -import matplotlib.pyplot as plt -import pickle - -# shares normalization factor -# 100 shares per trade -HMAX_NORMALIZE = 100 -# initial amount of money we have in our account -INITIAL_ACCOUNT_BALANCE=1000000 -# total number of stocks in our portfolio -STOCK_DIM = 30 -# transaction fee: 1/1000 reasonable percentage -TRANSACTION_FEE_PERCENT = 0.001 - -# turbulence index: 90-150 reasonable threshold -#TURBULENCE_THRESHOLD = 140 -REWARD_SCALING = 1e-4 - -class StockEnvValidation(gym.Env): - """A stock trading environment for OpenAI gym""" - metadata = {'render.modes': ['human']} - - def __init__(self, df, day = 0, turbulence_threshold=140, iteration=''): - #super(StockEnv, self).__init__() - #money = 10 , scope = 1 - self.day = day - self.df = df - # action_space normalization and shape is STOCK_DIM - self.action_space = spaces.Box(low = -1, high = 1,shape = (STOCK_DIM,)) - # Shape = 181: [Current Balance]+[prices 1-30]+[owned shares 1-30] - # +[macd 1-30]+ [rsi 1-30] + [cci 1-30] + [adx 1-30] - self.observation_space = spaces.Box(low=0, high=np.inf, shape = (181,)) - # load data from a pandas dataframe - self.data = self.df.loc[self.day,:] - self.terminal = False - self.turbulence_threshold = turbulence_threshold - # initalize state - self.state = [INITIAL_ACCOUNT_BALANCE] + \ - self.data.adjcp.values.tolist() + \ - [0]*STOCK_DIM + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - # initialize reward - self.reward = 0 - self.turbulence = 0 - self.cost = 0 - self.trades = 0 - # memorize all the total balance change - self.asset_memory = [INITIAL_ACCOUNT_BALANCE] - self.rewards_memory = [] - #self.reset() - self._seed() - - self.iteration=iteration - - - def _sell_stock(self, index, action): - # perform sell action based on the sign of the action - if self.turbulence 0: - #update balance - self.state[0] += \ - self.state[index+1]*min(abs(action),self.state[index+STOCK_DIM+1]) * \ - (1- TRANSACTION_FEE_PERCENT) - - self.state[index+STOCK_DIM+1] -= min(abs(action), self.state[index+STOCK_DIM+1]) - self.cost +=self.state[index+1]*min(abs(action),self.state[index+STOCK_DIM+1]) * \ - TRANSACTION_FEE_PERCENT - self.trades+=1 - else: - pass - else: - # if turbulence goes over threshold, just clear out all positions - if self.state[index+STOCK_DIM+1] > 0: - #update balance - self.state[0] += self.state[index+1]*self.state[index+STOCK_DIM+1]* \ - (1- TRANSACTION_FEE_PERCENT) - self.state[index+STOCK_DIM+1] =0 - self.cost += self.state[index+1]*self.state[index+STOCK_DIM+1]* \ - TRANSACTION_FEE_PERCENT - self.trades+=1 - else: - pass - - def _buy_stock(self, index, action): - # perform buy action based on the sign of the action - if self.turbulence< self.turbulence_threshold: - available_amount = self.state[0] // self.state[index+1] - # print('available_amount:{}'.format(available_amount)) - - #update balance - self.state[0] -= self.state[index+1]*min(available_amount, action)* \ - (1+ TRANSACTION_FEE_PERCENT) - - self.state[index+STOCK_DIM+1] += min(available_amount, action) - - self.cost+=self.state[index+1]*min(available_amount, action)* \ - TRANSACTION_FEE_PERCENT - self.trades+=1 - else: - # if turbulence goes over threshold, just stop buying - pass - - def step(self, actions): - # print(self.day) - self.terminal = self.day >= len(self.df.index.unique())-1 - # print(actions) - - if self.terminal: - plt.plot(self.asset_memory,'r') - plt.savefig('results/account_value_validation_{}.png'.format(self.iteration)) - plt.close() - df_total_value = pd.DataFrame(self.asset_memory) - df_total_value.to_csv('results/account_value_validation_{}.csv'.format(self.iteration)) - end_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - #print("previous_total_asset:{}".format(self.asset_memory[0])) - - #print("end_total_asset:{}".format(end_total_asset)) - #print("total_reward:{}".format(self.state[0]+sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):61]))- self.asset_memory[0] )) - #print("total_cost: ", self.cost) - #print("total trades: ", self.trades) - - df_total_value.columns = ['account_value'] - df_total_value['daily_return']=df_total_value.pct_change(1) - sharpe = (4**0.5)*df_total_value['daily_return'].mean()/ \ - df_total_value['daily_return'].std() - #print("Sharpe: ",sharpe) - - #df_rewards = pd.DataFrame(self.rewards_memory) - #df_rewards.to_csv('results/account_rewards_trade_{}.csv'.format(self.iteration)) - - # print('total asset: {}'.format(self.state[0]+ sum(np.array(self.state[1:29])*np.array(self.state[29:])))) - #with open('obs.pkl', 'wb') as f: - # pickle.dump(self.state, f) - - return self.state, self.reward, self.terminal,{} - - else: - # print(np.array(self.state[1:29])) - - actions = actions * HMAX_NORMALIZE - #actions = (actions.astype(int)) - if self.turbulence>=self.turbulence_threshold: - actions=np.array([-HMAX_NORMALIZE]*STOCK_DIM) - begin_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - #print("begin_total_asset:{}".format(begin_total_asset)) - - argsort_actions = np.argsort(actions) - - sell_index = argsort_actions[:np.where(actions < 0)[0].shape[0]] - buy_index = argsort_actions[::-1][:np.where(actions > 0)[0].shape[0]] - - for index in sell_index: - # print('take sell action'.format(actions[index])) - self._sell_stock(index, actions[index]) - - for index in buy_index: - # print('take buy action: {}'.format(actions[index])) - self._buy_stock(index, actions[index]) - - self.day += 1 - self.data = self.df.loc[self.day,:] - self.turbulence = self.data['turbulence'].values[0] - #print(self.turbulence) - #load next state - # print("stock_shares:{}".format(self.state[29:])) - self.state = [self.state[0]] + \ - self.data.adjcp.values.tolist() + \ - list(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)]) + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - - end_total_asset = self.state[0]+ \ - sum(np.array(self.state[1:(STOCK_DIM+1)])*np.array(self.state[(STOCK_DIM+1):(STOCK_DIM*2+1)])) - self.asset_memory.append(end_total_asset) - #print("end_total_asset:{}".format(end_total_asset)) - - self.reward = end_total_asset - begin_total_asset - # print("step_reward:{}".format(self.reward)) - self.rewards_memory.append(self.reward) - - self.reward = self.reward*REWARD_SCALING - - return self.state, self.reward, self.terminal, {} - - def reset(self): - self.asset_memory = [INITIAL_ACCOUNT_BALANCE] - self.day = 0 - self.data = self.df.loc[self.day,:] - self.turbulence = 0 - self.cost = 0 - self.trades = 0 - self.terminal = False - #self.iteration=self.iteration - self.rewards_memory = [] - #initiate state - self.state = [INITIAL_ACCOUNT_BALANCE] + \ - self.data.adjcp.values.tolist() + \ - [0]*STOCK_DIM + \ - self.data.macd.values.tolist() + \ - self.data.rsi.values.tolist() + \ - self.data.cci.values.tolist() + \ - self.data.adx.values.tolist() - - return self.state - - def render(self, mode='human',close=False): - return self.state - - - def _seed(self, seed=None): - self.np_random, seed = seeding.np_random(seed) - return [seed] \ No newline at end of file diff --git a/env/env_onlinestocktrading.py b/env/env_onlinestocktrading.py new file mode 100644 index 000000000..2719be56b --- /dev/null +++ b/env/env_onlinestocktrading.py @@ -0,0 +1,66 @@ +import numpy as np +import pandas as pd +from gym.utils import seeding +import gym +from gym import spaces +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import pickle +from stable_baselines3.common.vec_env import DummyVecEnv +from stable_baselines3.common import logger +from env_stocks import StockEnv + +class OnlineStockTradingEnv(StockEnv): + """A stock trading environment for OpenAI gym""" + metadata = {'render.modes': ['human']} + + def __init__(self, + initial_data, + stock_dim, + hmax, + initial_amount, + buy_cost_pct, + sell_cost_pct, + reward_scaling, + state_space, + action_space, + tech_indicator_list, + turbulence_threshold=None, + make_plots = False, + print_verbosity = 10, + day = 0, + initial=True, + previous_state=[], + model_name = '', + mode='', + iteration=''): + + super().__init__(initial_data,stock_dim,hmax,initial_amount, + buy_cost_pct, + sell_cost_pct, + reward_scaling, + state_space, + action_space, + tech_indicator_list, + turbulence_threshold=None, + make_plots = False, + print_verbosity = 10, + day = 0, + initial=True, + previous_state=[], + model_name = '', + mode='', + iteration='') + + self.data_history = initial_data + + + + + def _update_data(self,new_df): + self.data = new_df + self.data_history = self.data_history.append(new_df) + + + diff --git a/env/env_stocks.py b/env/env_stocks.py new file mode 100644 index 000000000..6b4fc28fc --- /dev/null +++ b/env/env_stocks.py @@ -0,0 +1,388 @@ +import numpy as np +import pandas as pd +from gym.utils import seeding +import gym +from gym import spaces +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import pickle +from stable_baselines3.common.vec_env import DummyVecEnv +from stable_baselines3.common import logger + + +class StockEnv(gym.Env): + """A stock trading environment for OpenAI gym""" + metadata = {'render.modes': ['human']} + + def __init__(self, + initial_data, + stock_dim, + hmax, + initial_amount, + buy_cost_pct, + sell_cost_pct, + reward_scaling, + state_space, + action_space, + tech_indicator_list, + turbulence_threshold=None, + make_plots = False, + print_verbosity = 10, + day = 0, + initial=True, + previous_state=[], + model_name = '', + mode='', + iteration=''): + + # How many days trading for + self.day = day + self.initial_data = initial_data + self.data = initial_data + # Number of stocks you are considering + self.stock_dim = stock_dim + # Max number of a single stock you can trade + self.hmax = hmax + # Initial amount to invest + self.initial_amount = initial_amount + # Transaction costs: Can set these based on real life + self.buy_cost_pct = buy_cost_pct + self.sell_cost_pct = sell_cost_pct + # Something having to do with training + self.reward_scaling = reward_scaling + # The dimension of the state space: 1 + 2*stock_dimension + len(config.TECHNICAL_INDICATORS_LIST)*stock_dimension + self.state_space = state_space + # Actions you can take depends on number of stocks and how many shares you can buy + self.action_space = action_space + self.tech_indicator_list = tech_indicator_list + self.action_space = spaces.Box(low = -1, high = 1,shape = (self.action_space,)) + self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape = (self.state_space,)) + + self.terminal = False + self.make_plots = make_plots + self.print_verbosity = print_verbosity + self.turbulence_threshold = turbulence_threshold + self.initial = initial + self.previous_state = previous_state + self.model_name=model_name + self.mode=mode + self.iteration=iteration + # initalize state + self.state = self._initiate_state() + + # initialize reward + self.reward = 0 + self.turbulence = 0 + self.cost = 0 + self.trades = 0 + self.episode = 0 + # memorize all the total balance change + self.asset_memory = [self.initial_amount] + self.rewards_memory = [] + self.actions_memory=[] + self.date_memory=[self._get_date()] + + self._seed() + + def _sell_stock(self, index, action): + def _do_sell_normal(): + if self.state[index+1]>0: + # Sell only if the price is > 0 (no missing data in this particular date) + # perform sell action based on the sign of the action + if self.state[index+self.stock_dim+1] > 0: + # Sell only if current asset is > 0 + sell_num_shares = min(abs(action),self.state[index+self.stock_dim+1]) + sell_amount = self.state[index+1] * sell_num_shares * (1- self.sell_cost_pct) + #update balance + self.state[0] += sell_amount + + self.state[index+self.stock_dim+1] -= sell_num_shares + self.cost +=self.state[index+1] * sell_num_shares * self.sell_cost_pct + self.trades+=1 + else: + sell_num_shares = 0 + else: + sell_num_shares = 0 + + return sell_num_shares + + # perform sell action based on the sign of the action + if self.turbulence_threshold is not None: + if self.turbulence>=self.turbulence_threshold: + if self.state[index+1]>0: + # Sell only if the price is > 0 (no missing data in this particular date) + # if turbulence goes over threshold, just clear out all positions + if self.state[index+self.stock_dim+1] > 0: + # Sell only if current asset is > 0 + sell_num_shares = self.state[index+self.stock_dim+1] + sell_amount = self.state[index+1]*sell_num_shares* (1- self.sell_cost_pct) + #update balance + self.state[0] += sell_amount + self.state[index+self.stock_dim+1] =0 + self.cost += self.state[index+1]*self.state[index+self.stock_dim+1]* \ + self.sell_cost_pct + self.trades+=1 + else: + sell_num_shares = 0 + else: + sell_num_shares = 0 + else: + sell_num_shares = _do_sell_normal() + else: + sell_num_shares = _do_sell_normal() + + return sell_num_shares + + def _buy_stock(self, index, action): + + def _do_buy(): + if self.state[index+1]>0: + #Buy only if the price is > 0 (no missing data in this particular date) + available_amount = self.state[0] // self.state[index+1] + # print('available_amount:{}'.format(available_amount)) + + #update balance + buy_num_shares = min(available_amount, action) + buy_amount = self.state[index+1] * buy_num_shares * (1+ self.buy_cost_pct) + self.state[0] -= buy_amount + + self.state[index+self.stock_dim+1] += buy_num_shares + + self.cost+=self.state[index+1] * buy_num_shares * self.buy_cost_pct + self.trades+=1 + else: + buy_num_shares = 0 + + return buy_num_shares + + # perform buy action based on the sign of the action + if self.turbulence_threshold is None: + buy_num_shares = _do_buy() + else: + if self.turbulence< self.turbulence_threshold: + buy_num_shares = _do_buy() + else: + buy_num_shares = 0 + pass + + return buy_num_shares + + def _make_plot(self): + plt.plot(self.asset_memory,'r') + plt.savefig('results/account_value_trade_{}.png'.format(self.episode)) + plt.close() + + def step(self, actions): + + if self.terminal: + # print(f"Episode: {self.episode}") + if self.make_plots: + self._make_plot() + end_total_asset = self.state[0]+ \ + sum(np.array(self.state[1:(self.stock_dim+1)])*np.array(self.state[(self.stock_dim+1):(self.stock_dim*2+1)])) + df_total_value = pd.DataFrame(self.asset_memory) + tot_reward = self.state[0]+sum(np.array(self.state[1:(self.stock_dim+1)])*np.array(self.state[(self.stock_dim+1):(self.stock_dim*2+1)]))- self.initial_amount + df_total_value.columns = ['account_value'] + df_total_value['date'] = self.date_memory + df_total_value['daily_return']=df_total_value['account_value'].pct_change(1) + if df_total_value['daily_return'].std() !=0: + sharpe = (252**0.5)*df_total_value['daily_return'].mean()/ \ + df_total_value['daily_return'].std() + df_rewards = pd.DataFrame(self.rewards_memory) + df_rewards.columns = ['account_rewards'] + df_rewards['date'] = self.date_memory[:-1] + if self.episode % self.print_verbosity == 0: + print(f"day: {self.day}, episode: {self.episode}") + print(f"begin_total_asset: {self.asset_memory[0]:0.2f}") + print(f"end_total_asset: {end_total_asset:0.2f}") + print(f"total_reward: {tot_reward:0.2f}") + print(f"total_cost: {self.cost:0.2f}") + print(f"total_trades: {self.trades}") + if df_total_value['daily_return'].std() != 0: + print(f"Sharpe: {sharpe:0.3f}") + print("=================================") + + if (self.model_name!='') and (self.mode!=''): + df_actions = self.save_action_memory() + df_actions.to_csv('results/actions_{}_{}_{}.csv'.format(self.mode,self.model_name, self.iteration)) + df_total_value.to_csv('results/account_value_{}_{}_{}.csv'.format(self.mode,self.model_name, self.iteration),index=False) + df_rewards.to_csv('results/account_rewards_{}_{}_{}.csv'.format(self.mode,self.model_name, self.iteration),index=False) + plt.plot(self.asset_memory,'r') + plt.savefig('results/account_value_{}_{}_{}.png'.format(self.mode,self.model_name, self.iteration),index=False) + plt.close() + + # Add outputs to logger interface + logger.record("environment/portfolio_value", end_total_asset) + logger.record("environment/total_reward", tot_reward) + logger.record("environment/total_reward_pct", (tot_reward / (end_total_asset - tot_reward)) * 100) + logger.record("environment/total_cost", self.cost) + logger.record("environment/total_trades", self.trades) + + return self.state, self.reward, self.terminal, {} + + else: + + actions = actions * self.hmax #actions initially is scaled between 0 to 1 + actions = (actions.astype(int)) #convert into integer because we can't by fraction of shares + if self.turbulence_threshold is not None: + if self.turbulence>=self.turbulence_threshold: + actions=np.array([-self.hmax]*self.stock_dim) + begin_total_asset = self.state[0]+ \ + sum(np.array(self.state[1:(self.stock_dim+1)])*np.array(self.state[(self.stock_dim+1):(self.stock_dim*2+1)])) + #print("begin_total_asset:{}".format(begin_total_asset)) + + argsort_actions = np.argsort(actions) + + sell_index = argsort_actions[:np.where(actions < 0)[0].shape[0]] + buy_index = argsort_actions[::-1][:np.where(actions > 0)[0].shape[0]] + + for index in sell_index: + # print(f"Num shares before: {self.state[index+self.stock_dim+1]}") + # print(f'take sell action before : {actions[index]}') + actions[index] = self._sell_stock(index, actions[index]) * (-1) + # print(f'take sell action after : {actions[index]}') + # print(f"Num shares after: {self.state[index+self.stock_dim+1]}") + + for index in buy_index: + # print('take buy action: {}'.format(actions[index])) + actions[index] = self._buy_stock(index, actions[index]) + + self.actions_memory.append(actions) + + self.day += 1 + + if self.turbulence_threshold is not None: + self.turbulence = self.data['turbulence'].values[0] + self.state = self._update_state() + + end_total_asset = self.state[0]+ \ + sum(np.array(self.state[1:(self.stock_dim+1)])*np.array(self.state[(self.stock_dim+1):(self.stock_dim*2+1)])) + self.asset_memory.append(end_total_asset) + self.date_memory.append(self._get_date()) + self.reward = end_total_asset - begin_total_asset + self.rewards_memory.append(self.reward) + self.reward = self.reward*self.reward_scaling + + return self.state, self.reward, self.terminal, {} + + + def reset(self): + #initiate state + self.state = self._initiate_state() + + if self.initial: + self.asset_memory = [self.initial_amount] + else: + previous_total_asset = self.previous_state[0]+ \ + sum(np.array(self.state[1:(self.stock_dim+1)])*np.array(self.previous_state[(self.stock_dim+1):(self.stock_dim*2+1)])) + self.asset_memory = [previous_total_asset] + + self.day = 0 + self.data = self.initial_data + self.turbulence = 0 + self.cost = 0 + self.trades = 0 + self.terminal = False + # self.iteration=self.iteration + self.rewards_memory = [] + self.actions_memory=[] + self.date_memory=[self._get_date()] + + self.episode+=1 + + return self.state + + def render(self, mode='human',close=False): + return self.state + + def _initiate_state(self): + if self.initial: + # For Initial State + if self.stock_dim>1: + # for multiple stock + state = [self.initial_amount] + \ + self.data.close.values.tolist() + \ + [0]*self.stock_dim + \ + sum([self.data[tech].values.tolist() for tech in self.tech_indicator_list ], []) + else: + # for single stock + state = [self.initial_amount] + \ + [self.data.close] + \ + [0]*self.stock_dim + \ + sum([[self.data[tech]] for tech in self.tech_indicator_list ], []) + else: + #Using Previous State + if self.stock_dim>1: + # for multiple stock + state = [self.previous_state[0]] + \ + self.data.close.values.tolist() + \ + self.previous_state[(self.stock_dim+1):(self.stock_dim*2+1)] + \ + sum([self.data[tech].values.tolist() for tech in self.tech_indicator_list ], []) + else: + # for single stock + state = [self.previous_state[0]] + \ + [self.data.close] + \ + self.previous_state[(self.stock_dim+1):(self.stock_dim*2+1)] + \ + sum([[self.data[tech]] for tech in self.tech_indicator_list ], []) + return state + + def _update_state(self): + if self.stock_dim>1: + # for multiple stock + state = [self.state[0]] + \ + self.data.close.values.tolist() + \ + list(self.state[(self.stock_dim+1):(self.stock_dim*2+1)]) + \ + sum([self.data[tech].values.tolist() for tech in self.tech_indicator_list ], []) + + else: + # for single stock + state = [self.state[0]] + \ + [self.data.close] + \ + list(self.state[(self.stock_dim+1):(self.stock_dim*2+1)]) + \ + sum([[self.data[tech]] for tech in self.tech_indicator_list ], []) + + return state + + def _get_date(self): + if self.stock_dim>1: + date = self.data.date.unique()[0] + else: + date = self.data.date + return date + + def save_asset_memory(self): + date_list = self.date_memory + asset_list = self.asset_memory + #print(len(date_list)) + #print(len(asset_list)) + df_account_value = pd.DataFrame({'date':date_list,'account_value':asset_list}) + return df_account_value + + def save_action_memory(self): + if self.stock_dim>1: + # date and close price length must match actions length + date_list = self.date_memory[:-1] + df_date = pd.DataFrame(date_list) + df_date.columns = ['date'] + + action_list = self.actions_memory + df_actions = pd.DataFrame(action_list) + df_actions.columns = self.data.tic.values + df_actions.index = df_date.date + #df_actions = pd.DataFrame({'date':date_list,'actions':action_list}) + else: + date_list = self.date_memory[:-1] + action_list = self.actions_memory + df_actions = pd.DataFrame({'date':date_list,'actions':action_list}) + return df_actions + + def _seed(self, seed=None): + self.np_random, seed = seeding.np_random(seed) + return [seed] + + + def get_sb_env(self): + e = DummyVecEnv([lambda: self]) + obs = e.reset() + return e, obs \ No newline at end of file diff --git a/model/online_stock_prediction.py b/model/online_stock_prediction.py index 2e48b338a..a04002ace 100644 --- a/model/online_stock_prediction.py +++ b/model/online_stock_prediction.py @@ -1,3 +1,5 @@ +import sys +import os import pandas as pd import numpy as np import datetime @@ -6,13 +8,13 @@ from finrl.preprocessing.preprocessors import FeatureEngineer from finrl.preprocessing.data import data_split from finrl.env.env_stocktrading import StockTradingEnv -from finrl.env.env_stocks import StockEnv -from finrl.env.env_onlinestocktrading import OnlineStockTradingEnv + from finrl.model.models import DRLAgent from finrl.trade.backtest import backtest_stats, backtest_plot, get_daily_return, get_baseline - - +sys.path.append(os.path.join(os.path.dirname(__file__),"..","env")) +from env_stocks import StockEnv +from env_onlinestocktrading import OnlineStockTradingEnv class OnlineStockPrediction: @@ -20,36 +22,13 @@ def __init__(self, e_trade_gym, model): self.e_trade_gym = e_trade_gym self.env_trade, self.cur_obs = self.e_trade_gym.get_sb_env() self.model = model - #self.env_trade.reset() - - - # def _get_numerical_data(self,all=True,col_list=['date','open', 'high', 'low', 'close', 'volume', 'tic','day']): - # if not all: - # window = 30 - # if len(self.e_trade_gym.df.index.unique() > window): - # indices = self.e_trade_gym.df.unique()[-window:] - # prev_numerical_df = self.e_trade_gym.df.loc[indices][col_list] - # else: - # prev_numerical_df = e_trade_gym.df[col_list] - # return prev_numerical_df - - # def process_data(self,numerical_df,sentiment_df): - # full_numerical_df = self.compute_technical_indicators(numerical_df) - # new_df = full_numerical_df.merge(sentiment_df,on=['date','tic']) - # self.add_data(new_df) - # return new_df - - # def compute_technical_indicators(self,numerical_df): - # prev_numerical_df = self._get_numerical_data(all=False) - # full_df = prev_numerical_df.append(numerical_df) - # full_df = self.feature_engineer.preprocess_data(full_df) - # #TODO Might need to just take the indices corresponding to the new numerical data - # return full_df + def add_data(self,df): self.e_trade_gym._update_data(df) + def predict(self): #print("CURRENT OBSERVATION:" , self.cur_obs) action, states = self.model.predict(self.cur_obs) @@ -117,7 +96,7 @@ def main(): # print(trade_data.index) #trained_a2c = agent.train_model(model=model_a2c, tb_log_name='a2c',total_timesteps=10000) feature_engineer = FeatureEngineer() - online_stock_pred = OnlineStockPrediction(e_trade_gym,model_a2c,feature_engineer) + online_stock_pred = OnlineStockPrediction(e_trade_gym,model_a2c) for i in range(1,trade_data.index.unique().max()): print(trade_data.loc[i]) online_stock_pred.add_data(trade_data.loc[i]) diff --git a/preprocessing/preprocessors.py b/preprocessing/preprocessors.py deleted file mode 100644 index b1d0a41f5..000000000 --- a/preprocessing/preprocessors.py +++ /dev/null @@ -1,161 +0,0 @@ -import numpy as np -import pandas as pd -from stockstats import StockDataFrame as Sdf -from config import config - -def load_dataset(*, file_name: str) -> pd.DataFrame: - """ - load csv dataset from path - :return: (df) pandas dataframe - """ - #_data = pd.read_csv(f"{config.DATASET_DIR}/{file_name}") - _data = pd.read_csv(file_name) - return _data - -def data_split(df,start,end): - """ - split the dataset into training or testing using date - :param data: (df) pandas dataframe, start, end - :return: (df) pandas dataframe - """ - data = df[(df.datadate >= start) & (df.datadate < end)] - data=data.sort_values(['datadate','tic'],ignore_index=True) - #data = data[final_columns] - data.index = data.datadate.factorize()[0] - return data - -def calcualte_price(df): - """ - calcualte adjusted close price, open-high-low price and volume - :param data: (df) pandas dataframe - :return: (df) pandas dataframe - """ - data = df.copy() - data = data[['datadate', 'tic', 'prccd', 'ajexdi', 'prcod', 'prchd', 'prcld', 'cshtrd']] - data['ajexdi'] = data['ajexdi'].apply(lambda x: 1 if x == 0 else x) - - data['adjcp'] = data['prccd'] / data['ajexdi'] - data['open'] = data['prcod'] / data['ajexdi'] - data['high'] = data['prchd'] / data['ajexdi'] - data['low'] = data['prcld'] / data['ajexdi'] - data['volume'] = data['cshtrd'] - - data = data[['datadate', 'tic', 'adjcp', 'open', 'high', 'low', 'volume']] - data = data.sort_values(['tic', 'datadate'], ignore_index=True) - return data - -def add_technical_indicator(df): - """ - calcualte technical indicators - use stockstats package to add technical inidactors - :param data: (df) pandas dataframe - :return: (df) pandas dataframe - """ - stock = Sdf.retype(df.copy()) - - stock['close'] = stock['adjcp'] - unique_ticker = stock.tic.unique() - - macd = pd.DataFrame() - rsi = pd.DataFrame() - cci = pd.DataFrame() - dx = pd.DataFrame() - - #temp = stock[stock.tic == unique_ticker[0]]['macd'] - for i in range(len(unique_ticker)): - ## macd - temp_macd = stock[stock.tic == unique_ticker[i]]['macd'] - temp_macd = pd.DataFrame(temp_macd) - macd = macd.append(temp_macd, ignore_index=True) - ## rsi - temp_rsi = stock[stock.tic == unique_ticker[i]]['rsi_30'] - temp_rsi = pd.DataFrame(temp_rsi) - rsi = rsi.append(temp_rsi, ignore_index=True) - ## cci - temp_cci = stock[stock.tic == unique_ticker[i]]['cci_30'] - temp_cci = pd.DataFrame(temp_cci) - cci = cci.append(temp_cci, ignore_index=True) - ## adx - temp_dx = stock[stock.tic == unique_ticker[i]]['dx_30'] - temp_dx = pd.DataFrame(temp_dx) - dx = dx.append(temp_dx, ignore_index=True) - - - df['macd'] = macd - df['rsi'] = rsi - df['cci'] = cci - df['adx'] = dx - - return df - - - -def preprocess_data(): - """data preprocessing pipeline""" - - df = load_dataset(file_name=config.TRAINING_DATA_FILE) - # get data after 2009 - df = df[df.datadate>=20090000] - # calcualte adjusted price - df_preprocess = calcualte_price(df) - # add technical indicators using stockstats - df_final=add_technical_indicator(df_preprocess) - # fill the missing values at the beginning - df_final.fillna(method='bfill',inplace=True) - return df_final - -def add_turbulence(df): - """ - add turbulence index from a precalcualted dataframe - :param data: (df) pandas dataframe - :return: (df) pandas dataframe - """ - turbulence_index = calcualte_turbulence(df) - df = df.merge(turbulence_index, on='datadate') - df = df.sort_values(['datadate','tic']).reset_index(drop=True) - return df - - - -def calcualte_turbulence(df): - """calculate turbulence index based on dow 30""" - # can add other market assets - - df_price_pivot=df.pivot(index='datadate', columns='tic', values='adjcp') - unique_date = df.datadate.unique() - # start after a year - start = 252 - turbulence_index = [0]*start - #turbulence_index = [0] - count=0 - for i in range(start,len(unique_date)): - current_price = df_price_pivot[df_price_pivot.index == unique_date[i]] - hist_price = df_price_pivot[[n in unique_date[0:i] for n in df_price_pivot.index ]] - cov_temp = hist_price.cov() - current_temp=(current_price - np.mean(hist_price,axis=0)) - temp = current_temp.values.dot(np.linalg.inv(cov_temp)).dot(current_temp.values.T) - if temp>0: - count+=1 - if count>2: - turbulence_temp = temp[0][0] - else: - #avoid large outlier because of the calculation just begins - turbulence_temp=0 - else: - turbulence_temp=0 - turbulence_index.append(turbulence_temp) - - - turbulence_index = pd.DataFrame({'datadate':df_price_pivot.index, - 'turbulence':turbulence_index}) - return turbulence_index - - - - - - - - - - diff --git a/Sentiment_model.py b/sentiment_analysis/Sentiment_model.py similarity index 100% rename from Sentiment_model.py rename to sentiment_analysis/Sentiment_model.py From 53126bff4ef7df639df69d925c1b5a1a55372581 Mon Sep 17 00:00:00 2001 From: nikhiljain217 Date: Wed, 28 Apr 2021 07:51:06 -0600 Subject: [PATCH 05/12] fix invalid path for windows --- .../A2C_30k_dow_126.zip | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename trained_models/{2021-03-31 10:30:20.968779 => 2021-03-31}/A2C_30k_dow_126.zip (100%) diff --git a/trained_models/2021-03-31 10:30:20.968779/A2C_30k_dow_126.zip b/trained_models/2021-03-31/A2C_30k_dow_126.zip similarity index 100% rename from trained_models/2021-03-31 10:30:20.968779/A2C_30k_dow_126.zip rename to trained_models/2021-03-31/A2C_30k_dow_126.zip From 2e8dc1d4da7f9cfcf31f55ec4d8d27b36cf1ef3c Mon Sep 17 00:00:00 2001 From: nikhiljain217 <55259994+nikhiljain217@users.noreply.github.com> Date: Wed, 28 Apr 2021 17:03:07 -0600 Subject: [PATCH 06/12] Twitter streaming --- .gitignore | 7 ++- backend/backend.py | 100 ++++++++++++++++++++++++++++++++++++++ backend/stock_metadata.py | 33 +++++++++++++ requirements.txt | 13 ++++- 4 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 backend/backend.py create mode 100644 backend/stock_metadata.py diff --git a/.gitignore b/.gitignore index e53cf95df..a96d18535 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ results/ # remove DS_Store **/.DS_Store -# Remove vs stuff -.vscode - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -114,8 +111,9 @@ celerybeat.pid # Environments .env .venv +env/ venv/ -#ENV/ +ENV/ env.bak/ venv.bak/ @@ -137,3 +135,4 @@ dmypy.json # Pyre type checker .pyre/ +backend/twitter_keys.py diff --git a/backend/backend.py b/backend/backend.py new file mode 100644 index 000000000..040be9bad --- /dev/null +++ b/backend/backend.py @@ -0,0 +1,100 @@ +from stock_metadata import * +import tweepy +import time +from twitter_keys import * +from stock_metadata import company_and_ticks +from pprint import pprint +import re +import sys +# init server + +class TweetStreamListener(tweepy.StreamListener): + + def __init__(self): + self.backoff_timeout = 1 + super(TweetStreamListener,self).__init__() + self.query_string = list() + self.query_string.extend(list(company_and_ticks.keys())) + #self.query_string.extend(list(company_and_ticks.values())) + #self.query_string.remove("V") + + def on_status(self, status): + + #reset timeout + self.backoff_timeout = 1 + + #send message on namespace + tweet = self.construct_tweet(status) + if (tweet): + print(tweet) + + def on_error(self, status_code): + + # exp back-off if rate limit error + if status_code == 420: + time.sleep(self.backoff_timeout) + self.backoff_timeout *= 2 + return True + else: + print("Error {0} occurred".format(status_code)) + return False + + def construct_tweet(self, status): + try: + tweet_text = "" + if hasattr(status, 'retweeted_status') and hasattr(status.retweeted_status, 'extended_tweet'): + tweet_text = status.retweeted_status.extended_tweet['full_text'] + elif hasattr(status, 'full_text'): + tweet_text = status.full_text + elif hasattr(status, 'extended_tweet'): + tweet_text = status.extended_tweet['full_text'] + elif hasattr(status, 'quoted_status'): + if hasattr(status.quoted_status, 'extended_tweet'): + tweet_text = status.quoted_status.extended_tweet['full_text'] + else: + tweet_text = status.quoted_status.text + else: + tweet_text = status.text + tweet_data = dict() + for q_string in self.query_string: + if tweet_text.lower().find(q_string.lower()) != -1: + tweet_data = { + "text": TweetStreamListener.sanitize_text(tweet_text), + "tic": company_and_ticks[q_string], + "date": status.created_at + } + break + return tweet_data + except Exception as e: + print("Exception occur while parsing status object:", e) + + @staticmethod + def sanitize_text(tweet): + tweet = tweet.replace('\n', '').replace('"', '').replace('\'', '') + return re.sub(r"http\S+", "", tweet) + +class TwitterStreamer: + + def __init__(self): + self.twitter_api = None + self.__get_twitter_connection() + self.listener = TweetStreamListener() + self.tweet_stream = tweepy.Stream(auth=self.twitter_api.auth, listener=self.listener, tweet_mode='extended') + + def __get_twitter_connection(self): + try: + auth = tweepy.OAuthHandler(tw_access_key, tw_secret_key) + auth.set_access_token(tw_access_token, tw_access_token_secret) + self.twitter_api = tweepy.API(auth, wait_on_rate_limit=True) + except Exception as e: + print("Exception occurred : {0}".format(e)) + + def start_tweet_streaming(self): + # start stream to listen to company tweets + self.tweet_stream.filter(track=self.listener.query_string, languages=['en']) + +if __name__=="__main__": + + #init twitter connection + twitter_streamer = TwitterStreamer() + twitter_streamer.start_tweet_streaming() diff --git a/backend/stock_metadata.py b/backend/stock_metadata.py new file mode 100644 index 000000000..fddff5256 --- /dev/null +++ b/backend/stock_metadata.py @@ -0,0 +1,33 @@ +company_and_ticks = { + "3M Company": "MMM", + "American Express": "AXP", + "Amgen": "AMGN", + "Apple": "AAPL", + "Boeing": "BA", + "Caterpillar": "CAT", + "Chevron":"CVX", + "Cisco": "CSCO", + "Coca-Cola": "KO", + "Dow": "DOW", + "Goldman Sachs": "GS", + "Home Depot": "HD", + "Honeywell": "HON", + "IBM": "IBM", + "Intel": "INTC", + "Johnson & Johnson": "JNJ", + "JPMorgan": "JPM", + "McDonald": "MCD", + "Merck": "MRK", + "Microsoft": "MSFT", + "Nike": "NKE", + "Proctor & Gamble": "PG", + "Salesforce": "CRM", + "The Travelers Companies": "TRV", + "UnitedHealth": "UNH", + "Verizon": "VZ", + "Visa": "V", + "Walgreens Boots Alliance": "WBA", + "Walmart": "WMT", + "Disney": "DIS" +} + diff --git a/requirements.txt b/requirements.txt index 92cc820a9..e5c01da91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ # Model Building Requirements numpy +finrl pandas==1.0.3 stockstats scikit-learn==0.21.0 @@ -16,8 +17,16 @@ matplotlib==3.2.1 pytest>=5.3.2,<6.0.0 # packaging -setuptools>=41.4.0,<42.0.0 -wheel>=0.33.6,<0.34.0 +setuptools +wheel + +#twitter +tweepy + +#yahoo finance +yfinance + + From bd273adbe6649d1e94e0d4bf622a60ce66a06ccc Mon Sep 17 00:00:00 2001 From: Aman Satya Date: Wed, 28 Apr 2021 17:30:08 -0600 Subject: [PATCH 07/12] Update config.py --- config/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.py b/config/config.py index 28b85e030..4a3189286 100644 --- a/config/config.py +++ b/config/config.py @@ -18,7 +18,8 @@ # data #TRAINING_DATA_FILE = "data/ETF_SPY_2009_2020.csv" TRAINING_DATA_FILE = "data/dow_30_2009_2020.csv" - +# List of stock tickers +stock_tickers=['MMM','AXP','AMGN','AAPL','BA','CAT','CVX','CSCO','KO','DIS','DOW','GS','HD','HON','IBM','INTC','JNJ','JPM','MCD','MRK','MSFT','NKE','PG','CRM','TRV','UNH','VZ','V','WBA','WMT'] now = datetime.datetime.now() TRAINED_MODEL_DIR = f"trained_models/{now}" os.makedirs(TRAINED_MODEL_DIR) From 349748fd8c2c9061428dde67795f37d2cb1cf7ec Mon Sep 17 00:00:00 2001 From: Harshit Hajela Date: Sat, 24 Apr 2021 20:07:14 -0600 Subject: [PATCH 08/12] initial stream implementation --- .gitignore | 3 +++ backend/companies | 1 + backend/subreddits | 1 + backend/tickers | 1 + backend/updater.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 backend/companies create mode 100644 backend/subreddits create mode 100644 backend/tickers create mode 100644 backend/updater.py diff --git a/.gitignore b/.gitignore index a96d18535..92e195be3 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,6 @@ dmypy.json .pyre/ backend/twitter_keys.py + +# Credentials +backend/creds.json diff --git a/backend/companies b/backend/companies new file mode 100644 index 000000000..6a263b5d2 --- /dev/null +++ b/backend/companies @@ -0,0 +1 @@ +3M Company,American Express,Amgen,Apple Inc.,Boeing,Caterpillar Inc.,Chevron Corporation,Cisco Systems,The Coca-Cola Company,Dow Inc.,Goldman Sachs,The Home Depot,Honeywell,IBM,Intel,Johnson & Johnson,JPMorgan Chase,McDonald's,Merck & Co.,Microsoft,Nike,Procter & Gamble,Salesforce,The Travelers Companies,UnitedHealth Group,Verizon,Visa Inc.,Walgreens Boots Alliance,Walmart,The Walt Disney Company diff --git a/backend/subreddits b/backend/subreddits new file mode 100644 index 000000000..30efbcad4 --- /dev/null +++ b/backend/subreddits @@ -0,0 +1 @@ +wallstreetbets diff --git a/backend/tickers b/backend/tickers new file mode 100644 index 000000000..89d324b72 --- /dev/null +++ b/backend/tickers @@ -0,0 +1 @@ +$MMM,$AXP,$AMGN,$AAPL,$BA,$CAT,$CVX,$CSCO,$KO,$DOW,$GS,$HD,$HON,$IBM,$INTC,$JNJ,$JPM,$MCD,$MRK,$MSFT,$NKE,$PG,$CRM,$TRV,$UNH,$VZ,$V,$WBA,$WMT,$DIS diff --git a/backend/updater.py b/backend/updater.py new file mode 100644 index 000000000..3aea520d3 --- /dev/null +++ b/backend/updater.py @@ -0,0 +1,59 @@ +import sys +import praw +import time +from datetime import datetime +import requests +import threading +import json + +redditClient = None + +def fetchComments(subreddit, companies, tickers): + sr_obj = redditClient.subreddit(subreddit) + companies.extend(tickers) + try: + for comment in sr_obj.stream.comments(skip_existing=True): + for company in companies: + if company in comment.body: + print(comment) + except Exception as error: + print("Error {0} occurred while streaming comments from subreddit {1}".format(error)) + + +if __name__=='__main__': + creds = json.loads(open("creds.json","r").read()) + print(creds) + redditClient = praw.Reddit(client_id=creds['client_id'], + client_secret=creds['client_secret'], + password=creds['password'], + user_agent=creds['user_agent'], + username=creds['username']) + + subreddits = [sr.strip() for sr in open("subreddits","r").read().split(',')] + companies = [cmp.strip() for cmp in open("companies","r").read().split(',')] + tickers = [tick.strip() for tick in open("tickers","r").read().split(',')] + + # start fetch thread for every subreddit + fetch_threads = [] + for sr in subreddits: + th = threading.Thread(name='fetch_comments_{0}'.format(sr), target=fetchComments, args=(sr, companies, tickers)) + th.start() + fetch_threads.append(th) + + try: + while True: + time.sleep(2) + except KeyboardInterrupt: + for th in fetch_threads: + th.join() + + +""" + +This module is responsible for + +Streaming comments + +Stream comments from reddit and write to specified source (stdout or kafka) + +""" From 43d7c9854f24e06fa7cfea34086924d32f489366 Mon Sep 17 00:00:00 2001 From: Harshit Hajela Date: Sat, 24 Apr 2021 21:57:46 -0600 Subject: [PATCH 09/12] refactor --- backend/subreddits | 2 +- backend/tickers | 2 +- backend/updater.py | 46 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/backend/subreddits b/backend/subreddits index 30efbcad4..7b011cc11 100644 --- a/backend/subreddits +++ b/backend/subreddits @@ -1 +1 @@ -wallstreetbets +wallstreetbets,SecurityAnalysis,Finance,Options,Investing,Stocks,StockMarket diff --git a/backend/tickers b/backend/tickers index 89d324b72..02ccc9dd9 100644 --- a/backend/tickers +++ b/backend/tickers @@ -1 +1 @@ -$MMM,$AXP,$AMGN,$AAPL,$BA,$CAT,$CVX,$CSCO,$KO,$DOW,$GS,$HD,$HON,$IBM,$INTC,$JNJ,$JPM,$MCD,$MRK,$MSFT,$NKE,$PG,$CRM,$TRV,$UNH,$VZ,$V,$WBA,$WMT,$DIS +MMM,AXP,AMGN,AAPL,BA,CAT,CVX,CSCO,KO,DOW,GS,HD,HON,IBM,INTC,JNJ,JPM,MCD,MRK,MSFT,NKE,PG,CRM,TRV,UNH,VZ,V,WBA,WMT,DIS diff --git a/backend/updater.py b/backend/updater.py index 3aea520d3..25c628998 100644 --- a/backend/updater.py +++ b/backend/updater.py @@ -1,28 +1,50 @@ import sys import praw import time -from datetime import datetime -import requests import threading import json redditClient = None -def fetchComments(subreddit, companies, tickers): - sr_obj = redditClient.subreddit(subreddit) - companies.extend(tickers) - try: - for comment in sr_obj.stream.comments(skip_existing=True): +class CommentsFetcher (threading.Thread): + die = False + sr_obj = None + tickers = [] + companies = [] + def __init__(self, subreddit, companies, tickers, exit_on_fail=False): + threading.Thread.__init__(self) + self.name = 'fetch_comments_{0}'.format(subreddit) + self.companies = companies + self.tickers = tickers + self.exit_on_fail = exit_on_fail + lock = threading.RLock() + with lock: + self.sr_obj = redditClient.subreddit(subreddit) + + def run(self): + while not self.die: + try: + self.fetchComments() + except Exception as e: + if self.exit_on_fail: + raise + else: + print("Thread {1}, Error {0} occurred while streaming comments, continuing".format(e, self.name)) + + def join(self): + self.die = True + super().join() + + def fetchComments(self): + search_strings = self.companies + self.tickers + for comment in self.sr_obj.stream.comments(skip_existing=True, pause_after=5): for company in companies: if company in comment.body: - print(comment) - except Exception as error: - print("Error {0} occurred while streaming comments from subreddit {1}".format(error)) + print(comment.body) if __name__=='__main__': creds = json.loads(open("creds.json","r").read()) - print(creds) redditClient = praw.Reddit(client_id=creds['client_id'], client_secret=creds['client_secret'], password=creds['password'], @@ -36,7 +58,7 @@ def fetchComments(subreddit, companies, tickers): # start fetch thread for every subreddit fetch_threads = [] for sr in subreddits: - th = threading.Thread(name='fetch_comments_{0}'.format(sr), target=fetchComments, args=(sr, companies, tickers)) + th = CommentsFetcher(sr, companies, tickers) th.start() fetch_threads.append(th) From f435d9b30e36566a2f32fd259dd440ff3d0afa9b Mon Sep 17 00:00:00 2001 From: Harshit Hajela Date: Wed, 28 Apr 2021 14:08:06 -0600 Subject: [PATCH 10/12] change input, add kafka output --- backend/companies | 1 - backend/companies.json | 32 ++++++++++++++++++++++++ backend/tickers | 1 - backend/updater.py | 56 +++++++++++++++++++++++++++++++----------- 4 files changed, 73 insertions(+), 17 deletions(-) delete mode 100644 backend/companies create mode 100644 backend/companies.json delete mode 100644 backend/tickers diff --git a/backend/companies b/backend/companies deleted file mode 100644 index 6a263b5d2..000000000 --- a/backend/companies +++ /dev/null @@ -1 +0,0 @@ -3M Company,American Express,Amgen,Apple Inc.,Boeing,Caterpillar Inc.,Chevron Corporation,Cisco Systems,The Coca-Cola Company,Dow Inc.,Goldman Sachs,The Home Depot,Honeywell,IBM,Intel,Johnson & Johnson,JPMorgan Chase,McDonald's,Merck & Co.,Microsoft,Nike,Procter & Gamble,Salesforce,The Travelers Companies,UnitedHealth Group,Verizon,Visa Inc.,Walgreens Boots Alliance,Walmart,The Walt Disney Company diff --git a/backend/companies.json b/backend/companies.json new file mode 100644 index 000000000..eaf617ce7 --- /dev/null +++ b/backend/companies.json @@ -0,0 +1,32 @@ +{ + "MMM": "3M Company", + "AXP": "American Express", + "AMGN": "Amgen", + "AAPL": "Apple Inc.", + "BA": "Boeing", + "CAT": "Caterpillar Inc.", + "CVX": "Chevron Corporation", + "CSCO": "Cisco Systems", + "KO": "The Coca-Cola Company", + "DOW": "Dow Inc.", + "GS": "Goldman Sachs", + "HD": "The Home Depot", + "HON": "Honeywell", + "IBM": "IBM", + "INTC": "Intel", + "JNJ": "Johnson & Johnson", + "JPM": "JPMorgan Chase", + "MCD": "McDonald's", + "MRK": "Merck & Co.", + "MSFT": "Microsoft", + "NKE": "Nike", + "PG": "Procter & Gamble", + "CRM": "Salesforce", + "TRV": "The Travelers Companies", + "UNH": "UnitedHealth Group", + "VZ": "Verizon", + "V": "Visa Inc.", + "WBA": "Walgreens Boots Alliance", + "WMT": "Walmart", + "DIS": "The Walt Disney Company" +} diff --git a/backend/tickers b/backend/tickers deleted file mode 100644 index 02ccc9dd9..000000000 --- a/backend/tickers +++ /dev/null @@ -1 +0,0 @@ -MMM,AXP,AMGN,AAPL,BA,CAT,CVX,CSCO,KO,DOW,GS,HD,HON,IBM,INTC,JNJ,JPM,MCD,MRK,MSFT,NKE,PG,CRM,TRV,UNH,VZ,V,WBA,WMT,DIS diff --git a/backend/updater.py b/backend/updater.py index 25c628998..9509ca3b6 100644 --- a/backend/updater.py +++ b/backend/updater.py @@ -1,22 +1,25 @@ -import sys +import argparse +import json +import math import praw -import time import threading -import json +import time + +from kafka import KafkaProducer redditClient = None class CommentsFetcher (threading.Thread): die = False sr_obj = None - tickers = [] - companies = [] - def __init__(self, subreddit, companies, tickers, exit_on_fail=False): + companies = {} + def __init__(self, subreddit, companies, exit_on_fail=False, producer=None, topic=None): threading.Thread.__init__(self) self.name = 'fetch_comments_{0}'.format(subreddit) self.companies = companies - self.tickers = tickers self.exit_on_fail = exit_on_fail + self.producer = producer + self.topic = topic lock = threading.RLock() with lock: self.sr_obj = redditClient.subreddit(subreddit) @@ -36,14 +39,33 @@ def join(self): super().join() def fetchComments(self): - search_strings = self.companies + self.tickers for comment in self.sr_obj.stream.comments(skip_existing=True, pause_after=5): - for company in companies: - if company in comment.body: - print(comment.body) - + for ticker in self.companies: + if ticker in comment.body or self.companies[ticker] in comment.body: + comment_obj = { "ticker": ticker, "text": comment.body, "timestamp": math.ceil(time.time_ns()/1000000) } + self.output(comment_obj) + break + + def output(self, comment): + if self.producer is None: + print(comment) + else: + if self.topic is None: + raise ValueError("topic not supplied") + key = "{0}_{1}".format(comment["ticker"],comment["timestamp"]) + try: + key_bytes = bytes(key, encoding='utf-8') + value = json.dumps(comment_obj) + value_bytes = bytes(value, encoding='utf-8') + self.producer.send(self.topic, key=key_bytes, value=value_bytes) + except Exception as e: + print("Error {0} occurred while publishing message with key {1}".format(e, key)) if __name__=='__main__': + parser = argparse.ArgumentParser(description='Stream reddit comments to stdout or kafka topic') + parser.add_argument('-t', '--topic', metavar='', help='Kafka topic name') + parser.add_argument('-H', '--host', metavar='', default='localhost:9092', help='Hostname:port of bootstrap server') + args = parser.parse_args() creds = json.loads(open("creds.json","r").read()) redditClient = praw.Reddit(client_id=creds['client_id'], client_secret=creds['client_secret'], @@ -51,14 +73,18 @@ def fetchComments(self): user_agent=creds['user_agent'], username=creds['username']) + subreddits = [sr.strip() for sr in open("subreddits","r").read().split(',')] - companies = [cmp.strip() for cmp in open("companies","r").read().split(',')] - tickers = [tick.strip() for tick in open("tickers","r").read().split(',')] + companies = json.loads(open("companies.json","r").read()) + + producer = None + if args.topic is not None: + producer = KafkaProducer(bootstrap_servers=[args.host], api_version=(0, 10)) # start fetch thread for every subreddit fetch_threads = [] for sr in subreddits: - th = CommentsFetcher(sr, companies, tickers) + th = CommentsFetcher(sr, companies, producer, args.topic) th.start() fetch_threads.append(th) From a6b55ff11f50e51c6c818ab647264fdc78211f45 Mon Sep 17 00:00:00 2001 From: Harshit Hajela Date: Wed, 28 Apr 2021 14:30:51 -0600 Subject: [PATCH 11/12] make string matching more lenient --- backend/companies.json | 26 +++++++++++++------------- backend/updater.py | 7 ++++++- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/backend/companies.json b/backend/companies.json index eaf617ce7..12457d9b6 100644 --- a/backend/companies.json +++ b/backend/companies.json @@ -1,32 +1,32 @@ { - "MMM": "3M Company", + "MMM": "3M", "AXP": "American Express", "AMGN": "Amgen", - "AAPL": "Apple Inc.", + "AAPL": "Apple", "BA": "Boeing", - "CAT": "Caterpillar Inc.", - "CVX": "Chevron Corporation", - "CSCO": "Cisco Systems", - "KO": "The Coca-Cola Company", - "DOW": "Dow Inc.", + "CAT": "Caterpillar", + "CVX": "Chevron", + "CSCO": "Cisco", + "KO": "Coca-Cola", + "DOW": "Dow", "GS": "Goldman Sachs", - "HD": "The Home Depot", + "HD": "Home Depot", "HON": "Honeywell", "IBM": "IBM", "INTC": "Intel", "JNJ": "Johnson & Johnson", - "JPM": "JPMorgan Chase", + "JPM": "JPMorgan", "MCD": "McDonald's", - "MRK": "Merck & Co.", + "MRK": "Merck", "MSFT": "Microsoft", "NKE": "Nike", "PG": "Procter & Gamble", "CRM": "Salesforce", "TRV": "The Travelers Companies", - "UNH": "UnitedHealth Group", + "UNH": "UnitedHealth", "VZ": "Verizon", - "V": "Visa Inc.", + "V": "Visa", "WBA": "Walgreens Boots Alliance", "WMT": "Walmart", - "DIS": "The Walt Disney Company" + "DIS": "Walt Disney" } diff --git a/backend/updater.py b/backend/updater.py index 9509ca3b6..0815e3110 100644 --- a/backend/updater.py +++ b/backend/updater.py @@ -40,8 +40,13 @@ def join(self): def fetchComments(self): for comment in self.sr_obj.stream.comments(skip_existing=True, pause_after=5): + comment_text = comment.body.casefold() for ticker in self.companies: - if ticker in comment.body or self.companies[ticker] in comment.body: + casefolded_company = self.companies[ticker].casefold() + if ('{0} '.format(ticker) in comment.body or + ' {0}'.format(ticker) in comment.body or + '{0} '.format(casefolded_company) in comment_text or + ' {0}'.format(casefolded_company) in comment_text): comment_obj = { "ticker": ticker, "text": comment.body, "timestamp": math.ceil(time.time_ns()/1000000) } self.output(comment_obj) break From d7c5cf073cbabce05cfe1fb1e95df1e9d2544ff5 Mon Sep 17 00:00:00 2001 From: Harshit Hajela Date: Wed, 28 Apr 2021 19:17:04 -0600 Subject: [PATCH 12/12] renamed file --- backend/{updater.py => stream_reddit.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/{updater.py => stream_reddit.py} (100%) diff --git a/backend/updater.py b/backend/stream_reddit.py similarity index 100% rename from backend/updater.py rename to backend/stream_reddit.py