Thursday, February 14, 2013

thinkscript included: aiSpy


after a long interlude i am back with an innovation in thinkscripting: sdi_aiSpy. to put it simply, sdi_aiSpy is a feed-forward neural-network strategy that simulates fixed-dollar, long-only trades on the etf, spy, using the daily aggregation period. here is a picture of the strategy at work:







the strategy mines the best practices for long-only trading on spy from 10 years of daily data. the circled fpl (floating profit and loss) does not include commissions.

the companion study, sdi_aiSpyMind, shows the output of the buy/sell "neurons" of the same nn as sdi_aiSpy. the threshold value of 0.8 is where the network considers the neuron to be "firing." in the rare cases where both buy and sell are both active then no action is taken. 

for comparison i have also displayed the sdi_passive study that shows the growth of a 10k investement via buy-and-hold and dollar-cost-averaging over the same period. this shows a profit of $1,440 vs $2,978 by aiSpy.

caveat: aiSpy is highly optimized for the specific environment that it was trained for: spy, daily aggregation period, long-only trades with prices in the range of $50-$200.

here is the thinkscripts for both aiSpy and aiSpyMind:
#############################
# sdi_aiSpy
#hint: feed-forward neural-network strategy that simulates fixed-dollar, long-only trades on the etf, spy, using the daily aggregation period. http://www.smallDogInvestor.com rev: 1.2.0
# author: allen everhart
# date: 2/14/2013
# revision:
#     1.0.1 7/27/2013 - round off long numbers for new 15 digit limit in TOS
#     1.2.0 11/17/2013 - account for commissions.
# Copyleft! This is free software. That means you are free
# to use or modify it for your own usage but not for resale.
# Help me get the word out about my blog by keeping this header
# in place.

def n00 = (1.0 / (1.0 + Exp(-(0.394412243353913*close[0] + -0.757893552541896*close[1] + 0.298268163430432*close[2] + 0.073011718813853*close[3] + 0.268481160972121*(-1.0))/1.0)));
def n01 = (1.0 / (1.0 + Exp(-(0.043156438870034*close[0] + 0.314385182984086*close[1] + 0.643174368836916*close[2] + 0.449210701821324*close[3] + -0.039991647581192*(-1.0))/1.0)));
def n02 = (1.0 / (1.0 + Exp(-(0.114407816382417*close[0] + -0.189924985908828*close[1] + 0.308215925671476*close[2] + 0.336453429416838*close[3] + 0.230327528474661*(-1.0))/1.0)));
def n03 = (1.0 / (1.0 + Exp(-(-0.475647872256089*close[0] + -0.112591560827932*close[1] + -0.613013913739063*close[2] + -0.164836967728427*close[3] + 0.080334976530424*(-1.0))/1.0)));
def n04 = (1.0 / (1.0 + Exp(-(0.338319944050806*close[0] + -0.187943246111633*close[1] + -0.045920637300546*close[2] + 0.065643428215118*close[3] + -0.136887709123339*(-1.0))/1.0)));
def n05 = (1.0 / (1.0 + Exp(-(-0.248025954494193*close[0] + -1.245654016701924*close[1] + 0.959282554357995*close[2] + 0.531781356224809*close[3] + 0.097232935684274*(-1.0))/1.0)));
def n06 = (1.0 / (1.0 + Exp(-(0.978509085019555*close[0] + 0.522508628185785*close[1] + -0.007746140763169*close[2] + 0.182100420784146*close[3] + 0.147782358875439*(-1.0))/1.0)));
def n10 = (1.0 / (1.0 + Exp(-(0.001353108521367*n00 + -0.207117354877679*n01 + 0.395157679885085*n02 + 0.318818663432868*n03 + 0.212632869510944*n04 + 0.735186053799497*n05 + 0.553872437254164*n06 + -0.02237150713544*(-1.0))/1.0)));
def n11 = (1.0 / (1.0 + Exp(-(0.662663591575132*n00 + -0.341341340003081*n01 + -0.16609680207996*n02 + 0.163148493179810*n03 + 0.136936511969303*n04 + -0.709184442200140*n05 + 0.698809451898064*n06 + -0.892538600208493*(-1.0))/1.0)));
def buysig = n10 >=0.8&& n11 < 0.8 ;
def sellsig = n10 < 0.8&& n11 >= 0.8 ;
input dollarsPerTrade = 10000;
#hint dollarsPerTrade: trades vary in share size in order to keep the invested dollars constant to create a fair comparison to passive strategies. http://www.smallDogInvestor.com rev: 1.2.0
input commission = 5;
#hint commission: dollar value of commission per trade i.e. this number is multiplied by 2 (to account for a round trip) and divided by the number of shares traded and added to the purchase price in order to slip the entry price to account for the effect of commissions on the p&l.
def shareSize = round(dollarsPerTrade/close,0) ;
def commissionPerShare = 2*commission/shareSize;
addOrder( OrderType.BUY_AUTO, buysig, tradeSize = shareSize, name="aiSpy", price = open[-1]+commissionPerShare );
addOrder( OrderType.SELL_TO_CLOSE, sellsig,  name="aiSpy");

plot buy =  buysig && !buysig[1] ;
buy.setPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_UP);
plot sell = sellsig && !sellsig[1] ;
sell.setPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_DOWN);

 


#############################
# sdi_aiSpyMind
#hint: Plots the buy/sell neuron output of the aiSpy feed-forward neural network simulating long-only, fixed-dollar trades on SPY using the daily aggregation period. http://www.smallDogInvestor.com rev:1.0.1
# author: allen everhart
# date: 2/14/2013
# revision:
#     1.0.1 7/27/2013 - round off long numbers for new 15 digit limit in TOS
# Copyleft! This is free software. That means you are free
# to use or modify it for your own usage but not for resale.
# Help me get the word out about my blog by keeping this header
# in place.

declare lower ;
def n00 = (1.0 / (1.0 + Exp(-(0.394412243353913*close[0] + -0.757893552541896*close[1] + 0.298268163430432*close[2] + 0.073011718813853*close[3] + 0.268481160972121*(-1.0))/1.0)));
def n01 = (1.0 / (1.0 + Exp(-(0.043156438870034*close[0] + 0.314385182984086*close[1] + 0.643174368836916*close[2] + 0.449210701821324*close[3] + -0.039991647581192*(-1.0))/1.0)));
def n02 = (1.0 / (1.0 + Exp(-(0.114407816382417*close[0] + -0.189924985908828*close[1] + 0.308215925671476*close[2] + 0.336453429416838*close[3] + 0.230327528474661*(-1.0))/1.0)));
def n03 = (1.0 / (1.0 + Exp(-(-0.475647872256089*close[0] + -0.112591560827932*close[1] + -0.613013913739063*close[2] + -0.164836967728427*close[3] + 0.080334976530424*(-1.0))/1.0)));
def n04 = (1.0 / (1.0 + Exp(-(0.338319944050806*close[0] + -0.187943246111633*close[1] + -0.045920637300546*close[2] + 0.065643428215118*close[3] + -0.136887709123339*(-1.0))/1.0)));
def n05 = (1.0 / (1.0 + Exp(-(-0.248025954494193*close[0] + -1.245654016701924*close[1] + 0.959282554357995*close[2] + 0.531781356224809*close[3] + 0.097232935684274*(-1.0))/1.0)));
def n06 = (1.0 / (1.0 + Exp(-(0.978509085019555*close[0] + 0.522508628185785*close[1] + -0.007746140763169*close[2] + 0.182100420784146*close[3] + 0.147782358875439*(-1.0))/1.0)));
def n10 = (1.0 / (1.0 + Exp(-(0.001353108521367*n00 + -0.207117354877679*n01 + 0.395157679885085*n02 + 0.318818663432868*n03 + 0.212632869510944*n04 + 0.735186053799497*n05 + 0.553872437254164*n06 + -0.02237150713544*(-1.0))/1.0)));
def n11 = (1.0 / (1.0 + Exp(-(0.662663591575132*n00 + -0.341341340003081*n01 + -0.16609680207996*n02 + 0.163148493179810*n03 + 0.136936511969303*n04 + -0.709184442200140*n05 + 0.698809451898064*n06 + -0.892538600208493*(-1.0))/1.0)));

plot buy = n10 ;
buy.setDefaultColor(Color.Dark_GREEN);
plot sell = n11 ;
sell.setDefaultColor(Color.RED);
plot threshold = 0.8 ;
threshold.setDefaultColor( Color.DARK_ORANGE);
threshold.setLineWeight(2);
threshold.setPaintingStrategy(PaintingStrategy.DASHES);

#########################


12 comments:

  1. Great work Allen, thanks for sharing! Had a couple of questions, how are you determining the weights of the weights of the output neurons? Assuming you're probably using an external program to solve for the weights, would it be at all possible to do this within the Thinkscript environment? Also, how did you determine the structure of the NN, (how many hidden layers, inputs/outputs, timeframe, etc)?

    ReplyDelete
  2. i used a genetic algorithm to determine the weightings of the neurons. i started with the java version of ai-junkie.com 's ga from the smartsweepers demonstration program. then i created a java program to simulate trading on spy data and used the ga to evolve the most profitable trading neural net. at the end of the evolution era the program spits out the thinkScript for the most profitable neural net.

    the type of nn that is produced is called a feed-forward net. the input layer feeds results to an intermediate layer(s) and thence to an output layer and i chose that type of nn because i saw that thinkscript could handle this type of interaction. this is in contrast to a feed-back network which is difficult implement in thinkscript because of the forward reference restrictions. i wouldn't even want to think about implementing the ga in thinkscript.

    one arrives at the structure of nn's by trial and error. simpler is better. i found that the best results came from using 3 or 4 days of closing prices as input (and i tried many combinations of ohlc data and input lengths.) i have 1 hidden layer which i also arrived at by trial and error. the final product is the result of simulating the evolution of 10,000 traders through a 1000 generations. at some point i want to blog about trading lore that i have been able to prove or disprove from writing aiSpy.

    also, i do have a long-short version of aiSpy - one that shorts the market as well as trades long. it does even better than this long-only version. right now it is my private tool and i have been debating about whether or not to release it for free to the internet. i have had a somewhat underwhelming response to this release (its maybe a little too sci-fi for most traders?) but thanks for spending a little time here to dig-in and ask some really interesting questions.

    best.
    -allen

    ReplyDelete
  3. Thanks for the reply Allen, I think the underwhelming response is more due to the fact that the universe of people that use thinkscript is small to begin with, and when you pare that small universe down to the people that understand, appreciate, and develop these kinds of models/strategies, that becomes a tiny number. Your work is definitely appreciated, and I look forward to seeing additional releases. I've been working on a script to model volatility using garch[1,1] that I've posted below. It's still in development right now, as I haven't found a way to solve for the parameters within thinkscript itself (use excel solver to calculate the weights), but definitely making progress. The embeded LRVW script is courtesy of Jocilyn Boily on the yahoo thinkscript forums.

    ReplyDelete
  4. ############------Coded By Eric Rasmussen------#############
    # ericrasmussentx@gmail.com
    # Garch[1,1] =
    # gamma * long run variance +
    # alpha * yesterdays squared return +
    # beta * yesterdays variance
    ############################################################

    declare lower;

    # Weight Inputs (sum of weights = 1)
    input gamma = 0.05;
    input alpha = 0.05;
    input beta = 0.90;

    # Yesterday's Return
    input n1R = {default Lagged, Today};
    def n_1Return;
    switch (n1R) {
    case "Lagged":
    n_1Return = log(close[1] / close[2]);;
    case "Today":
    n_1Return = log(close / close[1]);;
    }

    # Yesterday's Squared Return
    def n_1ReturnSquared = power(n_1Return, 2);

    # Average Returns
    def avgReturnLength = 252;
    def avgReturn = Average(n_1Return, length = avgReturnLength);

    # Yesterday's Variance = (return - average return)^2
    def n_1variance = Power(n_1Return - avgReturn, 2);

    # Long run Variance
    def lr_variance = Average(n_1variance, length = 252);

    # GARCH[1,1] calc
    def garch = (gamma * lr_variance) + (alpha * n_1ReturnSquared) + (beta * n_1variance);

    # Garch Variance Predicition
    # (where t is number periods foreward)
    # variance on day n+t =
    # long run variance +
    # (alpha+beta) to the t power *
    # (today's variance - long run variance)

    input daysForward = 1;

    def garchForecast = (lr_variance) + Power((alpha + beta), daysForward) * (n_1variance - lr_variance);

    def garchStdDev = ((sqrt(garchForecast)) * sqrt(252)) * 100;
    def lr_StdDev = ((sqrt(lr_variance)) * sqrt(252)) * 100;

    # Monthly Data
    def newMonth = GetMonth() <> GetMonth()[1];
    def peak = garchStdDev > lr_StdDev;

    # Finding Monthly Peaks
    def totalPeaks = TotalSum(peak);
    def peakTotalMonth = if newMonth then totalPeaks else peakTotalMonth[1];
    def peakSumMonth = peakTotalMonth - peakTotalMonth[1];

    # Finding Current Month's Volatility Peaks
    def currentMonthPeaks = totalPeaks - peakTotalMonth;

    # Garch Volatility Peak Stats
    def peakDistance = garchStdDev - lr_StdDev;
    def highPeak = peak and garchStdDev > (lr_StdDev + (lr_StdDev * .30));

    # Average Peaks
    def peakSum = TotalSum(peakSumMonth);
    def monthSum = TotalSum(newMonth);
    def avgPeaks = Round(peakSum / monthSum, numberOfDigits = 0);

    # Persistance (higher persistance is greater tendency to stick to series)
    def persistance = (alpha + beta) * 100;

    ReplyDelete
  5. # Time Series Forecast
    def d = 0;
    def price = garchStdDev;
    def bar_plus = 7;

    input length = 78;

    script LrVw {

    input Src_y = close;
    input Src_x = 0.0;
    input Src_xc = 0.0;
    input n = 78;
    input wi = 1;

    def y = if IsNaN(Src_y[n/2]) then double.NaN else if !IsNaN(Src_y) then Src_y else y[1];
    def x = if !IsNaN(Src_x) then Src_x else x[1];
    def xc = if !IsNaN(Src_xc) then Src_xc else x;
    def wx = if IsNaN(Src_y) or IsNaN(Src_x) or IsNaN(wi) then 1 else wi;

    def Sw = wma(wx, n);
    def Sx = wma(x * wx, n);
    def Sy = wma(y * wx, n);
    def Sxy = wma(x * y * wx, n);
    def Sxx = wma(x * x * wx, n);

    def Sw2 = wma(Sw, n);
    def Sx2 = wma(Sx, n);
    def Sy2 = wma(Sy, n);
    def Sxy2 = wma(Sxy, n);
    def Sxx2 = wma(Sxx, n);

    def Sw3 = wma(Sw2, n);
    def Sx3 = wma(Sx2, n);
    def Sy3 = wma(Sy2, n);
    def Sxy3 = wma(Sxy2, n);
    def Sxx3 = wma(Sxx2, n);

    def a = (Sw*Sxy-Sx*Sy)/(Sw*Sxx-Sx*Sx);
    def b = ((Sy)-a*(Sx))/Sw;

    def a2 = (Sw2 * Sxy2 - Sx2 * Sy2)/(Sw2 *Sxx2 - Sx2 * Sx2);
    def b2 =((Sy2) - a2 * (Sx2)) / Sw2;

    def a3 = (Sw3 * Sxy3 - Sx3 * Sy3)/(Sw3 * Sxx3 - Sx3 * Sx3);
    def b3 = ((Sy3) - a3 * (Sx3)) / Sw3;

    plot yc3 = a3 * xc + b3;
    plot yc2 = a2 * xc + b2;
    plot yc = a * xc + b;
    plot yc_zlg= 2 * yc2 - yc3;
    plot yc_ffi = 2 * yc - yc_zlg;
    }

    input ts = { default "Inertia", "LRVW"};
    def tsf;
    Switch (ts) {
    case "Inertia":
    tsf = Inertia(price[-d], length) + LinearRegressionSlope(price[-d], length) * bar_plus;
    case "LRVW":
    tsf = LrVw(price[-d], barNumber(),barNumber(), length,1).yc;
    }

    ReplyDelete
  6. ##### Look and Feel #####

    # Line Plots
    input zeroLine = yes;

    plot gstd = garchStdDev;
    gstd.AssignValueColor(color = if TSF > lr_StdDev then createColor(150, 0 , 255) else createColor(0, 175, 255));
    gstd.SetPaintingStrategy(PaintingStrategy.LINE);
    gstd.SetLineWeight(1);

    plot lrstd = lr_StdDev;
    lrstd.AssignValueColor(CreateColor(red = 0, green = 60, blue = 255));
    lrstd.SetPaintingStrategy(PaintingStrategy.LINE);
    lrstd.SetLineWeight(1);

    plot pp = if zeroLine == yes and highPeak then 0 else Double.NaN;
    pp.SetPaintingStrategy(PaintingStrategy.POINTS);
    pp.SetLineWeight(3);
    pp.AssignValueColor(CreateColor(red = 255, green = 0, blue = 255));
    pp.HideTitle();

    plot pp2 = if zeroLine == yes and highPeak then 0 else Double.NaN;
    pp2.SetPaintingStrategy(PaintingStrategy.POINTS);
    pp2.SetLineWeight(5);
    pp2.AssignValueColor(CreateColor(red = 255, green = 180, blue = 250));
    pp2.HideTitle();

    plot zLine = if zeroLine == yes and !IsNaN(close) then 0 else Double.NaN;
    zLine.SetPaintingStrategy(PaintingStrategy.LINE);
    zLine.SetLineWeight(4);
    zLine.AssignValueColor(if lr_StdDev > lr_StdDev[1] then CreateColor(red = 220, green = 60, blue = 255) else if lr_StdDev <= lr_StdDev[1] then CreateColor(red = 20 , green = 180, blue = 255) else Color.BLACK);
    zLine.HideTitle();

    # Cloud Plots
    input cloudColors = yes;

    AddCloud(0, garchStdDev, color1 = Color.CYAN, color2 = Color.CYAN);
    AddCloud(0, lr_StdDev, color1 = CreateColor(red = 40, green = 40, blue = 255), color2 = CreateColor(red = 40, green = 40, blue = 255));

    AddCloud(0, if cloudColors == yes and TSF > lr_StdDev then garchStdDev else Double.NaN, color1 = CreateColor(red = 255, green = 0, blue = 0), color2 = CreateColor(red = 165, green = 0, blue = 255));

    # Labels
    input Labels = yes;

    AddLabel(Labels, Concat("Periods Forward: ", daysForward), color = CreateColor(red = 20, green = 145, blue = 255));

    AddLabel(Labels, Concat("Persistance to Average: ", Concat(persistance, "%")), color = CreateColor(red = 20, green = 145, blue = 255));

    AddLabel(Labels, Concat("Current Peaks: ", currentMonthPeaks), color = if currentMonthPeaks > avgPeaks then Color.RED else CreateColor(red = 20, green = 145, blue = 255));

    # Average Peaks Label
    #addLabel(Labels, concat("Average Peaks: ", avgPeaks));

    # Chart Bubbles
    input monthlyBubbles = no;

    AddChartBubble("time condition" = newMonth and monthlyBubbles == yes and peakSumMonth > avgPeaks, text = peakSumMonth, "price location" = lr_StdDev, color = if peakSumMonth >= 10 then color.RED else if peakSumMonth > avgPeaks then color.BLUE else Color.CYAN);


    #Std Dev Plot

    input stdDevLength = 20;

    def barNum = if IsNaN(close) then Double.NaN else BarNumber();
    def highestBar = HighestAll(barNum);

    def standardDev = if barNum == highestBar - 1 then garchStdDev + StDevAll(garchstdDev, length = stdDevLength) else double.NaN;

    # Std Dev of Vol Lines Plots
    input stdDevLine = yes;

    plot stdplot = if stdDevLine == yes then HighestAll(standardDev) else Double.NaN;
    stdplot.setPaintingStrategy(paintingStrategy.LINE);
    stdplot.AssignValueColor(color.RED);
    stdplot.SetLineWeight(1);

    ####


    - Eric Rasmussen

    ReplyDelete
  7. Hi Allen, I've been going through your blog for a while now, so much good content here it really is a nice thing you're doing for all us little guys!
    I've been playing with NN's for a bit now, and have built a back-prop NN that predicts future prices, and until now, I hadn't even thought to train it to trade.

    I'm now having a hard time thinking of a way to do this. In your comment above you said you had written a java program that simulates trades, I assume this is some strategy you built? Which possibly denotes buy and sell signals, and perhaps even outputs the amount won or lost, or a simple 1 or 0 for win or lose? And then fed that information to the NN for training, along with the price data?

    I could be way off with this, like I said, I'm having trouble wrapping my head around how your NN comes to it's buy and sell conclusions. Or more accurately, what was fed to it to train for it to evolve it's own trading conclusions :P

    If you're willing to expand on any of this it would be much appreciated!

    Also, I'd like you to know back prop is possible in TOS by using folds and enum / switches, I have not been able to crack that egg, but know someone who has. You obviously being a bit more skilled might be able to do something with this knowledge. I'm also told it brings the platform to a crawl hehe

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. I'm now having a hard time thinking of a way to do this. In your comment above you said you had written a java program that simulates trades, I assume this is some strategy you built?
      >>>>>>>>> I started with the demonstration program from ai junkie called smartsweeper. he published it as c++ program originally but I located a java version of it on the web somewhere.

      Which possibly denotes buy and sell signals, and perhaps even outputs the amount won or lost, or a simple 1 or 0 for win or lose?
      >>>>>>>>>>>>>> I modified minesweeper by extracting the genetic algorithm stuff and discarding all the minesweeper stuff. then I wrote a program that simulates trading long with the trade decisions derived from the output of a feed-forward network. then I expanded that to create a population of sim-traders. then I went through a phase of experimenting on what data produced the best results. after a substantial period of experimentation and groping in the dark I hit on a nn that performed better than buy'n hold over that last 10 years (20 years puts you in the era of the 90's tech boom, a bnh era that is hard to beat) then I had my java program emit the best nn in the form of a thinkscript strategy and also a thinkscript lower study called aispymind which plots the output of the buy and sell neurons. no backpropagation is used anywhere, the training is all this genetic algo stuff; simtraders begetting simtraders.

      And then fed that information to the NN for training, along with the price data?
      >>>>>>>>>> well neural networks (plural), if I recall correctly, the final run on aiSpy was 10,000 nn's trained over 1,000 generations. this training run ran overnight.

      I could be way off with this, like I said, I'm having trouble wrapping my head around how your NN comes to it's buy and sell conclusions. Or more accurately, what was fed to it to train for it to evolve it's own trading conclusions :P
      >>>>>>>>>>>the proper care and feeding of these puppies was a learning experience. I originally coded the input layer to look at 30 days of vohlc data I obtained from yahoo finance historical data. running a smaller population of 200 hundred simtraders and 200 generations, I experimented with different levels of input data. for example, I found that when I limited the number of days that the nn's were looking at to 3 or 4 the results improved markedly. also, very surprising, was that data such as volume, high and low were counterproductive. the best results were obtained by having the nn look at 4 days of market closes, with market opens a distant second. the tmi problem is due to the huge solution space that is being explored. every additional input increases the number of weights in the network and every weight in the network has like 2^64 possible values. there are some 50 weights in my simple 4x3x2 network config. and this creates a solution space of 2^64^50 or like 2^3200 (~10^160.) for some perspective on this number scale, it is estimated there are about 10^60 atoms in the universe so this 10^160 is much, much bigger, by 100 orders of magnitude. that the ga can arrive at any significant solution is testimony to the power of evolution.

      If you're willing to expand on any of this it would be much appreciated!

      Also, I'd like you to know back prop is possible in TOS by using folds and enum / switches, I have not been able to crack that egg, but know someone who has. You obviously being a bit more skilled might be able to do something with this knowledge. I'm also told it brings the platform to a crawl hehe

      >>>>>>>>>>no doubt that it would do that because it would have to run its backprop cycle on every bar of the chart. better to keep the chart work to a simple feed-forward network, no?

      Delete
    3. Thanks much for the detailed reply! I'll be going through your responses in greater detail and see what I can come up with

      Delete
  8. Interested as a follow up 2 years what the success rate of the AI_SPY study has been? Would you mind sharing the long/short thinkscript?

    ReplyDelete