diff --git a/README.md b/README.md
index a1e19444e2c..ad4858f02f8 100644
--- a/README.md
+++ b/README.md
@@ -239,6 +239,7 @@ These are the ones where a key is necessary:
* Polygon: https://polygon.io
* Quandl: https://www.quandl.com/tools/api
* Reddit: https://www.reddit.com/prefs/apps
+* SentimentInvestor: https://sentimentinvestor.com
* Tradier: https://developer.tradier.com/getting_started
* Twitter: https://developer.twitter.com
@@ -260,6 +261,7 @@ Alternatively, you can also set them to the following environment variables:
| [Polygon](https://polygon.io) | GT_API_POLYGON_KEY |
| [Quandl](https://www.quandl.com) | GT_API_KEY_QUANDL |
| [Reddit](https://www.reddit.com) | GT_API_REDDIT_CLIENT_ID
GT_API_REDDIT_CLIENT_SECRET
GT_API_REDDIT_USERNAME
GT_API_REDDIT_USER_AGENT
GT_API_REDDIT_PASSWORD|
+| [SentimentInvestor](https://sentimentinvestor.com) | GT_API_SENTIMENTINVESTOR_TOKEN
GT_API_SENTIMENTINVESTOR_KEY |
| [Tradier](https://developer.tradier.com) | GT_TRADIER_TOKEN |
| [Twitter](https://developer.twitter.com) | GT_API_TWITTER_KEY
GT_API_TWITTER_SECRET_KEY
GT_API_TWITTER_BEARER_TOKEN |
diff --git a/gamestonk_terminal/behavioural_analysis/README.md b/gamestonk_terminal/behavioural_analysis/README.md
index 90414e70afe..0fba235a7ef 100644
--- a/gamestonk_terminal/behavioural_analysis/README.md
+++ b/gamestonk_terminal/behavioural_analysis/README.md
@@ -45,6 +45,13 @@ This menu aims to extrapolate behavioural analysis regarding a pre-loaded ticker
* [rise](#rise)
- top rising related queries with stock
+[SENTIMENT INVESTOR](#SENTIMENTINVESTOR)
+ * [metrics](#metrics)
+ - core social sentiment metrics for this stock
+ * [social](#social)
+ - social media figures for stock popularity
+ * [historical](#historical)
+ - plot the past week of data for a selected metric
## finbrain
```
@@ -251,3 +258,106 @@ Print top rising related queries with this stock's query. [Source: Google]
* -n : number of top rising related queries to print. Default 10.
+
+# SENTIMENTINVESTOR
+
+> **[Sentiment Investor](https://sentimentinvestor.com)** analyzes data from four major social media platforms to
+> generate hourly metrics on over 2,000 stocks. Sentiment provides volume and
+> sentiment metrics powered by proprietary NLP models.
+
+## Metric definitions
+
+### AHI (Absolute Hype Index)
+
+AHI is a measure of how much people are talking about a stock on social media.
+It is calculated by dividing the total number of mentions for the chosen stock
+on a social network by the mean number of mentions any stock receives on that
+social medium.
+
+### RHI (Relative Hype Index)
+
+RHI is a measure of whether people are talking about a stock more or less than
+usual, calculated by dividing the mean AHI for the past day by the mean AHI for
+for the past week for that stock.
+
+### Sentiment Score
+
+Sentiment score is the percentage of people talking positively about the stock.
+For each social network the number of positive posts/comments is divided by the
+total number of both positive and negative posts/comments.
+
+### SGP (Standard General Perception)
+
+SGP is a measure of whether people are more or less positive about a stock than
+usual. It is calculated by averaging the past day of sentiment values and then
+dividing it by the average of the past week of sentiment values.
+
+## metrics
+
+```
+usage: metrics [-h] [ticker]
+```
+
+The `metrics` command presents the four core metrics provided by Sentiment Investor,
+including AHI, RHI, sentiment and SGP, as described above.
+
+Optional arguments:
+- [optional, positional] ticker
+ ticker to use instead of the loaded one
+- `-h`, `--help`
+ show this help message
+
+Example output:
+
+
+
+## social
+
+```
+usage: social [-h] [ticker]
+```
+
+The `social` command prints the raw data for a given stock, including the number
+of mentions it has received on social media in the last hour and the sentiment
+score of those comments.
+
+Optional arguments:
+- [optional, positional] ticker
+ ticker to use instead of the loaded one
+- `-h`, `--help`
+ show this help message
+
+Example output:
+
+
+
+## historical
+
+```
+usage: historical [-t TICKER] [-s [{date,value}]] [-d [{asc,desc}]] [-h] [{sentiment,AHI,RHI,SGP}]
+```
+
+The `historical` command plots the past week of data for a selected
+SentimentInvestor core metric, one of:
+[AHI](#ahi), [RHI](#ahi), [sentiment](#sentiment) or [SGP](#SGP).
+It also outputs a sorted table of the mean value for that metric for each day.
+
+Positional arguments:
+- one of `sentiment`, `AHI`, `RHI` or `SGP`
+ (optional) the metric to plot (default: `sentiment`)
+
+Optional arguments:
+- `-t`, `--ticker TICKER`
+ ticker for which to fetch data
+- `-s`, `--sort [{date,value}]`
+ the parameter to sort output table by
+- `-d`, `--direction [{asc,desc}]`
+ the direction to sort the output table
+- `-h`, `--help`
+ show this help message
+
+Example output:
+
+
+
+
diff --git a/gamestonk_terminal/behavioural_analysis/ba_controller.py b/gamestonk_terminal/behavioural_analysis/ba_controller.py
index 5ab14d272da..3be9557a052 100644
--- a/gamestonk_terminal/behavioural_analysis/ba_controller.py
+++ b/gamestonk_terminal/behavioural_analysis/ba_controller.py
@@ -16,6 +16,7 @@ from gamestonk_terminal.behavioural_analysis import (
stocktwits_view,
finbrain_view,
finnhub_view,
+ sentimentinvestor_view,
)
@@ -46,6 +47,9 @@ class BehaviouralAnalysisController:
"rise",
"finbrain",
"stats",
+ "metrics",
+ "social",
+ "historical",
]
def __init__(self, ticker: str, start: datetime):
@@ -102,6 +106,11 @@ class BehaviouralAnalysisController:
print(" queries top related queries with this stock")
print(" rise top rising related queries with stock")
print("")
+ print("SentimentInvestor:")
+ print(" metrics core social sentiment metrics for this stock")
+ print(" social social media figures for stock popularity")
+ print(" historical plot the past week of data for a selected metric")
+ print("")
def switch(self, an_input: str):
"""Process and dispatch input
@@ -246,6 +255,18 @@ class BehaviouralAnalysisController:
"""Process stats command"""
finnhub_view.sentiment_stats(other_args, self.ticker)
+ def call_metrics(self, other_args: List[str]):
+ """Process metrics command"""
+ sentimentinvestor_view.metrics(self.ticker, other_args)
+
+ def call_social(self, other_args: List[str]):
+ """Process social command"""
+ sentimentinvestor_view.socials(self.ticker, other_args)
+
+ def call_historical(self, other_args: List[str]):
+ """Process historical command"""
+ sentimentinvestor_view.historical(self.ticker, other_args)
+
def menu(ticker: str, start: datetime, context: str = ""):
"""Behavioural Analysis Menu"""
diff --git a/gamestonk_terminal/behavioural_analysis/sentimentinvestor_view.py b/gamestonk_terminal/behavioural_analysis/sentimentinvestor_view.py
new file mode 100644
index 00000000000..9095057791d
--- /dev/null
+++ b/gamestonk_terminal/behavioural_analysis/sentimentinvestor_view.py
@@ -0,0 +1,611 @@
+import argparse
+import dataclasses
+import datetime
+import logging
+import multiprocessing
+import os
+import statistics
+import textwrap
+import time
+from typing import Union, Optional, List, Tuple
+
+import matplotlib.dates as mdates
+import pandas as pd
+import seaborn as sns
+from colorama import Fore, Style
+from matplotlib import pyplot as plt
+from sentipy.sentipy import Sentipy
+import tabulate
+
+from gamestonk_terminal import config_terminal as cfg
+from gamestonk_terminal.config_plot import PLOT_DPI
+from gamestonk_terminal.helper_funcs import (
+ parse_known_args_and_warn,
+ plot_autoscale,
+)
+
+sentipy: Sentipy = Sentipy(
+ token=cfg.API_SENTIMENTINVESTOR_TOKEN, key=cfg.API_SENTIMENTINVESTOR_KEY
+)
+"""Initialise SentiPy with the user's API token and key"""
+
+pd.plotting.register_matplotlib_converters()
+
+
+# suppress warning messages for a clean interface
+logging.getLogger().setLevel(logging.CRITICAL)
+
+
+@dataclasses.dataclass
+class _Boundary:
+ """Represents a strong or weak bounding inequality for categorising a value"""
+
+ min: Union[float, int]
+ "Minimum value that this metric could take"
+ max: Union[float, int]
+ "Maximum value that this metric could take"
+ strong: bool = False
+ "Whether this a strongly bounded inequality"
+
+ def categorise(self, num: Union[float, int]) -> Tuple[str, str]:
+ """
+ Categorise a given number using this bounding inequality
+
+ Parameters
+ ----------
+ num: the number to bucket
+
+ Returns
+ -------
+ A color string and a string with low / medium / high as appropriate
+
+ """
+
+ if num is None or self.min is None or self.max is None:
+ return Style.DIM + Fore.WHITE, "N/A"
+
+ boundaries = [self.min + (self.max - self.min) / 5 * i for i in range(1, 5)]
+
+ if (num <= self.min or num >= self.max) and self.strong:
+ return Fore.WHITE, "Extreme (anomaly?)"
+ if num < boundaries[0]:
+ return Style.BRIGHT + Fore.RED, "Much Lower"
+ if num < boundaries[1]:
+ return Fore.RED, "Lower"
+ if num < boundaries[2]:
+ return Fore.YELLOW, "Same"
+ if num < boundaries[3]:
+ return Fore.GREEN, "Higher"
+ return Style.BRIGHT + Fore.GREEN, "Much Higher"
+
+
+def _get_past_week_average(ticker: str, metric: str) -> float:
+ historical_data = [
+ dp
+ for dp in sentipy.historical(
+ ticker, metric, int(time.time() - 60 * 60 * 24 * 7), int(time.time())
+ ).values()
+ if dp is not None
+ ]
+ return statistics.mean(historical_data) if historical_data else None
+
+
+@dataclasses.dataclass
+class _MetricInfo:
+ name: str
+ """Metric attribute name"""
+ title: str
+ """Human-friendly attribute name"""
+ description: str
+ """Metric attribute description"""
+ format: str
+ """Format string for presenting the value"""
+
+
+class _Metric(_MetricInfo):
+ boundary: _Boundary
+ """A contextual range to for this metric"""
+ ticker: str
+ """Ticker symbol for which this metric relates"""
+ value: Union[float, int]
+ """Value"""
+
+ def __init__(
+ self,
+ metric_info: _MetricInfo,
+ ticker: str,
+ value: Union[float, int],
+ boundary: Optional[_Boundary] = None,
+ ):
+ super().__init__(**dataclasses.asdict(metric_info))
+
+ weekly_mean = _get_past_week_average(ticker, self.name)
+ self.boundary = (
+ _Boundary(0, None if weekly_mean is None else 2 * weekly_mean)
+ if boundary is None
+ else boundary
+ )
+ self.ticker = ticker
+ self.value = value
+
+ def visualise(self) -> Tuple[str, str, str, str]:
+ color, category = self.boundary.categorise(self.value)
+ return (
+ color + self.title,
+ category,
+ "N/A" if self.value is None else f"{self.value:{self.format}}",
+ self.description + Style.RESET_ALL,
+ )
+
+
+core_metrics = [
+ _MetricInfo(
+ "sentiment",
+ "Sentiment Score",
+ "the percentage of people that are talking positively about a stock",
+ ".0%",
+ ),
+ _MetricInfo(
+ "AHI",
+ "Average Hype Index",
+ "how much people are talking about a stock on social media",
+ ".3f",
+ ),
+ _MetricInfo(
+ "RHI",
+ "Relative Hype Index",
+ "whether people are talking about a stock more or less than usual",
+ ".3f",
+ ),
+ _MetricInfo(
+ "SGP",
+ "Standard General Perception",
+ "whether people are more or less positive about a stock than usual",
+ ".3f",
+ ),
+]
+
+social_metrics = [
+ _MetricInfo(
+ "reddit_comment_mentions",
+ "Reddit Comment Mentions",
+ "the number of mentions this stock has received in Reddit comments",
+ "n",
+ ),
+ _MetricInfo(
+ "reddit_comment_sentiment",
+ "Reddit Comment Sentiment",
+ "percentage of Reddit comments mentioning this stock in a positive light",
+ ".0%",
+ ),
+ _MetricInfo(
+ "reddit_post_mentions",
+ "Reddit Post Mentions",
+ "the number of mentions this stock has received in Reddit posts",
+ "n",
+ ),
+ _MetricInfo(
+ "reddit_post_sentiment",
+ "Reddit Post Sentiment",
+ "percentage of Reddit posts mentioning this stock in a positive light",
+ ".0%",
+ ),
+ _MetricInfo(
+ "tweet_mentions",
+ "Twitter Mentions",
+ "the number of mentions this stock has received on Twitter",
+ "n",
+ ),
+ _MetricInfo(
+ "tweet_sentiment",
+ "Twitter Sentiment",
+ "percentage of tweets mentioning this stock in a positive light",
+ ".0%",
+ ),
+ _MetricInfo(
+ "stocktwits_post_mentions",
+ "Stocktwits Mentions",
+ "the number of mentions this stock has received in Stocktwits posts",
+ "n",
+ ),
+ _MetricInfo(
+ "stocktwits_post_sentiment",
+ "Stocktwits Sentiment",
+ "percentage of Stocktwits posts mentioning this stock in a positive light",
+ ".0%",
+ ),
+ _MetricInfo(
+ "yahoo_finance_comment_mentions",
+ "Yahoo! Finance Comment Mentions",
+ "the number of mentions this stock has received in Yahoo! Finance comments",
+ "n",
+ ),
+ _MetricInfo(
+ "yahoo_finance_comment_sentiment",
+ "Yahoo! Finance Comment Sentiment",
+ "percentage of Yahoo! Finance comments mentioning this stock in a positive light",
+ ".0%",
+ ),
+]
+
+
+def _contextualise_metrics(
+ data: object, ticker: str, metric_infos: List[_MetricInfo]
+) -> Optional[List[_Metric]]:
+ arguments = []
+ for metric_info in metric_infos:
+ if not hasattr(data, metric_info.name):
+ logging.error(
+ "data for %s stock is incomplete. If you believe this is an error, "
+ "please contact hello@sentimentinvestor.com",
+ ticker,
+ )
+ return None
+ arguments.append((metric_info, ticker, data.__getattribute__(metric_info.name)))
+
+ with multiprocessing.Pool(os.cpu_count()) as pool:
+ return pool.starmap(_Metric, arguments)
+
+
+def _tabulate_metrics(ticker: str, metrics_list: List[_Metric]):
+ table_data = []
+ table_headers = [
+ f"{Style.BRIGHT}{ticker}{Style.RESET_ALL} Metrics",
+ "vs Past 7 Days",
+ "Value",
+ "Description",
+ ]
+
+ for metric in metrics_list:
+ table_data.append(metric.visualise())
+
+ return tabulate.tabulate(table_data, table_headers, tablefmt="grid")
+
+
+def _customise_plot() -> None:
+ sns.set(
+ font="Arial",
+ style="darkgrid",
+ rc={
+ "axes.axisbelow": False,
+ "axes.edgecolor": "lightgrey",
+ "axes.facecolor": "None",
+ "axes.grid": False,
+ "axes.labelcolor": "dimgrey",
+ "axes.spines.right": False,
+ "axes.spines.top": False,
+ "figure.facecolor": "white",
+ "lines.solid_capstyle": "round",
+ "patch.edgecolor": "w",
+ "patch.force_edgecolor": True,
+ "text.color": "dimgrey",
+ "xtick.bottom": False,
+ "xtick.color": "dimgrey",
+ "xtick.direction": "out",
+ "xtick.top": False,
+ "ytick.color": "dimgrey",
+ "ytick.direction": "out",
+ "ytick.left": False,
+ "ytick.right": False,
+ },
+ )
+ sns.despine(left=True, bottom=True)
+ sns.color_palette("pastel")
+ plt.legend(frameon=False)
+
+
+def _parse_args_for_ticker(other_args: List[str], ticker: str, command: str, desc: str):
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog=command,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=desc,
+ )
+
+ parser.add_argument(
+ "ticker",
+ action="store",
+ type=str,
+ nargs="?",
+ default=ticker,
+ help="ticker to use instead of the loaded one",
+ )
+
+ return parse_known_args_and_warn(parser, other_args)
+
+
+def metrics(ticker: str, other_args: List[str]) -> None:
+ command_description = f"""
+ {Style.BRIGHT}Sentiment Investor{Style.RESET_ALL} analyzes data from four major social media platforms to
+ generate hourly metrics on over 2,000 stocks. Sentiment provides volume and
+ sentiment metrics powered by proprietary NLP models.
+
+ The {Style.BRIGHT}metrics{Style.RESET_ALL} command prints the following realtime metrics:
+
+ {Style.BRIGHT}AHI (Absolute Hype Index){Style.RESET_ALL}
+ ---
+ AHI is a measure of how much people are talking about a stock on social media.
+ It is calculated by dividing the total number of mentions for the chosen stock
+ on a social network by the mean number of mentions any stock receives on that
+ social medium.
+
+ {Style.BRIGHT}RHI (Relative Hype Index){Style.RESET_ALL}
+ ---
+ RHI is a measure of whether people are talking about a stock more or less than
+ usual, calculated by dividing the mean AHI for the past day by the mean AHI for
+ for the past week for that stock.
+
+ {Style.BRIGHT}Sentiment Score{Style.RESET_ALL}
+ ---
+ Sentiment score is the percentage of people talking positively about the stock.
+ For each social network the number of positive posts/comments is divided by the
+ total number of both positive and negative posts/comments.
+
+ {Style.BRIGHT}SGP (Standard General Perception){Style.RESET_ALL}
+ ---
+ SGP is a measure of whether people are more or less positive about a stock than
+ usual. It is calculated by averaging the past day of sentiment values and then
+ dividing it by the average of the past week of sentiment values.
+ """
+
+ ns_parser = _parse_args_for_ticker(
+ other_args=other_args,
+ ticker=ticker,
+ command="metrics",
+ desc=textwrap.dedent(command_description),
+ )
+
+ try:
+
+ if not ns_parser:
+ logging.error("There was an error in parsing the command-line arguments.")
+ return
+
+ if not sentipy.supported(ns_parser.ticker):
+ print("This stock is not supported by the SentimentInvestor API.")
+ return
+
+ data = sentipy.parsed(ns_parser.ticker)
+
+ metric_values = _contextualise_metrics(data, ns_parser.ticker, core_metrics)
+
+ if not metric_values:
+ logging.error("No data available or an error occurred.")
+ return
+
+ print(_tabulate_metrics(ns_parser.ticker, metric_values))
+ print()
+
+ except Exception as e:
+ logging.error(e)
+ print(e, "\n")
+
+
+def socials(ticker: str, other_args: List[str]) -> None:
+ command_description = f"""
+ {Style.BRIGHT}Sentiment Investor{Style.RESET_ALL} analyzes data from four major social media platforms to
+ generate hourly metrics on over 2,000 stocks. Sentiment provides volume and
+ sentiment metrics powered by proprietary NLP models.
+
+ The {Style.BRIGHT}social{Style.RESET_ALL} command prints the raw data for a given stock, including the number
+ of mentions it has received on social media in the last hour and the sentiment
+ score of those comments.
+ """
+
+ try:
+
+ ns_parser = _parse_args_for_ticker(
+ other_args=other_args,
+ ticker=ticker,
+ command="social",
+ desc=textwrap.dedent(command_description),
+ )
+
+ if not ns_parser:
+ logging.error("There was an error in parsing the command-line arguments.")
+ return
+
+ if not sentipy.supported(ns_parser.ticker):
+ print("This stock is not supported by the SentimentInvestor API.")
+ return
+
+ data = sentipy.raw(ns_parser.ticker)
+
+ metric_values = _contextualise_metrics(data, ns_parser.ticker, social_metrics)
+
+ if not metric_values:
+ logging.error("No data available or an error occurred.")
+ return
+
+ print(_tabulate_metrics(ns_parser.ticker, metric_values))
+ print()
+
+ except Exception as e:
+ logging.error(e)
+ print(e, "\n")
+
+
+def historical(ticker: str, other_args: List[str]) -> None:
+ command_description = f"""
+ {Style.BRIGHT}Sentiment Investor{Style.RESET_ALL} analyzes data from four major social media platforms to
+ generate hourly metrics on over 2,000 stocks. Sentiment provides volume and
+ sentiment metrics powered by proprietary NLP models.
+
+ The {Style.BRIGHT}historical{Style.RESET_ALL} command plots the past week of data for a selected metric, one of:
+
+ {Style.BRIGHT}AHI (Absolute Hype Index){Style.RESET_ALL}
+ ---
+ AHI is a measure of how much people are talking about a stock on social media.
+ It is calculated by dividing the total number of mentions for the chosen stock
+ on a social network by the mean number of mentions any stock receives on that
+ social medium.
+
+ {Style.BRIGHT}RHI (Relative Hype Index){Style.RESET_ALL}
+ ---
+ RHI is a measure of whether people are talking about a stock more or less than
+ usual, calculated by dividing the mean AHI for the past day by the mean AHI for
+ for the past week for that stock.
+
+ {Style.BRIGHT}Sentiment Score{Style.RESET_ALL}
+ ---
+ Sentiment score is the percentage of people talking positively about the stock.
+ For each social network the number of positive posts/comments is divided by the
+ total number of both positive and negative posts/comments.
+
+ {Style.BRIGHT}SGP (Standard General Perception){Style.RESET_ALL}
+ ---
+ SGP is a measure of whether people are more or less positive about a stock than
+ usual. It is calculated by averaging the past day of sentiment values and then
+ dividing it by the average of the past week of sentiment values.
+ """
+
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="historical",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=textwrap.dedent(command_description),
+ )
+ parser.add_argument(
+ "-t",
+ "--ticker",
+ action="store",
+ dest="ticker",
+ type=str,
+ default=ticker,
+ help="ticker for which to fetch data",
+ )
+ parser.add_argument(
+ "-s",
+ "--sort",
+ action="store",
+ type=str,
+ default="date",
+ help="the parameter to sort output table by",
+ dest="sort_param",
+ nargs="?",
+ choices=["date", "value"],
+ )
+ parser.add_argument(
+ "-d",
+ "--direction",
+ action="store",
+ type=str,
+ default="desc",
+ help="the direction to sort the output table",
+ dest="sort_dir",
+ nargs="?",
+ choices=["asc", "desc"],
+ )
+ parser.add_argument(
+ "metric",
+ type=str,
+ action="store",
+ default="sentiment",
+ nargs="?",
+ choices=["sentiment", "AHI", "RHI", "SGP"],
+ help="the metric to plot",
+ )
+
+ try:
+
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ logging.error("There was an error in parsing the command-line arguments.")
+ return
+
+ if not sentipy.supported(ns_parser.ticker):
+ print("This stock is not supported by the SentimentInvestor API.")
+ return
+
+ data = sentipy.historical(
+ ns_parser.ticker,
+ ns_parser.metric,
+ int(time.time() - 60 * 60 * 24 * 7),
+ int(time.time()),
+ )
+ df = pd.DataFrame.from_dict(
+ {
+ "date": map(datetime.datetime.utcfromtimestamp, data.keys()),
+ ns_parser.metric: data.values(),
+ }
+ )
+
+ df.sort_index(ascending=True, inplace=True)
+
+ if df.empty:
+ logging.error("The dataset is empty, something must have gone wrong")
+ return
+
+ _customise_plot()
+
+ # use seaborn to lineplot
+ ax = sns.lineplot(
+ data=df,
+ x="date",
+ y=ns_parser.metric,
+ legend=False,
+ label=[ns_parser.metric],
+ )
+
+ # always show zero on the y-axis
+ plt.ylim(bottom=0)
+
+ # set the x-axis date formatting
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%x"))
+
+ # scale the plot appropriately
+ plt.gcf().set_size_inches(plot_autoscale())
+ plt.gcf().set_dpi(PLOT_DPI)
+ plt.gcf().autofmt_xdate()
+
+ # fill below the line
+ plt.fill_between(df.date, df[ns_parser.metric], alpha=0.3)
+
+ # add title e.g. AAPL sentiment since 22/07/21
+ plt.title(
+ f"{ns_parser.ticker} {ns_parser.metric} since {min(df.date).strftime('%x')}"
+ )
+
+ plt.show()
+
+ ###
+
+ boundary = _Boundary(
+ 0, max(df[ns_parser.metric].max(), 2 * df[ns_parser.metric].mean())
+ )
+
+ # average for each day
+ aggregated = df.resample("D", on="date").mean()
+
+ # reverse the ordering if requested
+ aggregated.sort_values(
+ ns_parser.metric
+ if ns_parser.sort_param == "value"
+ else ns_parser.sort_param,
+ axis=0,
+ ascending=ns_parser.sort_dir == "asc",
+ inplace=True,
+ )
+
+ # format the date according to user's locale
+ aggregated.index = aggregated.index.strftime("%x")
+
+ # apply coloring to every value
+ aggregated[ns_parser.metric] = [
+ boundary.categorise(value)[0] + str(value) + Style.RESET_ALL
+ for value in aggregated[ns_parser.metric]
+ ]
+
+ print(
+ tabulate.tabulate(
+ aggregated,
+ headers=["Day", f"average {ns_parser.metric}"],
+ tablefmt="psql",
+ floatfmt=".3f",
+ )
+ )
+ print()
+
+ except Exception as e:
+ logging.error(e)
+ print(e, "\n")
diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py
index b66ccf3e650..2d5e74128ce 100644
--- a/gamestonk_terminal/config_terminal.py
+++ b/gamestonk_terminal/config_terminal.py
@@ -70,3 +70,9 @@ API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME"
# https://iexcloud.io
API_IEX_TOKEN = os.getenv("GT_API_IEX_KEY") or "REPLACE_ME"
+
+# https://www.sentimentinvestor.com
+API_SENTIMENTINVESTOR_KEY = os.getenv("GT_API_SENTIMENTINVESTOR_KEY") or "REPLACE_ME"
+API_SENTIMENTINVESTOR_TOKEN = (
+ os.getenv("GT_API_SENTIMENTINVESTOR_TOKEN") or "REPLACE_ME"
+)
diff --git a/gamestonk_terminal/cryptocurrency/coinpaprika/coinpaprika_view.py b/gamestonk_terminal/cryptocurrency/coinpaprika/coinpaprika_view.py
index c0104f5e511..221b9b637f2 100644
--- a/gamestonk_terminal/cryptocurrency/coinpaprika/coinpaprika_view.py
+++ b/gamestonk_terminal/cryptocurrency/coinpaprika/coinpaprika_view.py
@@ -70,7 +70,8 @@ CURRENCIES = [
PLATFORMS = paprika.get_all_contract_platforms()["platform_id"].tolist()
COINS = paprika.get_list_of_coins()
COINS_DCT = dict(zip(COINS.id, COINS.symbol))
-EXCHANGES = paprika.get_list_of_exchanges()
+# see https://github.com/GamestonkTerminal/GamestonkTerminal/pull/562#issuecomment-887842888
+# EXCHANGES = paprika.get_list_of_exchanges()
def global_market(other_args: List[str]):
diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md
index 64a9ef49588..3ce888cae50 100644
--- a/gamestonk_terminal/discovery/README.md
+++ b/gamestonk_terminal/discovery/README.md
@@ -46,6 +46,10 @@ This menu aims to discover new stocks, and the usage of the following commands a
* dark pool short position [Stockgrid]
* [shortvol](#shortvol)
* short interest and days to cover [Stockgrid]
+* [popular](#popular)
+ * most popular stocks on social media right now [SentimentInvestor.com]
+* [emerging](#emerging)
+ * stocks that are being talked about more than usual [SentimentInvestor.com]
## ipo
@@ -327,3 +331,55 @@ Print short interest and days to cover. [Source: Stockgrid]
+## popular
+
+```
+usage: popular [-l LIMIT] [-h]
+```
+
+The `popular` command prints the top `n` (default=10) stocks with the highest Average Hype Index right now.
+
+### AHI (Absolute Hype Index)
+
+AHI is a measure of how much people are talking about a stock on social media.
+It is calculated by dividing the total number of mentions for the chosen stock
+on a social network by the mean number of mentions any stock receives on that
+social medium.
+
+> **[Sentiment Investor](https://sentimentinvestor.com)** analyzes data from four major social media platforms to
+> generate hourly metrics on over 2,000 stocks. Sentiment provides volume and
+> sentiment metrics powered by proprietary NLP models.
+
+Optional arguments:
+- `-l`, `--limit LIMIT`
+ the maximum number of stocks to retrieve
+- `-h`, `--help`
+ show this help message
+
+
+
+## emerging
+
+```
+usage: emerging [-l LIMIT] [-h]
+```
+
+The `emerging` command prints the top `n` (default=10) stocks with the highest Relative Hype Index right now.
+
+### RHI (Relative Hype Index)
+RHI is a measure of whether people are talking about a stock more or less than
+usual, calculated by dividing the mean AHI for the past day by the mean AHI for
+for the past week for that stock.
+
+> **[Sentiment Investor](https://sentimentinvestor.com)** analyzes data from four major social media platforms to
+> generate hourly metrics on over 2,000 stocks. Sentiment provides volume and
+> sentiment metrics powered by proprietary NLP models.
+
+
+Optional arguments:
+- `-l`, `--limit LIMIT`
+ the maximum number of stocks to retrieve
+- `-h`, `--help`
+ show this help message
+
+
diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py
index c3f8dfb0044..9dfe92bf1b6 100644
--- a/gamestonk_terminal/discovery/disc_controller.py
+++ b/gamestonk_terminal/discovery/disc_controller.py
@@ -24,6 +24,7 @@ from gamestonk_terminal.discovery import (
finra_ats_view,
finnhub_view,
stockgrid_view,
+ sentimentinvestor_view,
)
@@ -59,6 +60,8 @@ class DiscoveryController:
"darkpool",
"darkshort",
"shortvol",
+ "popular",
+ "emerging",
]
def __init__(self):
@@ -112,6 +115,12 @@ class DiscoveryController:
)
print(" darkshort dark pool short position [Stockgrid.io]")
print(" shortvol short interest and days to cover [Stockgrid.io]")
+ print(
+ " popular show most popular stocks on social media right now [SentimentInvestor.com]"
+ )
+ print(
+ " emerging show stocks that are being talked about more than usual [SentimentInvestor.com]"
+ )
print("")
def switch(self, an_input: str):
@@ -255,6 +264,14 @@ class DiscoveryController:
"""Process shortvol command"""
stockgrid_view.shortvol(other_args)
+ def call_popular(self, other_args: List[str]):
+ """Process popular command"""
+ sentimentinvestor_view.sort("AHI", other_args, "popular")
+
+ def call_emerging(self, other_args: List[str]):
+ """Process popular command"""
+ sentimentinvestor_view.sort("RHI", other_args, "emerging")
+
def menu():
"""Discovery Menu"""
diff --git a/gamestonk_terminal/discovery/sentimentinvestor_view.py b/gamestonk_terminal/discovery/sentimentinvestor_view.py
new file mode 100644
index 00000000000..a13dd7b1459
--- /dev/null
+++ b/gamestonk_terminal/discovery/sentimentinvestor_view.py
@@ -0,0 +1,91 @@
+import argparse
+import logging
+import textwrap
+from typing import Any, List
+
+from colorama import Style
+from sentipy.sentipy import Sentipy
+from tabulate import tabulate
+
+from gamestonk_terminal import config_terminal as cfg
+from gamestonk_terminal.helper_funcs import parse_known_args_and_warn
+
+sentipy: Sentipy = Sentipy(
+ token=cfg.API_SENTIMENTINVESTOR_TOKEN, key=cfg.API_SENTIMENTINVESTOR_KEY
+)
+"""Initialise SentiPy with the user's API token and key"""
+
+
+__command_descriptions = {
+ "popular": f"""
+ The {Style.BRIGHT}popular{Style.RESET_ALL} command prints the stocks with highest Average Hype Index right now.
+
+ {Style.BRIGHT}AHI (Absolute Hype Index){Style.RESET_ALL}
+ ---
+ AHI is a measure of how much people are talking about a stock on social media.
+ It is calculated by dividing the total number of mentions for the chosen stock
+ on a social network by the mean number of mentions any stock receives on that
+ social medium.
+
+ ===
+
+ {Style.BRIGHT}Sentiment Investor{Style.RESET_ALL} analyzes data from four major social media platforms to
+ generate hourly metrics on over 2,000 stocks. Sentiment provides volume and
+ sentiment metrics powered by proprietary NLP models.
+ """,
+ "emerging": f"""
+ The {Style.BRIGHT}emerging{Style.RESET_ALL} command prints the stocks with highest Relative Hype Index right now.
+
+ {Style.BRIGHT}RHI (Relative Hype Index){Style.RESET_ALL}
+ ---
+ RHI is a measure of whether people are talking about a stock more or less than
+ usual, calculated by dividing the mean AHI for the past day by the mean AHI for
+ for the past week for that stock.
+
+ ===
+
+ {Style.BRIGHT}Sentiment Investor{Style.RESET_ALL} analyzes data from four major social media platforms to
+ generate hourly metrics on over 2,000 stocks. Sentiment provides volume and
+ sentiment metrics powered by proprietary NLP models.
+ """,
+}
+
+
+def sort(metric: str, other_args: List[str], command_name: str) -> None:
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog=command_name,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=textwrap.dedent(__command_descriptions[command_name]),
+ )
+
+ parser.add_argument(
+ "-l",
+ "--limit",
+ action="store",
+ dest="limit",
+ type=int,
+ default=10,
+ help="the maximum number of stocks to retrieve",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ data = sentipy.sort(metric, ns_parser.limit)
+
+ table: List[List[Any]] = []
+ for index, stock in enumerate(data):
+ if not hasattr(stock, "symbol") or not hasattr(stock, metric):
+ logging.warning("data for stock %s is incomplete, ignoring", index + 1)
+ table.append([])
+ else:
+ table.append([index + 1, stock.symbol, stock.__getattribute__(metric)])
+
+ print(tabulate(table, headers=["Rank", "Ticker", metric], floatfmt=".3f"))
+ print()
+ except Exception as e:
+ logging.error(e)
+ print(e, "\n")
diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py
index c4412ca913d..b3cca1f15db 100644
--- a/gamestonk_terminal/main_helper.py
+++ b/gamestonk_terminal/main_helper.py
@@ -867,6 +867,20 @@ def check_api_keys():
else:
key_dict["BINANCE"] = "defined, not tested"
+ # SentimentInvestor keys
+ si_keys = [cfg.API_SENTIMENTINVESTOR_KEY, cfg.API_SENTIMENTINVESTOR_TOKEN]
+ if "REPLACE_ME" in si_keys:
+ key_dict["SENTIMENT_INVESTOR"] = "Not defined"
+ else:
+ account = requests.get(
+ f"https://api.sentimentinvestor.com/v4/account"
+ f"?token={cfg.API_SENTIMENTINVESTOR_TOKEN}&key={cfg.API_SENTIMENTINVESTOR_KEY}"
+ )
+ if account.ok and account.json().get("success", False):
+ key_dict["SENTIMENT_INVESTOR"] = "Defined, test passed"
+ else:
+ key_dict["SENTIMENT_INVESTOR"] = "Defined, test unsuccessful"
+
print(
tabulate(
pd.DataFrame(key_dict.items()),
@@ -966,6 +980,7 @@ def about_us():
f"{Fore.CYAN}FinBrain: {Style.RESET_ALL}https://finbrain.tech\n"
f"{Fore.CYAN}Quiver Quantitative: {Style.RESET_ALL}https://www.quiverquant.com\n"
f"{Fore.CYAN}Ops.Syncretism: {Style.RESET_ALL}https://ops.syncretism.io/api.html\n"
+ f"{Fore.CYAN}SentimentInvestor: {Style.RESET_ALL}https://sentimentinvestor.com\n"
f"\n{Fore.RED}"
"DISCLAIMER: Trading in financial instruments involves high risks including the risk of losing some, "
"or all, of your investment amount, and may not be suitable for all investors. Before deciding to trade in "
diff --git a/poetry.lock b/poetry.lock
index d5fef8979e8..407b917beab 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2380,6 +2380,28 @@ python-versions = "*"
[package.extras]
win32 = ["pywin32"]
+[[package]]
+name = "sentiment-investor"
+version = "2.0.0"
+description = "Access the Sentiment Investor API through Python"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+sentipy = "*"
+
+[[package]]
+name = "sentipy"
+version = "0.2.0"
+description = "Python implementations of Sentinel 2 Toolbox L2 products & spectral indices"
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+numpy = ">=1.18.4,<2.0.0"
+
[[package]]
name = "six"
version = "1.15.0"
@@ -3030,7 +3052,7 @@ prediction = ["tensorflow", "pmdarima"]
[metadata]
lock-version = "1.1"
python-versions = "^3.6.8"
-content-hash = "47ea04f7495d8f1e468fcf36dc988d67aacd436a44149261bd77c631b5fc4a72"
+content-hash = "f0f4648cb473e92ab7e49380ac8a8b7f0ec9ec05e8cf1c8c8bc2ffb3a390e599"
[metadata.files]
absl-py = [
@@ -3247,8 +3269,8 @@ chardet = [
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
charset-normalizer = [
- {file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"},
- {file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"},
+ {file = "charset-normalizer-2.0.1.tar.gz", hash = "sha256:ad0da505736fc7e716a8da15bf19a985db21ac6415c26b34d2fafd3beb3d927e"},
+ {file = "charset_normalizer-2.0.1-py3-none-any.whl", hash = "sha256:b68b38179052975093d71c1b5361bf64afd80484697c1f27056e50593e695ceb"},
]
click = [
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
@@ -4698,6 +4720,14 @@ send2trash = [
{file = "Send2Trash-1.7.1-py3-none-any.whl", hash = "sha256:c20fee8c09378231b3907df9c215ec9766a84ee20053d99fbad854fe8bd42159"},
{file = "Send2Trash-1.7.1.tar.gz", hash = "sha256:17730aa0a33ab82ed6ca76be3bb25f0433d0014f1ccf63c979bab13a5b9db2b2"},
]
+sentiment-investor = [
+ {file = "sentiment-investor-2.0.0.tar.gz", hash = "sha256:ff97986fbd414e8166f92e4503b3b9cb8c583bff860aaba1dc9a3536666b57a4"},
+ {file = "sentiment_investor-2.0.0-py3-none-any.whl", hash = "sha256:e5744dd6e591d52d44d2ea51b1c5483e434affc598ea1a4b0cabab02f0d455ec"},
+]
+sentipy = [
+ {file = "sentipy-0.2.0-py3-none-any.whl", hash = "sha256:ca80a3cc284c08de8c7df0ee9bccff27fb2f4b07e8aa7f0dc52683e765855287"},
+ {file = "sentipy-0.2.0.tar.gz", hash = "sha256:dfcb4d5caeee6606714bc6e00966692ebf637616ce9215c69768583ad757efac"},
+]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
diff --git a/pyproject.toml b/pyproject.toml
index 8cc65b2e6c8..14f5b2ad9da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -76,6 +76,7 @@ GitPython = "^3.1.17"
fear-greed-index = "^0.1.3"
pyEX = "^0.5.0"
charset-normalizer = "2.0.1"
+sentiment-investor = "^2.0.0"
[tool.poetry.dev-dependencies]
pytest = "^6.2.2"
diff --git a/requirements-full.txt b/requirements-full.txt
index 7659b5e765d..f55d741ebcd 100644
--- a/requirements-full.txt
+++ b/requirements-full.txt
@@ -178,6 +178,7 @@ scs==2.1.4; python_full_version >= "3.6.1" and python_full_version < "4.0.0" and
seaborn==0.11.1; python_version >= "3.6"
selenium==3.141.0
send2trash==1.7.1; python_version >= "3.6"
+sentiment-investor==2.0.0
six==1.15.0; python_full_version >= "3.6.1" and python_version >= "3.5" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.5") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0")
smmap==4.0.0; python_version >= "3.6"
snowballstemmer==2.1.0; python_full_version >= "3.6.8" and python_full_version < "4.0.0" and python_version >= "3.6"
diff --git a/requirements.txt b/requirements.txt
index fb91f10cdbe..56d43aafc34 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -163,6 +163,7 @@ scs==2.1.4; python_full_version >= "3.6.1" and python_full_version < "4.0.0" and
seaborn==0.11.1; python_version >= "3.6"
selenium==3.141.0
send2trash==1.7.1; python_version >= "3.6"
+sentiment-investor==2.0.0
six==1.15.0; python_full_version >= "3.6.1" and python_version >= "3.5" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.5") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
smmap==4.0.0; python_version >= "3.6"
snowballstemmer==2.1.0; python_full_version >= "3.6.8" and python_full_version < "4.0.0" and python_version >= "3.6"