Overall Statistics
Total Orders
159
Average Win
1.56%
Average Loss
-1.19%
Compounding Annual Return
97.424%
Drawdown
23.200%
Expectancy
0.139
Start Equity
100000
End Equity
125458.52
Net Profit
25.459%
Sharpe Ratio
1.55
Sortino Ratio
2.627
Probabilistic Sharpe Ratio
59.880%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
1.31
Alpha
0.537
Beta
-0.169
Annual Standard Deviation
0.349
Annual Variance
0.122
Information Ratio
1.03
Tracking Error
0.544
Treynor Ratio
-3.202
Total Fees
$792.20
Estimated Strategy Capacity
$15000000.00
Lowest Capacity Asset
TSLA UNU3P8Y3WFAD
Portfolio Turnover
258.68%
# region imports
from AlgorithmImports import *
# endregion

class SmoothBlueBull(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2023, 11, 1)
        self.SetEndDate(2024, 3, 1)
        self.SetCash(100000)
        self.tsla = self.AddEquity("TSLA", Resolution.Minute)

    def OnData(self, data: Slice):
        if not self.Portfolio.Invested:
            self.SetHoldings(self.tsla.Symbol, 1)
#region imports
from AlgorithmImports import *
#endregion

# - Starting in the Research Environment (research.ipynb), we gather the data we need.
# - We subscribe to all of the assets that we want to analyze. In this case, we use TSLA.
# - We then subscribe to the TiingoNews data of TSLA, which provides news articles that feature TSLA.
# - We gather the TiingoNews articles from 2023/11/01 - 2024/03/01. 
# - We choose these months because they are the most recent articles and we limit it to 5 months to stay within our OpenAI quota.
# - In the notebook, we next group the articles by date and by hour.
# - We exclude duplicated articles (TiingoNews articles that have the same title as one of the past 100 article titles).
# - The following image shows the number of articles per day and the cumulative number of articles during the time period.
# - The following image shows the number of articles per hour and the cumulative number of articles during the time period.
# - Next, we iterate through each hour of articles and ask GPT4 through the OpenAI API to summarize all the articles that were released during the hour with a single sentiment score between -10 and +10
# - Here is the prompt we use:
# -   "Article <i> title: <title>
# -    Article <i> description: <Description>
# -    . . .
# -   Review the news titles and descriptions above and then create an aggregated sentiment score which represents the emotional positivity towards TSLA after seeing all of the news articles. -10 represents extreme negative sentiment, +10 represents extreme positive sentiment, and 0 represents neutral sentiment. Reply ONLY with the numerical value in JSON format. For example, `{ "sentiment-score": 0 }`"
# - We gather all of the sentiment scores into a DataFrame and then save the results as a CSV into the ObjectStore.
# - 
# - Now, we transition to the backtesting environment to build an algorithm that uses the data we just created.
# - In the main.py file, we define a custom data class that reads the CSV data from the Object Store and injects it into the algorithm.
# - We apply a RateOfChange indicator to the data set so that we can observe the direction of sentiment changes.
# - The trading logic is:
# -   If sentiment is flat/increasing and not already long, long.
# -   If sentiment is negative and and sentiment is decreasing and not already short, short.
# - The algorithm achieves a 1.695 Sharpe ratio.
# - In contrast, the benchmark (buy and hold TSLA) achieves a -0.06 Sharpe ratio.
# - Therefore, the strategy outperforms the benchmark in terms of risk-adjusted returns.
# region imports
from AlgorithmImports import *
# endregion


class LLMSummarizationAlgorithm(QCAlgorithm):
    """
    This algorithm demonstrates how to load in the sentiment scores from
    the Object Store and use them to inform trading decisions. Before 
    you can run this algorithm, run the cells in the `research.ipynb` 
    file.
    """

    def initialize(self):
        self.set_start_date(2023, 11, 1)
        self.set_end_date(2024, 3, 1)
        self.set_cash(100_000)

        self._tsla = self.add_equity("TSLA")
        self._dataset_symbol = self.add_data(
            TiingoNewsSentiment, "TiingoNewsSentiment", Resolution.HOUR
        ).symbol
        self._roc = self.roc(self._dataset_symbol, 2)

        self.set_benchmark(self._tsla.symbol)

    def on_data(self, data):
        # Get the current sentiment.
        if self._dataset_symbol not in data:
            return
        sentiment = data[self._dataset_symbol].value
        
        # If the market isn't open right now, do nothing.
        if not self.is_market_open(self._tsla.symbol):
            return

        self.plot(
            "Sentiment", "Change in OpenAI Sentiment", self._roc.current.value
        )

        # If sentiment is flat/increasing and not already long, long.
        # The condition to buy here doesn't include `sentiment >= 0`
        # because excluding it allows the algorithm to buy when 
        # sentiment is down but relatively OK/good (since sentiment
        # is flat/increasing). If you wait for sentiment to be 
        # positive, it'll to late to benefit from the reversion
        # in price upwards following a crash from negative news. 
        if self._roc.current.value >= 0 and not self._tsla.holdings.is_long:
            self.set_holdings(self._tsla.symbol, 1)
        # If sentiment is negative and sentiment is decreasing and not 
        # already short, short.
        elif (sentiment < 0 and 
            self._roc.current.value < 0 and 
            not self._tsla.holdings.is_short):
            self.set_holdings(self._tsla.symbol, -1)


class TiingoNewsSentiment(PythonData):

    def get_source(self, config, date, is_live):
        return SubscriptionDataSource(
            f"tiingo-{date.strftime('%Y-%m-%d')}.csv", 
            SubscriptionTransportMedium.OBJECT_STORE, 
            FileFormat.CSV
        )

    def reader(self, config, line, date, is_live):
        # Skip the header line.
        if line[0] == ",": 
            return None
        
        # Parse the CSV line into a list.
        data = line.split(',')

        # Construct the new sentiment datapoint.
        t = TiingoNewsSentiment()
        t.symbol = config.symbol
        t.time = date.replace(hour=int(data[0]), minute=0, second=0)
        t.end_time = t.time + timedelta(hours=1)
        t.value = float(data[1])
        t["sentiment"] = t.value
        t["volume"] = float(data[2])

        return t