OK, now that I've got that disclaimer out of the way, we can continue. Seriously though, it's so easy to get connected and start trading that I am sure many people have gotten caught up in the excitement and whatever little knowledge they may have acquired from some infomercial-like seminar or book, that they went in head-first and ended up with a story to tell but no profits to show. Make sure you understand what you're getting into very well if you decide to experiment with trading (let alone automated trading), although you are probably better off just reading Malkiel's Random Walk Guide To Investing and following the 10 rules proposed there. Most of that is nicely summarized in Dilbert's investment advice:
- Make a will.
- Pay off your credit cards.
- Get term life insurance if you have a family to support.
- Fund your 401(k) to the maximum.
- Fund your IRA to the maximum.
- Buy a house if you want to live in a house and you can afford it.
- Put six months’ expenses in a money market fund.
- Take whatever money is left over and invest 70% in a stock index fund and 30% in a bond fund through any discount broker and never touch it until retirement.
- If any of this confuses you, or you have something special going on (retirement, college planning, tax issues) hire a fee-based financial planner, not one who charges a percentage of your portfolio.
Now, if really you wanna try your hand at automated trading systems, whatever your motivation may be, use a demo or paper trading account as much as possible and educate yourself on the markets you will be trading (an MBA should be a good baseline). If you're a student and have a professor interested, you could use IB's student trading lab. The strategy below can actually be run against Interactive Brokers' own demo software (which does not require opening an account, just download the software), and to do that, you can follow Max's and Domenic's tutorials.
The strategy below is a mean-reverting strategy where we imagine the prices to "bounce" between upper and lower price bands, based on one from Ernest Chan, and which connects to IB's Trader Workstation application through their ActiveX API. Max's tutorial should guide you through all that. Now, unlike Ernest's code, I don't use the matlab2IB library, so as long as you have Matlab on a Windows box you should be good to go. It is a technical analysis (TA) trading strategy which uses Bollinger bands, and although TA sounds a little too much like astrology for my taste, the mechanics of an automated trading system are well illustrated.
The code is contained in 2 files, 'trader.m' and 'driver.m', and 'driver.m' is the one you execute (sorry about the crappy syntax highlighting... Matlab is not a choice in syntaxhighlighter):
%driver.m
%clear workspace variables and command window
clear;
clc;
%state holds info that will be persisted througout the execution,
%and that needs to be accessed from elsewhere (trader)
global state;
state=containers.Map();
%exSeq denotes more or less the execution sequence
exSeq={'preInit','init','accountUpdates','accountUpdatesEnd',...
'marketData','histData','updateHistData','placeOrders','exit'};
currStep='preInit';
%leave ip blank for localhost
%socket port as defined in tws app
%ib api webinar says up to 8 concurrent clients are supported,
%which can be achieved by increasing clientId
conInfo=struct('ipAddress','','port',7496,'clientId',0);
%pacific standard time
timeZone='PST';
%create instance of tws activex control and pass event handler 'trader'
tws=actxcontrol('TWS.TwsCtrl.1',[0 0 0 0],figure('Visible','off'),...
'trader');
%all output should include as prefix this anonynous function
dispPrefix=@() [datestr(now,13),' driver: '];
%connect to tws app
disp([dispPrefix(),'connecting...']);
tws.connect(conInfo.ipAddress,conInfo.port,conInfo.clientId);
while true
switch currStep
case 'preInit'
%here we just get confirmation of the connection and
%some useful data such as the nextValidId
disp([dispPrefix(),'in preInit...']);
if isKey(state,'nextValidId')
%this is the condition that tells us we're good
%to move to the next step (see nextValidId event
%handled by trader)
currStep='init';
end
case 'init'
%here we add any kind of initialization or configuration
%parameters for this strategy
disp([dispPrefix(),'in init...']);
params=struct();
%symbol for the front contract
params.localSymbol='ESU9';
state('localSymbol')=params.localSymbol;
%IB symbol for this future
params.symbol='ES';
state('symbol')=params.symbol;
params.securityType='FUT';
params.exchange='GLOBEX';
params.currency='USD';
%desired order size
params.numberOfContracts=1;
%number of ticks used to compute the middle band,
%a N-period simple moving average (MA)
params.N=20;
%the upper band at KA times an N-period standard deviation
%above the middle band (MA+KA*sigma)
params.KA=.5;
%the lower band at KB times an N-period standard deviation
%below the middle band (MA-KB*sigma)
params.KB=.5;
%KA and KB are generally 2 according to wkipedia, but that all
%depends on the strategy... this is one value you can play with
%to see more action happen...
currStep='accountUpdates';
case 'accountUpdates'
%request account updates
disp([dispPrefix(),'in accountUpdates...']);
%the particular event that this will kick off is the
%'updatePortfolioEx' event, which will tell us if we
%currently have any positions in the security we'll be trading
tws.reqAccountUpdates(1,'');
currStep='accountUpdatesEnd';
case 'accountUpdatesEnd'
%wait until we get confirmation that those were all
%available account updates
disp([dispPrefix(),'in accountUpdatesEnd...']);
if isKey(state,'doneAccountUpdates')
%this batch of updates has completed, so let's ask to
%stop receiving account position updates;
%even though tws' activex api doc says to use '2' to stop,
%'0' is what seems to work (1=subscribe, 0=unsub?)
tws.reqAccountUpdates(0,'');
%set next step to be executed
currStep='marketData';
%if there was nothing for tws to nitify us of,
%then we need to initialize the value in state here
if ~isKey(state,'pos')
state('pos')=0;
end
disp([dispPrefix(),'starting position in security: ',...
num2str(state('pos'))]);
end
case 'marketData'
%here we are gonna ask tws to start sending us market data
disp([dispPrefix(),'in marketData...']);
%tws has these factory (create*) methods for all the
%COM objects we need to pass as arguments
contract=tws.createContract();
%string: this is the symbol of the underlying asset.
contract.symbol=params.symbol;
%string: this is the security type. Valid values are:
%STK, OPT, FUT, IND, FOP, CASH, BAG
contract.secType=params.securityType;
%string: the expiration date. Use the format YYYYMM if
%applicable
contract.expiry='';
%double: the strike price
contract.strike=0;
%string: specifies a Put or Call.
%Valid values are: P, PUT, C, CALL.
contract.right='';
%string: allows you to specify a future or option contract
%multiplier. This is only necessary when multiple
%possibilities exist.
contract.multiplier='';
%string: the order destination, such as Smart.
contract.exchange=params.exchange;
%string: specifies the currency. Ambiguities may require that
%this field be specified, for example, when SMART is the
%exchange and IBM is being requested (IBM can trade in
%GBP or USD). Given the existence of this kind of
%ambiguity, it is a good idea to always specify the currency.
contract.currency=params.currency;
%string: this is the local exchange symbol of the
%underlying asset.
contract.localSymbol=params.localSymbol;
%string: identifies the listing exchange for the
%contract (do not list SMART).
contract.primaryExch=params.exchange;
%integer: if set to true, contract details requests and
%historical data queries can be performed pertaining to
%expired contracts.
contract.includeExpired=0;
%object: dynamic memory structure used to store the leg
%definitions for this contract.
contract.comboLegs='';
%integer: the unique contract identifier.
contract.conId=0;
%request market data ('0' indicates we do not want a snapshot,
%but wanna continue to receive a stream of data)
tws.reqMktDataEx(contract.conId,contract,'',0);
currStep='histData';
case 'histData'
%request historical data
disp([dispPrefix(),'in histData...']);
if ~isKey(state,'primedHistData')
%we only need to request historical data once, after that,
%we'll just keep updating it with market data
%request historical data
tws.reqHistoricalDataEx(contract.conId,contract,...
[datestr(now,'yyyymmdd HH:MM:SS'),' ',timeZone],...
'1 D','1 min','TRADES',1,2);
%start tic from the time we request all initial histData
tic;
state('primedHistData')=true;
else
if isKey(state,'doneHistData')
%we've already requested historical data,
%so no need to do it again, we're now ready to
%process this data
currStep='updateHistData';
else
%do nothing, still waiting on all the data,
%which is getting handled by the 'historicalData'
%event
end
end
case 'updateHistData'
%update our historical data based on the market
%data that we subscribed to earlier
disp([dispPrefix(),'in updateHistData...']);
elapsed=toc;
%every X elapsed seconds, get last tick data and update
%our price history vector, keeping the vector's
%length constant, once there is an update,
%then we move forward to placeOrders
%the elapsed time thershold is also another value you can
%play with to see more action happen...
if (elapsed >= 5)
tic;
histDataVec=state('histDataVec');
histDataVec(1:end-1)=histDataVec(2:end);
%if by chance we haven't received any market data yet,
%this call below should fail
histDataVec(end)=state('last');
state('histDataVec')=histDataVec;
%calculate moving average
ma=mean(histDataVec(end-params.N+1:end));
%calculate moving standard deviation
mstd=std(histDataVec(end-params.N+1:end));
%calculate deviation of ask and bid prices from the
%moving average middle band. All we're doing here is
%solving for K on the 'ask' and 'bid' prices:
%ask=(MA+K*sigma)
askK=(state('ask')-ma)/mstd;
%bid=(MA+K*sigma)
bidK=(state('bid')-ma)/mstd;
%move onto next step
currStep='placeOrders';
end
case 'placeOrders'
%place orders if the signals we're watching tells us to do so
disp([dispPrefix(),'in placeOrders...']);
%here we decide whether to exit or go back to updateHistData,
%which could be based on time (trading during trading hours,
%or a predefined number of trades); we illustrate with a
%predefined number of trades
if ~isKey(state,'placeOrdersCount')
state('placeOrdersCount')=1;
currStep='updateHistData';
else
if state('placeOrdersCount') > 50
currStep='exit';
else
state('placeOrdersCount')=state('placeOrdersCount')+1;
currStep='updateHistData';
end
end
pos=state('pos');
orderId=state('nextValidId');
order=tws.createOrder();
order.orderId=orderId;
order.clientId=conInfo.clientId;
order.action=''; %need below!
order.totalQuantity=0; %need below!
order.orderType='MKT';
order.transmit=1;
order.ocaGroup='';
order.lmtPrice=0;
order.auxPrice=0;
order.timeInForce='DAY';
disp([dispPrefix(),...
'pos: ',num2str(pos),...
', askK: ',num2str(askK),...
', bidK: ',num2str(bidK),...
', ma: ',num2str(ma),...
', bid: ',num2str(state('bid')),...
', ask: ',num2str(state('ask'))]);
if (pos == 0 && askK < -params.KA)
%if we are below the lower band and have nothing, go long
order.action='BUY';
order.totalQuantity=params.numberOfContracts;
tws.placeOrderEx(orderId,contract,order);
disp([dispPrefix(),'placed entry only...']);
pos=params.numberOfContracts;
elseif (pos < 0 && askK < -params.KA)
%if we are below the lower band and are short, buy 2,
%one for establishing the long position and
%another one to cover the short
order.action='BUY';
order.totalQuantity=2*params.numberOfContracts;
tws.placeOrderEx(orderId,contract,order);
disp([dispPrefix(),'placed exit and entry...']);
pos=params.numberOfContracts;
elseif (pos == 0 && bidK > params.KB)
%if we are above the upper band and have nothing, go short
order.action='SELL';
order.totalQuantity=params.numberOfContracts;
tws.placeOrderEx(orderId,contract,order);
disp([dispPrefix(),'placed entry only...']);
pos=-params.numberOfContracts;
elseif (pos > 0 && bidK > params.KB)
%if we're above the upper band and are currently long,
%sell 2, one for establishing the short position and
%anothe rone to take profits on the previous long
order.action='SELL';
order.totalQuantity=2*params.numberOfContracts;
tws.placeOrderEx(orderId,contract,order);
disp([dispPrefix(),'placed exit and entry...']);
pos=-params.numberOfContracts;
end
state('pos')=pos;
state('nextValidId')=orderId+1;
case 'exit'
%get ready to leave
disp([dispPrefix(),'in exit...']);
%maybe should cancel marketdata request, but we'll let
%disconnecting take care of that for now
pos=state('pos');
orderId=state('nextValidId');
order=tws.createOrder();
order.orderId=orderId;
order.clientId=conInfo.clientId;
order.action=''; %need below!
order.totalQuantity=0; %need below!
order.orderType='MKT';
order.transmit=1;
order.ocaGroup='';
order.lmtPrice=0;
order.auxPrice=0;
order.timeInForce='DAY';
%should really be looking and waiting for order confirmations,
%but the pauses will do the job for now...
if (pos > 0)
%cover the longs
order.action='SELL';
order.totalQuantity=pos;
tws.placeOrderEx(orderId,contract,order);
disp([dispPrefix(),'placed final order...']);
pause(5);
elseif (pos < 0)
%cover the shorts
order.action='BUY';
order.totalQuantity=abs(pos);
tws.placeOrderEx(orderId,contract,order);
disp([dispPrefix(),'placed final order...']);
pause(5);
end
pos=0;
state('pos')=pos;
state('nextValidId')=orderId+1;
break;
otherwise
disp([dispPrefix(),'invalid currStep: ',state('currStep')]);
end
%the pause gives the activex component events a chance to be handled
pause(2);
end
%disconnect from the tws app
tws.disconnect();
disp([dispPrefix(),'disconnecting...']);
pause(2);
This is 'trader.m':
%trader.m
function trader(varargin)
%as per matlab's documentation on arguments passed to event handlers,
%'end-1' is the position the event payload will be at
arg=varargin{end-1};
%very little code should go in the switch statement,
%all logic should be dispatched to subfunctions on this file
switch arg.Type
case 'errMsg'
%EVERY message should be well understood
custom_disp(['in errMsg: ',arg.errorMsg]);
case 'updateAccountValue'
%no need to do anything here
case 'updateAccountTime'
%no need to do anything here
case 'updatePortfolioEx'
updatePortfolioEx(arg);
case 'accountDownloadEnd'
accountDownloadEnd();
case 'nextValidId'
nextValidId(arg);
case 'openOrderEnd'
custom_disp('in openOrderEnd');
case 'tickPrice'
tickPrice(arg);
case 'tickSize'
%no need to do anything here
case 'tickString'
%no need to do anything here
case 'tickGeneric'
%no need to do anything here
case 'historicalData'
historicalData(arg);
case 'updatePortfolio'
%this guy has been replaced by updatePortfolioEx, so
%no need to do anything here
otherwise
custom_disp(['unknown event: ',arg.Type]);
end
end
function custom_disp(textMessage)
disp([datestr(now,13),' trader: ',textMessage]);
end
function nextValidId(arg)
%we get this message soon after having established a connection
custom_disp('in nextValidId');
global state;
state('nextValidId')=arg.id;
end
function updatePortfolioEx(arg)
custom_disp('in updatePortfolioEx');
global state;
if strcmp(arg.contract.symbol,state('symbol')) &&...
strcmp(arg.contract.localSymbol,state('localSymbol'))
state('pos')=arg.position;
end
end
function accountDownloadEnd()
custom_disp('in accountDownloadEnd');
global state;
state('doneAccountUpdates')=true;
end
function tickPrice(arg)
%as we get updates for this guy, shove the info into our state
custom_disp('in tickPrice');
global state;
switch arg.tickType
case 1 %bid
state('bid')=arg.price;
case 2 %ask
state('ask')=arg.price;
case 4 %last
state('last')=arg.price;
end
end
function historicalData(arg)
%all we're doing here is gathering the historical data messages
%into a vector
%custom_disp('in historicalData');
global state;
if ~isKey(state,'histDataVec')
%create the vector and position indicator
%we shouldn't get more than a day's worth of data
state('histDataVec')=zeros(24*60,1,'double');
state('histDataVecN')=0;
end
%-1 is shown on the last message in the histData request
if (arg.close == -1)
%resize the data vector to the number of messages we actualy received
state('doneHistData')=true;
histDataVec=state('histDataVec');
N=state('histDataVecN');
state('histDataVec')=histDataVec(1:N,1);
else
%Matlab arrays have pass-by-value semantics, so this thing
%that we're doing here is very inneficient (the array is getting
%cloned everytime). Just a heads up...
%keep track of the pieces we're interested in
histDataVec=state('histDataVec');
N=state('histDataVecN')+1;
histDataVec(N,1)=arg.close;
state('histDataVec')=histDataVec;
state('histDataVecN')=N;
end
end
And the output window with all the tracing would look something like this:
14:14:14 driver: connecting...
14:14:15 driver: in preInit...
14:14:15 trader: in nextValidId
14:14:15 trader: in openOrderEnd
14:14:15 trader: in errMsg: Market data farm connection is OK:ibdemo
14:14:15 trader: in errMsg: HMDS data farm connection is OK:demohmds
14:14:17 driver: in preInit...
14:14:19 driver: in init...
14:14:21 driver: in accountUpdates...
14:14:22 trader: in accountDownloadEnd
14:14:23 driver: in accountUpdatesEnd...
14:14:23 driver: starting position in security: 0
14:14:25 driver: in marketData...
14:14:25 trader: in tickPrice
14:14:25 trader: in tickPrice
14:14:25 trader: in tickPrice
14:14:25 trader: in tickPrice
14:14:25 trader: in tickPrice
14:14:25 trader: in tickPrice
14:14:25 trader: in tickPrice
14:14:26 trader: in tickPrice
14:14:26 trader: in tickPrice
14:14:27 driver: in histData...
14:14:29 trader: in tickPrice
14:14:29 driver: in histData...
14:14:31 driver: in updateHistData...
14:14:32 trader: in tickPrice
14:14:33 driver: in updateHistData...
14:14:35 driver: in placeOrders...
14:14:35 driver: pos: 0, askK: -0.62025, bidK: -1.5063, ma: 966.1, bid: 965.25, ask: 965.75
14:14:35 driver: placed entry only...
14:14:36 trader: in tickPrice
14:14:37 trader: unknown event: openOrder1
14:14:37 trader: unknown event: openOrder2
14:14:37 trader: unknown event: openOrder3
14:14:37 trader: unknown event: openOrder4
14:14:37 trader: unknown event: openOrderEx
14:14:37 trader: unknown event: orderStatus
14:14:37 trader: in tickPrice
14:14:37 trader: in tickPrice
14:14:37 trader: unknown event: execDetails
14:14:37 trader: unknown event: execDetailsEx
14:14:37 trader: unknown event: openOrder1
14:14:37 trader: unknown event: openOrder2
14:14:37 trader: unknown event: openOrder3
14:14:37 trader: unknown event: openOrder4
14:14:37 trader: unknown event: openOrderEx
14:14:37 trader: unknown event: orderStatus
14:14:37 trader: unknown event: openOrder1
14:14:37 trader: unknown event: openOrder2
14:14:37 trader: unknown event: openOrder3
14:14:37 trader: unknown event: openOrder4
14:14:37 trader: unknown event: openOrderEx
14:14:37 trader: unknown event: orderStatus
14:14:37 driver: in updateHistData...
...
14:19:45 trader: in tickPrice
14:19:45 trader: in tickPrice
14:19:45 trader: unknown event: execDetails
14:19:45 trader: unknown event: execDetailsEx
14:19:45 trader: unknown event: openOrder1
14:19:45 trader: unknown event: openOrder2
14:19:45 trader: unknown event: openOrder3
14:19:45 trader: unknown event: openOrder4
14:19:45 trader: unknown event: openOrderEx
14:19:45 trader: unknown event: orderStatus
14:19:45 trader: unknown event: openOrder1
14:19:45 trader: unknown event: openOrder2
14:19:45 trader: unknown event: openOrder3
14:19:45 trader: unknown event: openOrder4
14:19:45 trader: unknown event: openOrderEx
14:19:45 trader: unknown event: orderStatus
14:19:45 trader: in tickPrice
14:19:46 trader: in tickPrice
14:19:47 trader: in tickPrice
14:19:48 trader: in tickPrice
14:19:49 trader: in tickPrice
14:19:49 driver: disconnecting...
The code is pretty self-explanatory, and is a nice baseline to get started on some simple strategies. Anything more complicated than that and I would recommend some sort of state pattern implementation.
I ran it against the demo account and despite the fact that my parameters were optimized to minimize my waiting time (very low params.KA and params.KB and an arbitrary number of cycles before I liquidated any current positions, not to mention the fact that only market orders were used), it only lost about $100 (you start with about $500,000 in the demo account and each transaction cost about $1,000). This shouldn't by any means sound encouraging, since those prices aren't even real, they're simulated.
Anyways, this should give you an idea of the whole thing. You may wanna look into what futures and e-minis are all about or change the strategy to trade some stock that you may be familiar with.
Another good set of resources are IB's webinars on their APIs and the sample applications they provide that hook up to their APIs.
Enjoy!
0 comments:
Post a Comment