The Design and Development (and Performance) of a Simple Trading AI

Growing up, I always thought “wouldn't it be cool if I could write a program to trade stocks automatically, and have it make money!”

This article explains how to do that.

It doesn't make a ton of money, but it does make money — about 7-12% per year on a boring old bank stock.

Note that the program doesn't actually make the trades, it simply records trades that it would have made. I don't quite have the nerve to hook it up “live” to my margin account!

I aim to explain the general implementation of the program, so that you can write your own. It would be cool if you could do better than mine; I'm sure it's possible, the algorithm I describe here isn't very sophisticated.

In order to get started, you'll need the following:

When I first started doing this (2006), getting reliable stock quotations was difficult. There were many paid services that would give you end-of-day quotations, but even though you paid for them they weren't all that reliable (for example, one service I used just didn't grasp the concept of splits).

Nowadays, you can get quotes from many places. I get mine from finance.yahoo.com as a CSV (Comma Separated Value) file. (There's a lot more detail in the PowerPoint presentation Getting, Managing, and Analyzing Stock Market Information with FreeBSD, that I presented at BSDCan in May 2007.)

The algorithm is a little harder. Yes, you could just “buy low, sell high,” but that's not so easy in practice. The algorithm I use relies on two things — a good, solid, boring dividend paying stock, and “options,” also known as “derivatives.” You can read all about options in my other article, Understanding Puts and Calls via a Tutorial using the Bank of Nova Scotia. That article describes the basis of the algorithm; but if you're a TL;DR kind of person, the long and short of it can be summarized as follows:

The “AI” part of it

I always thought that an “AI” would be something that was inherently ungraspable, except by the top N smartest people on the planet. Since I'm not one of the top N smartest people on the planet (for some value of N, anyway), then by definition I must be incapable of writing an AI.

Wrong.

An AI is, really, just an automated set of rules that respond to certain conditions. The “genius,” if you will, of this particular AI is that it follows the rules, doesn't get bored, doesn't think it can outsmart itself, and just churns along, making money month after month.

The current otrader (autotrader was already taken) AI is a little over 2,500 lines of C code, and really just follows the rules from the algorithm above.

Specifically, it starts with a line of credit (with $0 on it), no cash and no stocks. It keeps track of its current state (which is one of WAITING, NAKED_PUT, HOLDING, or COVERED_CALL), and market conditions. When market conditions are “right,” it makes a trade and moves to a different state. To keep things fair, it also tracks commissions on each trade, and uses the least-favourable data from the quotes database (by that I mean that if it's buying, it uses the ask price, if it's selling, it uses the bid price).

So, without further ado, here's the trading AI's state diagram (described below — the program starts in WAITING state):

Figure 1, Trading AI State Diagram
Figure 1, Trading AI State Diagram
State Meaning Action Financial Impact
WAITING The AI doesn't hold any stocks or derivatives. It's waiting for conditions to be “right.” If the stock is in “buy” territory, the AI will seek out favourable put options. As currently defined, a favourable put option is one that:
  • is 3-6 months out (that is, has an expiration date 3-6 months from today),
  • has a strike price that's favourable to the stock being no more than 5% out of “buy” territory, and
  • is “in the money.”
If these conditions are met, the AI transits to NAKED_PUT state (circle #1), otherwise the AI continues waiting.
CREDIT: If a transition occurs, the AI receives a premium
NAKED_PUT Here, the AI has written what's called a “naked put.” This means that the AI has sold short some put contracts and is now on the hook for being exercised (meaning someone will be able to force the AI to buy the stock at a certain price) or (ideally) the contracts expire worthless (the AI still pockets the premium it received in either case). One of three things will happen here:
  • the AI will decide to de-obligate itself (that is, re-buy the naked puts that it sold). If this is the case, the AI goes back to the WAITING state (circle #5).
  • the puts may expire worthless (all on their own — this is a standard characteristic of puts). If this is the case, the AI goes back to the WAITING state as well (circle #5).
  • the puts may be assigned (that is, the party on the other end of the put has “exercised” their right to sell us the stock at the agreed-upon strike price.). In this case, the AI goes into debt (we assume it has a line of credit) and purchases the stock, and the AI transits to the HOLDING state (circle #2).
DEBIT: If the AI rebuys the puts, it will need to shell out the cash (we're assuming it does that from its profit, not the line of credit); if the AI is forced to buy the underlying stock, it will need to shell out a lot more cash (and will do that from its line of credit).
HOLDING In this state, the AI has debt (it dipped into its line of credit to buy stock) and it is holding shares. To keep things simple, I made it a rule that the AI always deals in quantity 1000 of the stock. Of course, for expensive stocks this means the AI goes into more debt than for cheap stocks.
Furthermore, the AI is collecting dividends on the stock (circle #7).
In order to capitalize on the fact that the AI is holding stock, the AI is looking for conditions to be right for writing “covered calls.” As currently defined, a favourable call option is one that:
  • is a few months out (no more than 6 months),
  • is above the purchase price of the stock (so we never lose money that way), and
  • returns > 2X the monthly dividend (via the premium)
If these conditions are met, the AI transits to COVERED_CALL state by writing covered calls (circle #3). Note that the premium the AI receives for the covered calls is the AI's to keep regardless of if the calls expire or are exercised.
CREDIT: If the AI writes covered calls, it receives a premium. The AI will also receive dividends periodically.
COVERED_CALL In the “covered call” state, the AI is in debt, is holding stock, and is on the hook for selling the stock at a fixed price. Also, the AI is collecting dividend payments (circle #7). Just like in the NAKED_PUT state, the AI is continually looking for an exit. It may be the case that the covered calls are very inexpensive now, so the AI will re-buy them to de-obligate itself. If that happens, then the AI transits back to the HOLDING state (circle #6), and tries to write more covered calls. CREDIT: if the AI's call options are assigned, it sells the stock at the strike price and receives a credit. The AI will also receive dividends periodically.
DEBIT: if the AI rebuys the call options, it will need to pay for them.

This really looks much more complicated than it is.

The key here is that at every state, there are a finite number of decisions to be made. The simplest state, the WAITING state, involves just one decision: “is it time to write naked puts?”

The amount of time that the AI spends in each state varies with market conditions. The NAKED_PUT and COVERED_CALL states have upper limits on the amount of time the AI spends in them — in both of those states, derivatives with an expiration date are at stake, and eventually the expiration date will be reached. At that point, the derivatives will expire or be assigned. The AI may choose to repurchase them earlier than at expiration. Also, in the real world, they may be assigned earlier than expiration; the model does not take that into consideration. Practically speaking though, in my trading experience I have never had this happen, nor have I ever had cause to make this happen (i.e., force the writer of the call or put to be assigned).

Results

So, here are some actual results from the trading AI. This is a snapshot of the last 12 months of trading, ending 2015-08-13:

Date Transaction Amount Stock Margin Comm. State Profit # days APR AVG/month
2014-10-15 Sold $64 Jan/2015 naked puts (+$1.83) $1807.51 $65.80 $0.00 $22.49 NAKED_PUT $1807.51 0
2015-01-15 Puts assigned (at $64.00/share) $-64054.99 $61.34 $-64000.00 $77.48 HOLDING $1752.52 92 11.82% $571.47
2015-01-16 Sold $66 Jul/2015 covered calls (+$1.37)$1347.51 $61.62 $-64000.00 $99.97 COVERED_CALL $3100.03 93 21.24% $1000.01
2015-04-08 Dividend (payable 2015-04-28) $680.00 $63.69 $-64000.00 $99.97 COVERED_CALL $3780.03 175 12.78% $648.01
2015-06-25 Rebought Calls (-$0.00) $-22.49 $66.53 $-64000.00 $99.97 HOLDING $3757.54 253 8.25% $445.56
2015-06-26 Sold $66 Aug/2015 covered calls (+$1.36)$1337.51 $66.64 $-64000.00 $122.46 COVERED_CALL $5095.05 254 11.17% $601.78
2015-07-23 Rebought Calls (-$0.12) $-142.49 $62.63 $-64000.00 $122.46 HOLDING $4952.56 281 10.39% $528.74
2015-07-29 Sold $66 Jan/2016 covered calls (+$1.26)$1237.51 $63.36 $-64000.00 $144.95 COVERED_CALL $6190.07 287 12.59% $647.05
2015-08-13 Closing Balance $61.10 $-64000.00 $144.95 COVERED_CALL $6190.07 302 12.37% $614.91

Yes, the AI is currently $64k in the hole (via the line of credit), and the stock is only worth $61k. However, it's currently earning a little over 4% in dividends (BNS pays $0.68/share as of writing, which is more than the cost of the line of credit), and like with all things, “this too shall pass.” BNS has consistently raised dividends, which is one of the reasons I like the stock.

In spite of all that, even if the AI was forced to sell today, it would be up $3,190.07 — which if you do the math over the interest payments on the line of credit (since January 15th, so 7 months, at 2.85% on $64k is $1,064) is an almost 300% return :-)

Note that the APR figure shown in the table is computed on the full line of credit amount (i.e, the $64k).

Going Forward

It's interesting to analyze what the AI will do next. Right now (based on the last entry in the table above), it's holding stock and is obligated via covered calls. The AI received $1.26 per call (the 2015-07-29 transaction), but those calls are now worth only around $0.60. If the stock continues to drop (or stays the same), the calls will fall in price. At some point, the AI may buy them back, and de-obligate itself. If the price of the underlying stock exceeds $66 (the strike price of the calls), then the other party will exercise their calls and force the AI to sell the stock at $66. This is fine, because the AI bought the stock at $64 per share. All it's done is limit the upside potential.

Tuning

Does this trading AI work for every stock?

Simply put — no.

There are two considerations to that answer. First off, the algorithm has explicitly not been “tuned” to match any particular stock. I looked at my general requirements (time horizons for the derivatives, dividend targets) and let the program do its own thing.

Secondly, some stocks are simply too volatile (and/or don't pay dividends) to work with this algorithm.

The “trick” with the dividends is that they set a target price for the stock. Believing as I do that BNS should, all things being equal, give me about 4% dividend, I can tell if the stock is in buy or sell territory. At the current dividend of $0.68, the stock should be at $68.00 per share in order to yield a 4% dividend. ($0.68 is 1% of $68.00; dividends are quarterly, so there's 4 x 1% = 4% in dividends yearly.)

If BNS goes to $60.00, then the dividend has increased as a percentage of the stock price:

So, one of the tuning parameters is the “expected dividend rate,” and I have that set to 4%. There are other parameters, like the time horizon for the derivatives. Some things you will need to tweak to match your comfort levels.

Software Architecture

I've described the algorithm, but how do you translate that into actual software?

The way the otrader trading AI works is that every day, after the stock data has been harvested, it goes back one year and “backtests” the current model. (See my C++ Library for Stock Analysis article for some more technical ideas.)

So, if today is 2015 08 14, otrader will go back to 2014 08 14, and execute the algorithm over one year's worth of data. It then reports on the actions it would take today, based on starting the position one year ago.

Ok, that's not very helpful for someone trying to “follow along at home” and mirror the trades.

There are two mitigations for that: one is a “what would otrader do right now, if it was starting from scratch?,” and the second is “going back as far as I have data for, what's otrader up to today?”

So, there are really three views on the data — two backtests (one over 12 months, and one since inception), and a “current” mode. These are all implemented by the same software, we just run it with different starting dates (first datum date for the “since inception” model, current date minus one year for the “12 month” model, and today's date for the “current” model).

State Machine

Given a start date, we run the trading AI state machine through all available data, recording significant events along the way. There's an accounting module that keeps track (via a journal) of the money — every time we perform a transaction, the accounting module adds or subtracts cash, or takes out or pays back a loan, or calculates commission.

These journal entries are rendered by an HTML rendering module, which just means that the journal entries are put into a nice HTML5 table with the appropriate CSS style sheets.

The heart of the state machine, trade, is 250 lines of C that basically looks at the current state and calls helper functions. The below pseudo-code illustrates the important decisions:

trade ()
{
    // in current mode, we just look for the best
    // entry, which is done by way of a naked put.
    // Therefore, find the best naked put (if any).
    if (mode == CURRENT) {
        find_best_naked_put ();
        return;
    }

    if (we_have_shares) {
        if (we_have_covered_calls) {
            if (covered_calls_expired_today) {
                if (calls_are_in_the_money) {
                    sell_stock_at_strike ();
                }
            } else {
                find_best_call_rebuy ();
            }
        } else {
            find_best_call ();
        }
    } else {
        if (we_have_naked_puts) {
            if (naked_puts_expired_today) {
                if (puts_are_in_the_money) {
                    buy_stock_at_strike ();
                }
            } else {
                find_best_naked_put_rebuy ();
            }
        } else {
            find_best_naked_put ();
        }
    }
}

The key thing to keep in mind about the decisions is that they follow from this table (“X” == “Don't care”):

Shares? Calls? Puts? State Meaning
No No No WAITING Initial state, looking to enter by writing puts and going to NAKED_PUT state
No No Yes NAKED_PUT Have written puts, looking to rebuy them (go to WAITING), have them be assigned to us (go to HOLDING), or have them expire (go to WAITING)
No Yes X (invalid) The AI doesn't write naked calls
Yes No No HOLDING We own stock, now we're looking to write covered calls (go to COVERED_CALL state)
Yes X Yes (invalid) The AI doesn't over-commit and write naked puts while holding stock
Yes Yes No COVERED_CALL Have written calls, looking to rebuy them (go to HOLDING), have them be assigned to us (go to WAITING), or have them expire (go to HOLDING)

So as you can see, there are really only 4 cases that need to be handled. The exits from the NAKED_PUT and COVERED_CALL case are complicated, because there are three possible ways to exit each.

Technically, the two invalid states are opportunities for more aggressive investment strategies. Writing naked calls (i.e., calls with no underlying equity) means that if the stock price really starts to go up, so do your losses. Writing additional naked puts while already having naked puts outstanding means that you are on the hook for buying more shares, and may blow your line of credit.

Kids, don't try this at home.