From 7ffef3ffb2fb852ac294b9a0f80b98d5b4e458a5 Mon Sep 17 00:00:00 2001 From: Alex Schneider Date: Sun, 31 May 2026 14:52:19 +0200 Subject: [PATCH] Document optional sentiment data columns --- doc/examples/Quick Start User Guide.ipynb | 80 +++++++++++++++++++++-- doc/examples/Quick Start User Guide.py | 71 ++++++++++++++++++-- 2 files changed, 143 insertions(+), 8 deletions(-) diff --git a/doc/examples/Quick Start User Guide.ipynb b/doc/examples/Quick Start User Guide.ipynb index d3a5c7fa3..10e89cc65 100644 --- a/doc/examples/Quick Start User Guide.ipynb +++ b/doc/examples/Quick Start User Guide.ipynb @@ -450,6 +450,81 @@ "GOOG.tail()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "External data can be joined to the strategy data frame before it is passed to\n", + "`Backtest`. For example, the snippet below adds a daily sentiment column. If\n", + "`ADANOS_API_KEY` is set, it tries to load Reddit stock sentiment from the\n", + "[Adanos Market Sentiment API](https://api.adanos.org/reddit/stocks/v1/);\n", + "otherwise, it uses a tiny sample series so the tutorial stays runnable offline.\n", + "\n", + "The resulting `Sentiment` column is available in a strategy as\n", + "`self.data.Sentiment[-1]`, just like `self.data.Close[-1]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "from urllib.error import HTTPError, URLError\n", + "from urllib.parse import urlencode\n", + "from urllib.request import Request, urlopen\n", + "\n", + "import pandas as pd\n", + "\n", + "\n", + "def adanos_daily_sentiment(index, ticker):\n", + " daily_index = pd.DatetimeIndex(index).normalize()\n", + " sample = pd.Series(\n", + " [.15, -.05, .22, .08, .18],\n", + " index=pd.to_datetime([\n", + " '2013-02-25',\n", + " '2013-02-26',\n", + " '2013-02-27',\n", + " '2013-02-28',\n", + " '2013-03-01',\n", + " ]),\n", + " name='Sentiment',\n", + " )\n", + "\n", + " api_key = os.environ.get('ADANOS_API_KEY')\n", + " if api_key:\n", + " params = urlencode({\n", + " 'from': daily_index.min().date().isoformat(),\n", + " 'to': daily_index.max().date().isoformat(),\n", + " })\n", + " request = Request(\n", + " f'https://api.adanos.org/reddit/stocks/v1/stock/{ticker}?{params}',\n", + " headers={'X-API-Key': api_key},\n", + " )\n", + " try:\n", + " with urlopen(request, timeout=10) as response:\n", + " payload = json.load(response)\n", + " rows = payload.get('daily_trend') or []\n", + " sentiment = pd.Series(\n", + " {pd.Timestamp(row['date']): float(row.get('sentiment_score') or 0)\n", + " for row in rows},\n", + " name='Sentiment',\n", + " )\n", + " if not sentiment.empty:\n", + " sample = sentiment\n", + " except (HTTPError, URLError, OSError, ValueError, KeyError):\n", + " pass\n", + "\n", + " return sample.reindex(daily_index).ffill().fillna(0).to_numpy()\n", + "\n", + "\n", + "sentiment_data = GOOG.copy()\n", + "sentiment_data['Sentiment'] = adanos_daily_sentiment(sentiment_data.index, 'GOOG')\n", + "sentiment_data[['Close', 'Sentiment']].tail()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -470,9 +545,6 @@ "metadata": {}, "outputs": [], "source": [ - "import pandas as pd\n", - "\n", - "\n", "def SMA(values, n):\n", " \"\"\"\n", " Return simple moving average of `values`, at\n", @@ -652,7 +724,7 @@ "source": [ "from backtesting import Backtest\n", "\n", - "bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002)\n", + "bt = Backtest(sentiment_data, SmaCross, cash=10_000, commission=.002)\n", "stats = bt.run()\n", "stats" ] diff --git a/doc/examples/Quick Start User Guide.py b/doc/examples/Quick Start User Guide.py index 35466d8ba..42f72f9bf 100644 --- a/doc/examples/Quick Start User Guide.py +++ b/doc/examples/Quick Start User Guide.py @@ -47,6 +47,72 @@ GOOG.tail() +# %% [markdown] +# External data can be joined to the strategy data frame before it is passed to +# `Backtest`. For example, the snippet below adds a daily sentiment column. If +# `ADANOS_API_KEY` is set, it tries to load Reddit stock sentiment from the +# [Adanos Market Sentiment API](https://api.adanos.org/reddit/stocks/v1/); +# otherwise, it uses a tiny sample series so the tutorial stays runnable offline. +# +# The resulting `Sentiment` column is available in a strategy as +# `self.data.Sentiment[-1]`, just like `self.data.Close[-1]`. + +# %% +import json +import os +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +import pandas as pd + + +def adanos_daily_sentiment(index, ticker): + daily_index = pd.DatetimeIndex(index).normalize() + sample = pd.Series( + [.15, -.05, .22, .08, .18], + index=pd.to_datetime([ + '2013-02-25', + '2013-02-26', + '2013-02-27', + '2013-02-28', + '2013-03-01', + ]), + name='Sentiment', + ) + + api_key = os.environ.get('ADANOS_API_KEY') + if api_key: + params = urlencode({ + 'from': daily_index.min().date().isoformat(), + 'to': daily_index.max().date().isoformat(), + }) + request = Request( + f'https://api.adanos.org/reddit/stocks/v1/stock/{ticker}?{params}', + headers={'X-API-Key': api_key}, + ) + try: + with urlopen(request, timeout=10) as response: + payload = json.load(response) + rows = payload.get('daily_trend') or [] + sentiment = pd.Series( + {pd.Timestamp(row['date']): float(row.get('sentiment_score') or 0) + for row in rows}, + name='Sentiment', + ) + if not sentiment.empty: + sample = sentiment + except (HTTPError, URLError, OSError, ValueError, KeyError): + pass + + return sample.reindex(daily_index).ffill().fillna(0).to_numpy() + + +sentiment_data = GOOG.copy() +sentiment_data['Sentiment'] = adanos_daily_sentiment(sentiment_data.index, 'GOOG') +sentiment_data[['Close', 'Sentiment']].tail() + + # %% [markdown] # ## Strategy # @@ -58,9 +124,6 @@ # but for this example, we can define a simple helper moving average function ourselves: # %% -import pandas as pd - - def SMA(values, n): """ Return simple moving average of `values`, at @@ -160,7 +223,7 @@ def next(self): # %% from backtesting import Backtest -bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002) +bt = Backtest(sentiment_data, SmaCross, cash=10_000, commission=.002) stats = bt.run() stats