{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Does sentiment agree with observed market risk?\n", "Combine CNN Fear & Greed observations, VIX closes and SPY returns without treating sentiment as a trade signal.\n", "Common observations in August\u2013October 2026. CNN records have dates without intraday timestamps, so the chart uses the mean of available observations per day. VIX and SPY use daily observations; this is market context, not signal performance." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Combine CNN Fear & Greed observations, VIX closes and SPY returns without treating sentiment as a trade signal.\n", "\n", "Research example. Live observations can change.\n", "\"\"\"\n", "\n", "from datetime import UTC, datetime\n", "from decimal import Decimal\n", "from textwrap import fill\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "\n", "AS_OF = datetime.now(UTC).date()\n", "NAVY, BLUE, PINK = \"#132244\", \"#8fb9ff\", \"#e7a2cc\"\n", "\n", "\n", "def fetch(qj):\n", " return {\n", " \"cnn\": qj.domains.call(\n", " \"sentiment.fear_greed.get_index_range\", provider=\"cnnf\", start_date=\"2026-08-01\", end_date=\"2026-10-30\"\n", " ),\n", " \"vix_current\": qj.domains.call(\n", " \"macro.indicators.get_fred_data_series_by_id\",\n", " provider=\"fred\",\n", " search_id=\"VIXCLS\",\n", " start=\"2026-01-01\",\n", " end=\"2026-10-30\",\n", " ),\n", " \"recent_prices\": qj.domains.call(\n", " \"equity.pricing.get_historical_prices\",\n", " provider=\"eod\",\n", " symbols=[\"SPY\"],\n", " start_date=\"2026-01-01\",\n", " end_date=\"2026-10-30\",\n", " frequency=\"1d\",\n", " ),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " observations = frame(raw[\"cnn\"])\n", " observations[\"fg_value\"] = pd.to_numeric(observations[\"fg_value\"], errors=\"raise\")\n", " sentiment = observations.groupby(\"date\")[\"fg_value\"].mean().rename(\"CNN daily mean\")\n", " prices = history(raw[\"recent_prices\"], \"SPY\")[\"price\"]\n", " returns = (100 * change(prices)).rename(\"SPY daily return %\")\n", " vix = macro(raw[\"vix_current\"], \"VIXCLS\").rename(\"VIX close\")\n", " data = pd.concat([sentiment, returns, vix], axis=1, join=\"inner\").dropna()\n", " if len(data) < 20:\n", " raise ValueError(\"Need at least 20 common sentiment, VIX and price dates\")\n", " fig, (top, bottom) = canvas(\"Sentiment \u00b7 price action and market risk\", \"CNN Fear & Greed \u00b7 daily mean\", panels=2)\n", " top.plot(data.index, data[\"CNN daily mean\"], color=BLUE)\n", " top.set_ylim(0, 100)\n", " points = bottom.scatter(data[\"VIX close\"], data[\"SPY daily return %\"], c=data[\"CNN daily mean\"], cmap=\"cool\", s=35)\n", " bottom.set_xlabel(\"VIX close\")\n", " bottom.set_ylabel(\"SPY daily adjusted return %\")\n", " bottom.axhline(0, color=\"#afc1dc\", lw=0.8)\n", " fig.colorbar(points, ax=bottom, label=\"CNN daily mean\")\n", " return data.reset_index(), fig\n", "\n", "\n", "def payload(response):\n", " if hasattr(response, \"to_dict\"):\n", " response = response.to_dict()\n", " while isinstance(response, dict):\n", " if response.get(\"error\") or response.get(\"error_code\") or response.get(\"success\") is False:\n", " raise ValueError(\"Provider request failed\")\n", " if \"data\" in response:\n", " response = response[\"data\"]\n", " elif \"value\" in response:\n", " response = response[\"value\"]\n", " else:\n", " break\n", " return response\n", "\n", "\n", "def frame(response, date=\"date\"):\n", " rows = payload(response)\n", " if not isinstance(rows, list) or not rows:\n", " raise ValueError(\"No provider observations returned\")\n", " result = pd.DataFrame(rows)\n", " if date:\n", " result[date] = pd.to_datetime(result[date], utc=True)\n", " result = result.sort_values(date)\n", " return result\n", "\n", "\n", "def decimal(value):\n", " if pd.isna(value):\n", " raise ValueError(\"Required monetary value or quantity is missing\")\n", " return Decimal(str(value))\n", "\n", "\n", "def ratio(numerator, denominator):\n", " denominator = decimal(denominator)\n", " if denominator == 0:\n", " raise ValueError(\"Required denominator is zero\")\n", " return float(decimal(numerator) / denominator)\n", "\n", "\n", "def change(values, periods=1):\n", " previous = values.shift(periods)\n", " return pd.Series(\n", " [ratio(x, y) - 1 if pd.notna(x) and pd.notna(y) else np.nan for x, y in zip(values, previous, strict=True)],\n", " index=values.index,\n", " )\n", "\n", "\n", "def history(response, symbol, adjusted=True):\n", " values = payload(response)\n", " if not isinstance(values, dict) or symbol not in values:\n", " raise ValueError(f\"No price coverage for {symbol}\")\n", " result = frame(values[symbol]).set_index(\"date\")\n", " field = \"adjusted_close\" if adjusted else \"close\"\n", " result[\"price\"] = result[field].map(decimal)\n", " result[\"volume\"] = result[\"volume\"].map(decimal)\n", " if result.index.has_duplicates:\n", " raise ValueError(\"Duplicate trading dates\")\n", " return result\n", "\n", "\n", "def macro(response, column):\n", " values = frame(response).set_index(\"date\")[column]\n", " values = pd.to_numeric(values, errors=\"coerce\").dropna()\n", " if values.empty:\n", " raise ValueError(f\"No numeric observations for {column}\")\n", " return values\n", "\n", "\n", "def canvas(title, ylabel, panels=1):\n", " plt.rcParams.update(\n", " {\n", " \"figure.facecolor\": NAVY,\n", " \"axes.facecolor\": NAVY,\n", " \"savefig.facecolor\": NAVY,\n", " \"text.color\": \"#e9eef8\",\n", " \"axes.labelcolor\": \"#afc1dc\",\n", " \"xtick.color\": \"#afc1dc\",\n", " \"ytick.color\": \"#afc1dc\",\n", " \"axes.edgecolor\": \"#34486b\",\n", " \"grid.color\": \"#34486b\",\n", " \"font.size\": 10,\n", " \"axes.prop_cycle\": plt.cycler(color=[BLUE, PINK, \"#ba9dea\"]),\n", " }\n", " )\n", " fig, axes = plt.subplots(panels, 1, figsize=(9, 6.1), squeeze=False, layout=\"constrained\")\n", " fig.suptitle(fill(title, width=58), fontsize=17, x=0.04, ha=\"left\", weight=\"medium\")\n", " for ax in axes.flat:\n", " ax.grid(axis=\"y\", alpha=0.45)\n", " ax.set_axisbelow(True)\n", " ax.spines[[\"top\", \"right\"]].set_visible(False)\n", " axes.flat[0].set_ylabel(ylabel)\n", " return fig, list(axes.flat)\n", "\n", "\n", "def run():\n", " from quantjourney.sdk import QuantJourneyAPI\n", "\n", " qj = QuantJourneyAPI.from_env()\n", " raw = fetch(qj)\n", " table, fig = analyze(raw)\n", " print(table.to_string(index=False))\n", " fig.savefig(OUTPUT, dpi=180)\n", " plt.show()\n", "\n", "\n", "OUTPUT = \"sdk_sentiment.png\"\n", "\n", "if __name__ == \"__main__\":\n", " run()\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }