Adaptrade Software Newsletter Article

Is ChatGPT a Viable Trading Strategy Editor?

Following the release of ChatGPT in November of last year by the artificial intelligence (AI) company OpenAI, individuals and researchers alike have been exploring the capabilities of these so-called large language models (LLMs) in a variety of domains. In addition to being able to write essays, poems, bedtime stories for children, and other text, ChatGPT can generate programming code in a variety of languages. This made me wonder if ChatGPT could respond to prompts about systematic trading and write trading strategy code in common scripting languages, such as EasyLanguage. It turns out the answer is YES. In this article, I'll explore some ways to exploit this new technology to write trading strategy code. I'll provide several examples that demonstrate both the benefits and the limitations of LLMs for systematic trading.

What is ChatGPT and How Does it Work?

ChatGPT is a language-processing neural network based on the transformer architecture (GPT is short for "generative pre-trained transformer"). The transformer architecture, originally developed at Google,1 utilizes so-called self-attention to determine the relative importance of the different words — technically, tokens — in the sequence of words comprising the prompt. Unlike recurrent neural networks, which process the input tokens sequentially, transformers process all tokens simultaneously, which allows for longer input sequences and faster training times.2

LLMs are trained by feeding in large amounts of text and adjusting the weights of the neural network so that it can predict the next word in the input sequence. When you type in a prompt, ChatGPT outputs the next word most likely to follow the prompt. It then adds that word to the prompt and outputs the word most likely to follow those words. The process is repeated as each new word is added to the sequence, resulting in sentences, paragraphs, code examples, and so on.3, 4

Neural networks based on transformers typically contain a very large number of parameters (i.e., the network weights), which allows them to learn from vast amounts of data. GPT-3, a predecessor to ChatGPT, consists of 175 billion parameters (the number of parameters in ChatGPT has not been disclosed by OpenAI).5 Like GPT-3, ChatGPT was reportedly trained on several large databases, including Wikipedia and Common Crawl, the latter of which consists of the text from more than 20 million web sites (including, incidentally, Adaptrade.com) and more than three billion web pages.6

The large amount of text on which the model was trained is why it can respond cogently to a wide variety of prompts. And because the training set also contained discussions of systematic trading and examples of trading strategy code, it's possible to prompt ChatGPT to output strategy code.

Prompting Approaches

To generate trading strategy code from a LLM, I considered three approaches. In the first, I simply asked for a trading strategy of a certain type, such as a trend-following strategy. In the second approach, I copied the code for an existing strategy as part of the prompt and asked ChatGPT to modify the code is a specific way, such as adding an additional entry filter to the code. In the third approach, I used the LLM to implement specific ideas, such as asking for a moving average crossover entry with a look-back length of 15 and a protective stop based on a multiple of the average true range. I restricted my requested code output to the EasyLanguage scripting language because of the relative simplicity of the language and the likelihood that the LLM would contain more training examples in EasyLanguage than in most other trading platform languages.

Above, I explained that LLMs generate text word by word, where each word is chosen as the most likely word to follow the sequence of words already generated. More precisely, the models use a statistical approach in which each token in the sequence is randomly sampled from a distribution. Consequently, repeating the same prompt multiple times will generally result in different responses. This suggests that one prompting approach is simply to repeat the same prompt several times and keep the best answer.

For all the examples below, I used the free version of ChatGPT, available at OpenAI.com. I compiled the generated EasyLanguage code using the MultiCharts64 PowerLanguage Editor. For (relative) brevity, I only show the code generated by ChatGPT in its responses and leave out the accompanying descriptions of the code, many of which were quite long and detailed.

A Trend Following Strategy

Prompt: Write the EasyLanguage code for a trend-following trading strategy.

Response:
                            Inputs:
                                Length(20),
                                Trend(0.01),
                                StopLoss(1000),
                                Target(2000);

                            Vars:
                                TrendUp(false),
                                TrendDown(false),
                                EntryPrice(0),
                                StopLossPrice(0),
                                TargetPrice(0);

                            TrendUp = Close > Average(Close, Length) * (1 + Trend);
                            TrendDown = Close < Average(Close, Length) * (1 - Trend);

                            If MarketPosition = 0 Then Begin
                                If TrendUp Then Begin
                                    Buy Next Bar At Market;
                                    EntryPrice = Close;
                                    StopLossPrice = EntryPrice - StopLoss * MinMove;
                                    TargetPrice = EntryPrice + Target * MinMove;
                                    Sell Next Bar At StopLossPrice Stop;
                                    Sell Next Bar At TargetPrice Limit;
                                End;
                                If TrendDown Then Begin
                                    SellShort Next Bar At Market;
                                    EntryPrice = Close;
                                    StopLossPrice = EntryPrice + StopLoss * MinMove;
                                    TargetPrice = EntryPrice - Target * MinMove;
                                    BuyToCover Next Bar At StopLossPrice Stop;
                                    BuyToCover Next Bar At TargetPrice Limit;
                                End;
                            End;

                            If MarketPosition > 0 Then Begin
                                If Close < StopLossPrice Then
                                    Sell("Stop Loss") Next Bar At Market;
                                If Close > TargetPrice Then
                                    Sell("Target") Next Bar At Market;
                            End;

                            If MarketPosition < 0 Then Begin
                                If Close > StopLossPrice Then
                                    BuyToCover("Stop Loss") Next Bar At Market;
                                If Close < TargetPrice Then
                                    BuyToCover("Target") Next Bar At Market;
                            End;
                            

Impressively, this compiles successfully — no errors. However, there are a few small problems. First, the code redefines the built-in variable EntryPrice as the close. While the MultiCharts editor did not flag this as an error, a different variable name should have been used, or the close should have been used directly in the statements for StopLossPrice and TargetPrice. Secondly, the code blocks at the end that place market exits if the market position is positive or negative repeat the order names ("Stop Loss" and "Target"), which triggers an error message when the strategy is back-tested in MultiCharts.

Aside from those minor errors, there are several interesting aspects to the strategy code. First, the TrendUp and TrendDown boolean variables use a price band defined by a moving average. The input variable Trend is used to scale the moving average up for long trades and down for short trades, and the trend is then defined as the close relative to these price bands, providing a simple alternative to Bollinger bands or Keltner channels. Also interesting is the use of the MinMove keyword, which represents the minimum price change (scaled) for the instrument to which the strategy is applied. Its use suggests that StopLoss and Target are dimensionless multiples of the price increment. Though I probably wouldn't use this myself, it may make the values of StopLoss and Target less dependent on the symbol. A more common approach would be to make the sizes of the stops and targets multiples of the average true range.

Also note that the logic takes into account possible exits on the bar of entry. The stop and limit exit orders in the first code block ("If MarketPosition = 0...") ensure that the stop and limit orders are placed on the same bar as the entry. The market exit orders in the last two code blocks handle exits after the bar of entry. However, it's a bit unusual to place stops and targets as market orders, as in the last two code blocks. A more typical approach would be to use the same type of exit orders as in the first code block. However, there's nothing technically wrong with the code as written.

It should be noted that the strategy code, provided the repeated order names are corrected, is profitable in a back-test on daily bars of the E-mini S&P 500 with moderate costs and for a wide variety of input values. However, like many trend-following strategies, it will show the best back-tested results with a small number of trades and with relatively large drawdowns.

At the bottom of the ChatGPT screen is a button labeled "Regenerate response". As explained above, this can be used to generate a different answer to the same prompt. When I clicked this button for the preceding prompt, I received the following new strategy code:

                            Inputs:
                                TrendLength(20),
                                StopLoss(1000);
    
                            Vars:
                                TrendUp(false),
                                TrendDn(false),
                                EntryPrice(0),
                                StopPrice(0);
    
                            TrendUp = Close > Average(Close, TrendLength);
                            TrendDn = Close < Average(Close, TrendLength);

                            If MarketPosition = 0 Then
                            Begin
                                If TrendUp Then
                                Begin
                                    Buy Next Bar at Market;
                                    EntryPrice = Close;
                                    StopPrice = EntryPrice - StopLoss;
                                    Sell Stop Next Bar StopPrice Limit;
                                End
                                Else If TrendDn Then
                                Begin
                                    SellShort Next Bar at Market;
                                    EntryPrice = Close;
                                    StopPrice = EntryPrice + StopLoss;
                                    Buy to Cover Stop Next Bar StopPrice Limit;
                                End;
                            End
                            Else If MarketPosition > 0 Then
                            Begin
                                If TrendDn Then Sell Next Bar at Market;
                            End
                            Else If MarketPosition < 0 Then
                            Begin
                                If TrendUp Then Buy to Cover Next Bar at Market;
                            End;
                            

Unlike the first code, this one does not compile. It uses incorrect names for the sell and buy-to-cover orders. It also confuses stop and limit orders by using the keyword "limit" for the stop orders. However, the main problem with this strategy is that it does not include an exit type for exiting at a profit. It includes a stop-loss exit and, additionally, exits when the trend reverses. So, for a long trade, it will exit when the trend turns down. More often than not, this means it waits for the trade to turn into a loss before exiting.

Modifying an Existing Strategy

Since the last strategy seemed to be incomplete, let's ask ChatGPT to improve it by adding a profit target.

Prompt: Here is the EasyLanguage code for a trading strategy. Can you modify this by adding a profit target?

Because the ChatGPT interface responds to a return/enter key as the end of the prompt, I had to add the code to the end of the prompt in a text editor to start it on the next line. I then pasted the prompt sentence with the code (including the order name corrections) into the ChatGPT message prompt window. Unlike the first two examples, which generated the code in separate "code" windows, the code for this response was just a series of unformatted lines. I manually indented the code blocks before copying the code, which is shown below.

Response:
                            Inputs:
                               TrendLength(20),
                               StopLoss(1000),
                               ProfitTarget(2000);

                            Vars:
                               TrendUp(false),
                               TrendDn(false),
                               EntryPrice(0),
                               StopPrice(0),
                               TargetPrice(0);

                            TrendUp = Close > Average(Close, TrendLength);
                            TrendDn = Close < Average(Close, TrendLength);

                            If MarketPosition = 0 Then
                            Begin
                              If TrendUp Then
                              Begin
                                 Buy Next Bar at Market;
                                 EntryPrice = Close;
                                 StopPrice = EntryPrice - StopLoss;
                                 TargetPrice = EntryPrice + ProfitTarget;
                                 Sell Next Bar StopPrice stop;
                                 Sell Next Bar TargetPrice limit;
                              End
                              Else If TrendDn Then
                              Begin
                                 SellShort Next Bar at Market;
                                 EntryPrice = Close;
                                 StopPrice = EntryPrice + StopLoss;
                                 TargetPrice = EntryPrice - ProfitTarget;
                                 BuytoCover Next Bar StopPrice stop;
                                 BuytoCover Next Bar TargetPrice limit;
                              End;
                            End
                            Else If MarketPosition > 0 Then
                            Begin
                               If TrendDn Then
                               Begin
                                 Sell Next Bar at Market;
                                 TargetPrice = 0;
                               End
                               Else If TargetPrice > 0 And Close >= TargetPrice Then
                               Begin
                                 Sell Next Bar at TargetPrice;
                                 TargetPrice = 0;
                               End;
                            End
                            Else If MarketPosition < 0 Then
                            Begin
                               If TrendUp Then
                               Begin
                                 BuytoCover Next Bar at Market;
                                 TargetPrice = 0;
                               End
                               Else If TargetPrice > 0 And Close <= TargetPrice Then
                               Begin
                                 BuytoCover Next Bar at TargetPrice;
                                 TargetPrice = 0;
                               End;
                            End;
                            

It's apparent that the new code is basically the same as the prior code with the addition of a profit target, as requested. However, the new target exit orders in the bottom part of the code fail to include the "limit" keyword — the same error present in the original code, above, even though I corrected that error before submitting the code for modification. Also, the same part of the code contains unnecessary and arguably incorrect code. The conditions "If TargetPrice > 0 And Close >= TargetPrice" for the long-side target exit and "If TargetPrice > 0 And Close <= TargetPrice" for the short-side target exit serve no purpose. The TargetPrice should always be a positive number, so the first condition is unnecessary. Similarly, there is no reason to compare the close to the target price if you plan to place a limit/target order unless the reason for doing so is avoid placing an order at a price that is too far away from the market, which these conditions do not address.

Also, the statements that reset the target price ("TargetPrice = 0;") are unnecessary because a new trade cannot be placed until the current one exits, and the target price is redefined at that time. If the aforementioned conditions for the target orders are removed, the statements to reset the target price should also be removed to avoid losing the correct value of the target price in circumstances where the limit order is not filled immediately and must be placed again.

A More Challenging Strategy Request

Trend-following strategies are very common, so it's not surprising that ChatGPT was able to fulfill such a request. For something more challenging, I chose a counter-trend strategy. Counter-trend trading strategies are more difficult to define, and good ones are generally more difficult to develop.

Prompt: Write the EasyLanguage code for a counter-trend trading strategy.

Response:
                            Inputs:
                                ShortLength(20),   // Short moving average length
                                LongLength(50),    // Long moving average length
                                OverSold(30),      // Oversold level
                                OverBought(70);    // Overbought level

                            Vars:
                                ShortMA(0),        // Short moving average value
                                LongMA(0),         // Long moving average value
                                RSIVal(0);         // Current RSI value

                            ShortMA = Average(Close, ShortLength);
                            LongMA = Average(Close, LongLength);
                            RSIVal = RSI(Close, 14);

                            If RSIVal < OverSold AND Close < ShortMA AND Close[1] > ShortMA[1] AND Close > Low[1] Then
                                Buy("CounterTrendBuy") Next Bar at Market;

                            If RSIVal > OverBought AND Close > LongMA AND Close[1] < LongMA[1] AND Close < High[1] Then
                                SellShort("CounterTrendSell") Next Bar at Market;
                            

This strategy compiles correctly without errors and correctly captures the essense of a counter-trend strategy in that it looks to buy when the market is over-sold and sells short when the market is over-bought, as determined by the RSI indicator. In addition, the entry logic includes two other conditions. For a long trade, it waits for the close to be below the moving average on the current bar and the prior close to be above the moving average on the prior bar; i.e., it waits for the close to cross below the moving average. At the same time, it requires the close to be above the prior low for a long entry. The conditions are reversed for a short trade.

Though not obvious, there is a problem with the entry logic. It became apparent when I applied it to daily bars of the E-mini S&P futures that the entry conditions are too restrictive. There were zero trades. There were no cases where the close crossed below the moving average when the market was alread over-sold (RSI below 30). When the market is over-sold, the close is already below the moving average. Even with the moving average lengths set to 2, there were only two trades.

It should be noted that this is a stop-and-reverse strategy: it alternates between long and short trades, where each trade reverses the direction of the prior trade, so that it goes from long to short and back again, repeatedly. If the crossover conditions are removed, there are levels of the RSI that show positive back-test results for the E-mini S&P — found through optimization in MultiCharts — though the results include large drawdowns. Such results are not atypical for stop-and-reverse strategies, which are always in the market. As demonstrated above, a possible fix would be to ask ChatGPT to modify the code to add stops and/or profit targets in an attempt to improve its performance.

Specifying the Logic

Rather than asking ChatGPT to deliver a trading strategy based on a general description, such as trend-following or counter-trend trading, a more common use might be to more directly specify what you want.

Prompt: Write the EasyLanguage code for a trading strategy that enters on a moving average crossover and exits at a loss using a protective stop based on a multiple of the average true range. Also, add a profit target exit based on a different multiple of the average true range.

Response:
                            { Moving Average Crossover with ATR Stop and Profit Target }

                            Inputs:
                                FastMA_Length(9),
                                SlowMA_Length(21),
                                ATR_Multiplier(2.0),
                                Stop_Multiplier(1.5),
                                Target_Multiplier(2.0);

                            Vars:
                                FastMA(0),
                                SlowMA(0),
                                ATR(0),
                                EntryPrice(0),
                                StopPrice(0),
                                TargetPrice(0);

                            FastMA = Average(Close, FastMA_Length);
                            SlowMA = Average(Close, SlowMA_Length);
                            ATR = AverageTrueRange(High, Low, Close, 14);

                            If CrossAbove(FastMA, SlowMA) Then
                            Begin
                                EntryPrice = Close;
                                StopPrice = EntryPrice - (ATR_Multiplier * ATR);
                                TargetPrice = EntryPrice + (Target_Multiplier * ATR);
                                Buy ("MA_Cross") Next Bar at EntryPrice Stop;
                            End;

                            If MarketPosition = 1 Then
                            Begin
                                Sell ("Stop Loss") Next Bar at StopPrice Stop;
                                Sell ("Profit Target") Next Bar at TargetPrice Limit;
                            End;
                            

While it got the essense of my request correct, there are a few errors. First, the input Stop_Multiplier is not used anywhere in the code. Second, there is no EasyLanguage function named CrossAbove; the correct code would be "FastMA crosses above SlowMA". Third, the AverageTrueRange function has only one input, the look-back length. Also, as it did in the first example, it redefined a built-in variable (EntryPrice), rather than using a different name — not illegal but bad practice. Lastly, the strategy enters long on a stop, but the stop price is simply the current close. To enter at the current bar's close, it makes more sense to simply place a market order. Placing a stop order could result in a rejected order if the market is too close to the stop price at the time the order is received.

Let's see what happens when I point out the errors and ask for corrected code.

Prompt: There are a few problems with this code. First, the input Stop_Multiplier is not used anywhere in the code. Second, there is no EasyLanguage function named CrossAbove; the correct code would be "FastMA crosses above SlowMA". Third, the AverageTrueRange function has only one input, the look-back length. Also, EntryPrice is a built-in variable, so a different name should be used. Lastly, the entry order should be a market order, rather than a stop order. Can you make these corrections?

Response:
                            { Moving Average Crossover with ATR Stop and Profit Target }

                            Inputs:
                                FastMA_Length(9),
                                SlowMA_Length(21),
                                ATR_Length(14),
                                ATR_Multiplier(2),
                                Stop_Multiplier(1.5),
                                Target_Multiplier(2);

                            Vars:
                                FastMA(0),
                                SlowMA(0),
                                ATR(0),
                                TradePrice(0),
                                StopPrice(0),
                                TargetPrice(0);

                            FastMA = Average(Close, FastMA_Length);
                            SlowMA = Average(Close, SlowMA_Length);
                            ATR = AverageTrueRange(Close, ATR_Length);

                            if FastMA crosses above SlowMA then
                            begin
                                TradePrice = Close;
                                StopPrice = TradePrice - (ATR_Multiplier * ATR);
                                TargetPrice = TradePrice + (Target_Multiplier * ATR);
                                Buy ("MA_Cross") next bar at TradePrice;
                            end;

                            if MarketPosition = 1 then
                            begin
                                Sell ("Stop Loss") next bar at StopPrice Stop;
                                Sell ("Profit Target") next bar at TargetPrice Limit;
                            end;
                            

Although improved, there are still problems. ChatGPT corrected the use of EntryPrice and the "crosses above" usage but failed to remove the Stop_Multiplier input, still added an extra input to AverageTrueRange, and got the market entry order wrong ("TradePrice" should be "market"). If I chose to keep going, I could be more specific in what corrections are needed and probably get the correct code eventually. However, it's probably easier to make the remaining corrections in the MultiCharts editor.

Alternatives to ChatGPT

Of course, ChatGPT is hardly the only chat-based large language model available to the public, so how do some of the other ones compare in their ability to generate EasyLanguage code? I tried Google's Bard, which is based on Google's LaMDA LLM. Despite trying various prompts for generating EasyLanguage code, Bard was unable to generate any code containing EasyLanguage, despite its assurance that it knew what EasyLanguage was. In fact, when asked, it correctly described EasyLanguage before displaying code examples in the C programming language using numerous undefined functions and variables.

Microsoft Bing Chat, which is based on OpenAI's GPT-4, is now available either via Bing search or a separate search bar on the Windows desktop. Not surprisingly, since it's based on GPT-4, its results are similar to those of ChatGPT, including its ability to generate EasyLanguage code.

There are number of other LLMs available as well, with more coming out all the time. In addition to ones from the large software companies, there are open source versions, such as Dolly from databricks and Bloom from Hugging Face. While I haven't tested these models, their open source nature offers more privacy and fewer restrictions than the commercial options.

The Verdict

So is ChatGPT a viable trading strategy editor? It knows EasyLanguage fairly well, although it makes mistakes. It can not only create some basic trading strategies (trend-following, counter-trend) but respond to at least some simple instructions for creating specified strategy code. However, in each example I considered, I would not have been able to arrive at error-free code without knowing EasyLanguage myself. It's pretty clear that ChatGPT cannot be used as a substitute for knowing how to code trading strategies.

Nonetheless, I can imagine some traders might find ChatGPT useful as a coding assistant. It could be used to generate at least partial (and partially correct) code quickly, possibly leading to a more efficient strategy development process. It will never replace a true editor, which will always be required to compile the code and check for errors, but as an adjunct to an editor like the MultiCharts PowerLanguage Editor, it may have value.

Another possible use of ChatGPT would be as an idea generator. If you keep the instructions general, as I did when asking for a trend-following strategy, it might be possible to come up with code you would not have thought of on your own. Using the "Regenerate response" button, as I did with the first example, could be used to quickly generate additional ideas.

One thing ChatGPT and other LLMs cannot do is respond to code requests based on performance results. ChatGPT has no way to back-test the code it generates, so it cannot generate a reasonable response to a request such as "Generate the EasyLanguage code for a trading strategy that makes $500 per day on a forex symbol." What happens if you try this prompt? ChatGPT apologizes and points out that it cannot guarantee profitability and that creating a profitable trading strategy is complicated, etc., etc. It then offers the code for a simple moving average crossover strategy.

By the way, although my focus in this article has been on EasyLanguage, I did try a few prompts for NinjaScript for NinjaTrader and AFL for AmiBroker. ChatGPT seems to know those languages as well. However, NinjaScript in particular can be quite complicated when adding in the code required to properly manage orders and perform other functions that are handled automatically in other languages, such as EasyLanguage. I would be cautious in using ChatGPT for a language like NinjaScript without a high level of expertise in the language.

Large language models, such as ChatGPT, are a fascinating development with numerous applications. As a coding assistance or idea generator for writing trading strategies, ChatGPT in particular or, equivalently, Bing Chat, could be well worth exploring as an addition to your manual process of strategy development.

References

  1. Vaswani, A., et al., Attention Is All You Need, arXiv:1706.03762, 2017.
  2. Transformer (machine learning model), Wikipedia.
  3. The Illustrated Transformer.
  4. Glassner, A., Deep Learning: A Visual Approach. No Starch Press, Inc., San Francisco, 2021, pp. 565-599.
  5. Brown, T. B., et al., Language Models are Few-Shot Learners, arXiv:2005.14165v4, 2020.
  6. Common Crawl.

Good luck with your trading.

Mike Bryant
Adaptrade Software

This article appeared in the May 2023 issue of the Adaptrade Software newsletter.

HYPOTHETICAL OR SIMULATED PERFORMANCE RESULTS HAVE CERTAIN INHERENT LIMITATIONS. UNLIKE AN ACTUAL PERFORMANCE RECORD, SIMULATED RESULTS DO NOT REPRESENT ACTUAL TRADING. ALSO, SINCE THE TRADES HAVE NOT ACTUALLY BEEN EXECUTED, THE RESULTS MAY HAVE UNDER- OR OVER-COMPENSATED FOR THE IMPACT, IF ANY, OF CERTAIN MARKET FACTORS, SUCH AS LACK OF LIQUIDITY. SIMULATED TRADING PROGRAMS IN GENERAL ARE ALSO SUBJECT TO THE FACT THAT THEY ARE DESIGNED WITH THE BENEFIT OF HINDSIGHT. NO REPRESENTATION IS BEING MADE THAT ANY ACCOUNT WILL OR IS LIKELY TO ACHIEVE PROFITS OR LOSSES SIMILAR TO THOSE SHOWN.