From 6f9be2f812423015c61a9df4c93a7c991fed9f9e Mon Sep 17 00:00:00 2001 From: Yush Kapoor <55430541+yushdotkapoor@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:44:20 -0800 Subject: [PATCH 1/8] Added support for historical data --- src/fear_and_greed/cnn.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/fear_and_greed/cnn.py b/src/fear_and_greed/cnn.py index 049b1ea..4c00a3d 100644 --- a/src/fear_and_greed/cnn.py +++ b/src/fear_and_greed/cnn.py @@ -62,3 +62,24 @@ def get(fetcher: Fetcher = None) -> FearGreedIndex: description=response["rating"], last_update=datetime.datetime.fromisoformat(response["timestamp"]), ) + +def historical(fetcher: Fetcher = None) -> dict: + """Returns CNN's Fear & Greed Index historical data.""" + + if fetcher is None: + fetcher = Fetcher() + + response = fetcher()["fear_and_greed_historical"] + fear_greed_historical = [] + + if "data" in response: + historical_data = response["data"] + for data in historical_data: + fear_greed_historical.append(FearGreedIndex( + value=data["y"], + description=data["rating"], + last_update=datetime.datetime.fromtimestamp(data["x"] / 1000), + )) + + return fear_greed_historical + From 228160e616419d95109fcdc678386d82ae5cf368 Mon Sep 17 00:00:00 2001 From: Yush Kapoor <55430541+yushdotkapoor@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:53:52 -0800 Subject: [PATCH 2/8] Update __init__.py for historical method --- src/fear_and_greed/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fear_and_greed/__init__.py b/src/fear_and_greed/__init__.py index fabcb30..8cd67f1 100644 --- a/src/fear_and_greed/__init__.py +++ b/src/fear_and_greed/__init__.py @@ -1,3 +1,3 @@ -from .cnn import FearGreedIndex, get +from .cnn import FearGreedIndex, get, historical __version__ = "0.4" From caa984320b99e45d6dddd5334c7d322d5090ca4e Mon Sep 17 00:00:00 2001 From: Yush Kapoor <55430541+yushdotkapoor@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:42:04 -0500 Subject: [PATCH 3/8] Update cnn.py --- src/fear_and_greed/cnn.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/fear_and_greed/cnn.py b/src/fear_and_greed/cnn.py index 4c00a3d..bc8cba7 100644 --- a/src/fear_and_greed/cnn.py +++ b/src/fear_and_greed/cnn.py @@ -1,19 +1,9 @@ #! /usr/bin/env python3 import datetime -import os.path -import tempfile import typing - import requests -import requests_cache - -from random import choice -requests_cache.install_cache( - cache_name=os.path.join(tempfile.gettempdir(), "cnn_cache"), - expire_after=datetime.timedelta(minutes=1), -) URL = "https://production.dataviz.cnn.io/index/fearandgreed/graphdata" From 7bb5fa7176afc6e9fff3e499a6da0b2bb5f03920 Mon Sep 17 00:00:00 2001 From: Yush Kapoor <55430541+yushdotkapoor@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:58:36 -0500 Subject: [PATCH 4/8] forgot the random library --- src/fear_and_greed/cnn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fear_and_greed/cnn.py b/src/fear_and_greed/cnn.py index bc8cba7..1d4fc92 100644 --- a/src/fear_and_greed/cnn.py +++ b/src/fear_and_greed/cnn.py @@ -3,6 +3,7 @@ import datetime import typing import requests +from random import choice URL = "https://production.dataviz.cnn.io/index/fearandgreed/graphdata" From e4e21e6cc87b30974a4e54cc5fd657a432038067 Mon Sep 17 00:00:00 2001 From: Yush Kapoor <55430541+yushdotkapoor@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:29:33 -0500 Subject: [PATCH 5/8] Reverted to 228160e I'm still a git noob it seems, wasn't aware that subsequent commits get automatically included in pull request --- src/fear_and_greed/cnn.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/fear_and_greed/cnn.py b/src/fear_and_greed/cnn.py index 1d4fc92..4c00a3d 100644 --- a/src/fear_and_greed/cnn.py +++ b/src/fear_and_greed/cnn.py @@ -1,10 +1,19 @@ #! /usr/bin/env python3 import datetime +import os.path +import tempfile import typing + import requests +import requests_cache + from random import choice +requests_cache.install_cache( + cache_name=os.path.join(tempfile.gettempdir(), "cnn_cache"), + expire_after=datetime.timedelta(minutes=1), +) URL = "https://production.dataviz.cnn.io/index/fearandgreed/graphdata" From 8b5d3b6235027360db81de71805ff14492692154 Mon Sep 17 00:00:00 2001 From: Yush Kapoor Date: Tue, 10 Dec 2024 18:16:05 -0500 Subject: [PATCH 6/8] Deterministic Sorting and Error Handling The historical method raises error when no data is found Additionally, the data is sorted from distant to most recent in case the data source provides unsorted data --- src/fear_and_greed/cnn.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/fear_and_greed/cnn.py b/src/fear_and_greed/cnn.py index 4c00a3d..dafe8c2 100644 --- a/src/fear_and_greed/cnn.py +++ b/src/fear_and_greed/cnn.py @@ -69,17 +69,21 @@ def historical(fetcher: Fetcher = None) -> dict: if fetcher is None: fetcher = Fetcher() - response = fetcher()["fear_and_greed_historical"] + response = fetcher().get("fear_and_greed_historical", {}) fear_greed_historical = [] - if "data" in response: - historical_data = response["data"] - for data in historical_data: - fear_greed_historical.append(FearGreedIndex( - value=data["y"], - description=data["rating"], - last_update=datetime.datetime.fromtimestamp(data["x"] / 1000), - )) + if "data" not in response: + raise ValueError("No historical data found") + + historical_data = response["data"] + for data in historical_data: + fear_greed_historical.append(FearGreedIndex( + value=data["y"], + description=data["rating"], + last_update=datetime.datetime.fromtimestamp(data["x"] / 1000), + )) + + fear_greed_historical.sort(key=lambda x: x.last_update) return fear_greed_historical From 2c82f0f2cb27e527adc9f5682d10a403a305e50c Mon Sep 17 00:00:00 2001 From: Yush Kapoor Date: Tue, 10 Dec 2024 18:18:43 -0500 Subject: [PATCH 7/8] Created 3 test cases for historical Tests for the correct number of entries. Tests the first and last 5 entries of the historical data to make sure dataset is sorted in the correct order. I chose not to cross check the entire dataset to prevent bloating. We can reasonable assume based on the first and last five entries that method works as expected --- tests/test_cnn.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_cnn.py b/tests/test_cnn.py index 5da17ca..f3a9b32 100644 --- a/tests/test_cnn.py +++ b/tests/test_cnn.py @@ -37,6 +37,34 @@ def test_get(self): ) self.assertEqual(got, want) + def test_historical(self): + with open(FAKE_JSON_RESPONSE_GOLDEN_FILE, "rt") as fd: + # Lambda is evaluated lazily, force JSON load while file is open. + content = json.load(fd) + fake_fetcher = lambda: content + got = cnn.historical(fetcher=fake_fetcher) + + self.assertEqual(len(got), 269) + + first_five = [ + cnn.FearGreedIndex(51.03333333333334, "neutral", datetime.datetime.fromtimestamp(1619395200.0)), + cnn.FearGreedIndex(52.79999999999999, "neutral", datetime.datetime.fromtimestamp(1619481600.0)), + cnn.FearGreedIndex(54.133333333333326, "neutral", datetime.datetime.fromtimestamp(1619568000.0)), + cnn.FearGreedIndex(58.93333333333334, "greed", datetime.datetime.fromtimestamp(1619654400.0)), + cnn.FearGreedIndex(49.26666666666667, "neutral", datetime.datetime.fromtimestamp(1619740800.0)) + ] + + self.assertEqual(got[:5], first_five) + + last_five = [ + cnn.FearGreedIndex(41.1246, "fear", datetime.datetime.fromtimestamp(1650585600.0)), + cnn.FearGreedIndex(40.1512, "fear", datetime.datetime.fromtimestamp(1650672000.0)), + cnn.FearGreedIndex(40.1512, "fear", datetime.datetime.fromtimestamp(1650758400.0)), + cnn.FearGreedIndex(30.8254, "fear", datetime.datetime.fromtimestamp(1650844800.0)), + cnn.FearGreedIndex(30.8254, "fear", datetime.datetime.fromtimestamp(1650903669.254)) + ] + + self.assertEqual(got[-5:], last_five) if __name__ == "__main__": absltest.main() From 76fb1b53051cec3d251c00f9c8cc348e07237c4a Mon Sep 17 00:00:00 2001 From: Yush Kapoor Date: Tue, 10 Dec 2024 18:19:06 -0500 Subject: [PATCH 8/8] Updated README to include usage for the historical method --- README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f0f561..c48229b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ pip install fear-and-greed ``` # Usage example +## Fetch Current Fear & Greed Index ```python import fear_and_greed @@ -22,11 +23,37 @@ Returns a three-element namedtuple with (a) the current value of the Fear & Gree FearGreedIndex( value=31.4266, description='fear', - last_update=datetime.datetime(2022, 4, 25, 16, 51, 9, 254000, tzinfo=datetime.timezone.utc)), + last_update=datetime.datetime(2022, 4, 25, 16, 51, 9, 254000, tzinfo=datetime.timezone.utc), ) ``` -Requests to CNN's website are locally [cached](https://pypi.org/project/requests-cache/) for 1m. +## Fetch Historical Fear & Greed Data + +```python +historical_data = fear_and_greed.historical() +``` +The historical function provides a list of historical Fear & Greed Index values, each with its corresponding description and timestamp. + +```python +[ + FearGreedIndex( + value=80.0, + description='extreme greed', + last_update=datetime.datetime(2022, 4, 20, 0, 0, 0, tzinfo=datetime.timezone.utc) + ), + FearGreedIndex( + value=60.5, + description='greed', + last_update=datetime.datetime(2022, 4, 21, 0, 0, 0, tzinfo=datetime.timezone.utc) + ) +] +... +``` + +## Features +* Fetch the current Fear & Greed Index value along with its description and timestamp. +* Retrieve historical Fear & Greed Index data to analyze trends. +* Uses locally [cached](https://pypi.org/project/requests-cache/) requests to CNN's website for 1 minute to minimize network usage. [![Test workflow](https://github.com/vterron/fear-and-greed/actions/workflows/test.yml/badge.svg)](https://github.com/vterron/fear-and-greed/actions/workflows/test.yml) [![PyPI badge](https://img.shields.io/pypi/v/fear-and-greed?color=blue)](https://pypi.org/project/fear-and-greed/)