New Forum

Visit the new forum at http://godelsmarket.com/bb

Monday, December 24, 2012

Simple Automated Trading System

I put together some Python code for an automated trading program for use with Interactive Brokers. It's very simple, but with a little ingenuity, could easily be expanded to include multiple contracts and various buying or selling triggers.

Sometimes it's nice to have a simple template to work from. Hope this helps someone get started with their very own automated execution program.

It uses the ibPy module and Python 2.7 (although it probably works on previous versions). I tried commenting the most important aspects.

What it does is send you a quote of the last price of the 'YM' 20130315 contract as long as that contract is between 13070 and 13100. Furthermore, (assuming you remove the '##'s) it will make a purchase if the contract is above 13083 and a sale if the contract is below 13082.

I recommend using a paper trading account while testing trade execution code. Also, the strategy within the code, if you were curious, has no quantitative backing. It's merely an example. The point is, you can add your own system in place of the one in the code.

I tried to comment on everything important in the code. If anything doesn't work or doesn't make sense, let me know! I'll try to reply quickly.

Here's the code:

from ib.ext.Contract import Contract
from ib.ext.Order import Order
from ib.opt import ibConnection, message
from time import sleep, strftime, localtime
from datetime import datetime

## Global variables
shares = 3
action = 'BUY' 
orderID = 30
last = 0
prev_last = 0
sym = 'YM'

## Contract Creation Function
def makeStkContract(sym):
    contract = Contract()
    contract.m_symbol = 'YM'
    contract.m_secType = 'FUT'
    contract.m_expiry = '20130315'
    contract.m_exchange = 'ECBOT'
    contract.m_currency = 'USD'
    return contract

## Order Creation Function
def makeStkOrder(shares,action):
    order = Order()
    order.m_minQty = shares
#    order.m_lmtPrice = limit_price
    order.m_orderType = 'MKT'
    order.m_totalQuantity = shares
    order.m_action = str(action).upper()
    return order

## Tick Handler
def my_tick_handler(msg):
    global last
    global prev_last
    #print msg
    if msg.field == 4:
        prev_last = last
        last = float(msg.price)
    print msg

##Connect
con = ibConnection()
con.register(my_tick_handler, message.TickSize, message.TickPrice)
con.connect()

## Make your contract
stkContract = makeStkContract(sym)
## Request tick data
con.reqMktData(orderID, stkContract, '', False)
sleep(1)
print last

## Prints last price as long as between 13070 and 13100
while (last > 13070 and last < 13100):
    print last

#### A possible execution plan. Remove '##' to initiate.
#### A paper trading account is recommended for testing and debugging trade execution code.
##    if (last > 13083):
##        action = 'BUY'
##        stkOrder = makeStkOrder(shares,action)
##        con.placeOrder(orderID,stkContract,stkOrder)
##    elif (last < 13082):
##        action = 'SELL'
##        stkOrder = makeStkOrder(shares,action)
##        con.placeOrder(orderID,stkContract,stkOrder)
    sleep(.5)

##Stop receiving tick values
con.cancelMktData(orderID)

##Disconnect from TWS
con.disconnect()

Wednesday, December 19, 2012

TV is Dead

As I have yet to figure out what to do about my quantitative writings, I am back to discussing the shortfalls of  articles I find largely through Abnormal Returns. 


While still pertinent to understand, acquiring such knowledge now will not put you ahead of the game. I'm guessing this type of article is just an easy throw-away mimic article, in the vein of the "OMG the newspaper is dying!" article. Of course, it's kind of hard to write about newspapers demise anymore. Most are dead and gone, killed by up to the second free reporting. When it comes to people searching for deals and coupons in the paper, amazon and groupon (and its many clones) provide much better services--for both customer and business.

But, the newspaper sensation was fun to write about. People liked to talk about it. Now, that it is nearly dead and everyone has heard about it, I guess media outlets are moving onto the next thing. The next thing to watch die. 

You should've seen it coming ten years ago. And for the last five, online video advertising has been exploding in revenue and profit. Plus, most shows are online, and anyone with an average computer know-how can download the latest episode of anything that is not online (or ask a friend to do it for them).

The internet has replaced television for many people, and it will continue to decline. Besides habit and comfort of use, there's really no reason to access content through the television anymore. There will be even less reason to do so in ten years.

Friday, December 14, 2012

AAPL to Apple? Or APPL to orange?

I never really liked these kind of comparisons: (http://www.marketanthropology.com/2012/12/the-universal-law-of-gravatation.html). He could be right, but I don't think he's showing any data that is statistically significant. It's an analogy, alright, but not something I'd necessarily want to trade directly off of.

Thursday, September 27, 2012

SPY Intraday Mondays

I have been doing a little research into the peculiarities of certain days of the week. For instance, Monday's tend to have very little economic news released intraday and very few earnings release or reports. You might expect 2008 to have some large intraday turbulence on Mondays, given government reports released over the weekends. But this is not the case. In face, the largest down day on a Monday since 1993 has been approx -4.7% and took place during 2009. The largest up day is 4.3% and also took place during 2009.

Here's a chart displaying the return of $100,000 placed into the SPY at its open on Monday morning and taken out of the SPY at its Monday close. Rather uneventful.


Wednesday, September 26, 2012

Minutes per Price Chart

To compliment yesterday's post, here's a chart displaying the minutes spent at each price level.

(Note: This is for AAPL between 7/17/2012 and 9/21/2012)

Tuesday, September 25, 2012

Using SQL to Generate Volume by Price

Sometimes you may want to know at which dollar level the most trades have taken place in your favorite stock. Here's an easy way to do this in MySQL using a simple SQL query:

SELECT ROUND(close,0), SUM(volume) FROM stocks.stock_prices_minute
WHERE SYMBOL = 'AAPL'
AND DATE(date) = '2012-09-21'
GROUP BY ROUND(close, 0)

Above, 'close' is the name of the column with 1-minute (or whatever you have) close prices, 'volume' is the name of the column with volume data, stocks is the database, and stock_prices_minute is the table.

Here's a graph of a similar query (I removed the date requirement in order to get all data within the period I had available).

Tuesday, September 18, 2012

Placing an Order Through Interactive Brokers API

This is one way to place an order through IB's API using ibPy and Python 2.7.

It places a 'SELL' order for 2 'YM' contracts with expiry '20120921'. Also, 'YM' is on the 'ECBOT' exchange, so, if you want to trade a different future check to see what its exchange is. You can't just change 'YM' to 'ES', for instance. You also have to change 'ECBOT' to 'GLOBEX'.

Also, remember to update your orderID each time you send an order. If you put this into a program you can have it auto-increment after each order. In this simple script, you'll have to do it by hand.

And, if you want to put in a limit order you can change 'MKT' to 'LMT' and make sure you put in a m_lmtPrice, i.e. a limit price. I have it commented out with '#' right now. But it's an easy adjustment.

from ib.ext.Contract import Contract
from ib.ext.Order import Order
from ib.opt import ibConnection, message

sym = 'YM'
shares = 2
action = 'SELL' 
orderID = 3

def makeStkContract(sym):
    contract = Contract()
    contract.m_symbol = 'YM'
    contract.m_secType = 'FUT'
    contract.m_expiry = '20120921'
    contract.m_exchange = 'ECBOT'
    contract.m_currency = 'USD'
    return contract

def makeStkOrder(shares,action):
    order = Order()
    order.m_minQty = shares
#    order.m_lmtPrice = limit_price
    order.m_orderType = 'MKT'
    order.m_totalQuantity = shares
    order.m_action = str(action).upper()
    return order

con = ibConnection()
#con.registerAll(watcher)
con.connect()

stkContract = makeStkContract(sym)
stkOrder = makeStkOrder(shares, action)
con.placeOrder(orderID, stkContract, stkOrder)

con.disconnect()

Tuesday, September 4, 2012

Volume vs Range

I've been reading some TraderFeed (this post for instance) recently and Dr. Steenbarger often mentions the relationship between Volatility and Volume. I did a simple study and present the data in the charts below. What I consider as Volatility is the Range for the day (the change between high and low). Volume is, just that, volume. In any case, the past few years has shown remarkable correlation.

(average volume has fluctuated over time and increased significantly from 1993)


Now, of course, it would be interesting to take the first 5,10, 15, 30mins, or hour of volume and see if that in any way correlates to the range expected for the rest of the day. I'm guessing it does. But I'll have to wait on that study until I have a bit more data.

Monday, September 3, 2012

Overnight Edges

I've followed Quantifiable Edges for years. One of my previous posts commented on his interesting, insightful and useful book on Fed Edges. It is always a pleasure to read his work, which is why I am so excited that he has a new site. I highly recommend watching the video below, and checking out his new site OvernightEdges.com.

Your brokerage account will thank you.

Saturday, September 1, 2012

System Trades with Huge Profit Potential

If you've been following this blog and my other trading blog, you know I've been working on a systematic trading strategy that I can passively implement. Here are the results of what appear to be a consistently exploitable inefficiency in a certain large (~5mil average daily volume) stock. The strategy is relatively simple and easy to execute. It is based off of taking advantage of supply inefficiencies.

There is a short strategy and a long strategy (both on the same stock). Below are the results (starting with $25,000):



I'm not going to post the method I used (which, is also good on a number of other stocks--typically movers--and the indices), but if you would like to purchase a guide explaining the statistical findings, I'd be happy to share. 

Of course, I am only presenting information on a situation and not on how to react to that situation. I cannot give financial advice and any action you take is your responsibility. 

Leave a comment if you'd like more information!

Thursday, August 30, 2012

Analysis of Hits

If you were to look at the number of hits certain pages of this site generated, you would find that my post entitled WSJ/FT Paywall Bypass has received, by far, the most attention. And it's not like you can't find substantial other sites online with information on how to bypass those paywalls. Quite frankly, I am not even sure I'm very proud of posting that information, and, in fact, thought about withdrawing the posting multiple times. It is, however, still up.

It does tell me something about the type of posts people like to read. I don't think many people like to read extremely quantitative posts. If I were to go for popularity, I should focus on other content. I have yet to decide how to go about utilizing this data, but it will influence my decision as to what to post.

Tuesday, August 28, 2012

AAPL by the Hour

I've been collecting intraday minute data for over a month. Here's a quick set of graphs showing the gain/loss for every hour throughout the day (the first half hour considered an hour by itself). The images are named in PST.
I doubt much can be gleaned from this alone. On average the 7am hour has been the best play for a long, the 9am hour (lunch? in NY) is the best hour to sit, and the last hour is the best to go short. The largest standard deviation in values is in the 7am hour, followed by the 6am half hour.

Sunday, August 26, 2012

Replay

Want to watch a replay of the day?

Using the tick data from slickcharts.com one could take each data line and print it on the screen at the time the trade was initiated.

Given the smallest interval is a second, in my version of the program below, I divide the second up by however many orders are placed, and then equally spread them throughout the second. Unfortunately, outside of regular trading hours certain seconds have no data. These gaps aren't accounted for in the program below. So going from 15:00:37 to 15:00:49, assuming no data in between, only takes 1 second rather than the 12 seconds it should take. But, given this type of program would generally be used to replay a trading day, I don't think these outside hours matter too much. I'll leave that for a future iteration.

The goal is to ultimately allow for replays at various speeds (1x, 2x, 5x the regular trading day), print the quotes on a graph and allow for sim trading through virtual order tracking. Lots more could be added, such as visual backtesting, but I'll save that for when I get some more of the basics done...

Here's the code:
import string
from time import sleep

f = open('gx120823/ES-2012-09.txt', 'r')

time_data = []
price_data = []
volume_data = []
oldtimeInfo = '15:00:00'

for line in f:
    line_split = string.split(line,',')
    timeInfo = '%s' % str(line_split[3])
    if oldtimeInfo == timeInfo:
        time_data = time_data + [str(line_split[3])]
        price_data = price_data + [float(line_split[4])]
        volume_data = volume_data + [int(line_split[5])]
    if oldtimeInfo != timeInfo:
        sleep_time = float(len(time_data))
        #print 'sleep_time is ', sleep_time
        j = 0
        for i in time_data:
            print i, '|', price_data[j], '|', volume_data[j]
            j = j + 1
            #print float(1/sleep_time)
            sleep(float(1/sleep_time))
        time_data = [str(line_split[3])]
        price_data = [float(line_split[4])]
        volume_data = [int(line_split[5])]
    oldtimeInfo = timeInfo

Thursday, August 23, 2012

SPY Gap Statistics

I read some of The Gap Guy yesterday while researching anything I could find online about gap trading. He got me to thinking about ways I could narrow down the gaps that I trade. Anyway, I posted some statistics on individual stock gaps a while back and I will post some stats on individual stocks again; but, the table below is on the SPY. 

The table shows the percentage of gaps that closed based on certain characteristics. These characteristics are whether the "prev day" (i.e. previous day) was positive or negative (i.e. up or down); the percentage of the gap (e.g. >.001<.005 = >0.1%<0.5%), and perhaps least familiar, the "type."

Something is "type 0" if the gap opens above the high of the previous day; "type 1" if it opens between the high and the next position ('prev open' when the last day was down, 'prev close' if the last day was up); "type 2" if it opens between the open and the close of the previous day; "type 3" if it opens below the next position ('prev close' when the last day was down, 'prev open' if the last day was up); and, "type 4" if the gap opens below the previous days' low.  (Check out this page at the gap guy for more visual info)

Note: I highlighted everything with a 55% occurrence or higher. I cannot guarantee the accuracy of the info! And, even if a certain zone has a high occurrence of gap closures, I cannot guarantee trading these gaps will result in profit. Please do your own due diligence and use this info only as a start to your own analysis. 


CLICK ON LINKS TO SEE TABLE.

Friday, August 17, 2012

Wheat Prices, Sunspots, and One-Dimensional Theories

I meant to post a link to this article on the increase in grain prices a couple weeks ago. It is still an important article especially from a critical thinking point of view. The author begins by noting the assertions by many in the media that the reason for higher grain prices is drought caused by global warming. He then goes on to say:
A better explanation for the drought, and the ensuing spike in grain prices, is that this is all part of the normal 11-year sunspot cycle.  But to find that relationship in the data is what the story is about. 
Given the data presented in the article, I would have to say the information is compelling. But, that is not the point of my post. I have little knowledge of what causes grain prices to fluctuate. In fact, that lack of knowledge is my point. So often people are willing to hurriedly, lazzily, and/or sloppily accept what sounds good. This, of course, can lead to an imperfect idea as to what will happen. What you think may happen, might happen for some time; but then, when the premise on which you built your theory continues to hold (e.g. global warming, drought, etc) but the underlying asset fails to continue its path, you will find that your single loose premise is not much help. Yes, global warming may be a cause, but perhaps it is merely an auxiliary cause of some greater impetus.

Abstract lessons learned in one area and apply to other investments and trades. Grow and profit.

Wednesday, August 15, 2012

Fed Days

I purchased Quantifiable Edge's Fed Guide a day before the last Fed meeting. If you have an interest in reading solid analysis, I suggest you buy it too. For $25, it's not too much to add an extra edge to your trading.

While it definitely doesn't sum up everything presented in QE's Guide, the NY Fed has a paper of its own with the following as an introduction:
This paper documents that stocks in the U.S. and several other major economies have experienced large abnormal returns right before U.S. monetary policy announcements since 1994. These returns are puzzling and difficult to explain with standard asset pricing models.
Despite only a handful of Fed Days throughout the year, it is well worth your time to take notice. The edge provided is enough to strengthen anyone's portfolio with relatively little exposure.

Tuesday, August 14, 2012

Gap Trading


If you were to fade each gap on AAPL that was .5% or bigger (up or down) over the last month or so, this is what you could expect for max gains (i.e. max return: if a negative gap, then you would go long so the max gain would be daily high minus open; and vice versa for gap ups).

PS: Been working on analyzing data like this across a number of stocks and with more info like drawdown before target is reached, contributing factors (e.g. earnings, news releases), etc. 

Tuesday, August 7, 2012

TICK Backtest

As I mentioned in the last post, I want to do a little TICK-NYSE backtesting in order to see if it can aid when making entries and exits. Perhaps it can also give some ideas as to the direction of the market.

The idea is that when the TICK index is "large" in either direction, traders are either dumping shares or grabbing shares. Here's a quick description of a TICK index. Because traders are eager to purchase or sell, it might hint as to the direction of the overall market, or at least, and opportune entry or exit.

Here's the test that I ran:
- If the TICK-NYSE's high > 500, go long SPY on the close of the minute bar.
- Sell after (1,2,3,4 or 5) minutes.
-If the TICK-NYSE's low < -500, go short SPY on the close of the minute bar.
-Cover after (1,2,3,4 or 5) minutes.

Unfortunately this data only goes back a week (I don't have access to more), but it is interesting nonetheless. What you find is gains when going long or short based on the above rules, for 1, 2, 3, 4 or 5 minutes. When the TICK index is high, holding for 3 minutes is opportune, giving a return of 1.3% over the last week. When the TICK index is low, holding for 5 minutes is opportune, giving a 1.3% gain over the last week.

Obviously, this could (and most likely will) change. But, it is nice to see that there are instances (and hopefully I can check more cases in the future) where the TICK index does hint towards the movement of the SPY.



Y-axis should be multiplied by 100 to find percent gain...

Monday, August 6, 2012

TICK-NYSE vs SPY


Starting to collect data on NYSE's TICK index. Hoping TICK research will aid in position entry and exit.

Sunday, August 5, 2012

VIX Term Structure

Here's a cool link showing the VIX term structure.

PS: Working on some object oriented programming. Trying to improve my programming knowledge...

Saturday, August 4, 2012

Non-Linearity

Here's an interesting video on the non-linearity of risk with Nassim Taleb.

Wednesday, August 1, 2012

Options Expiration Friday Range

I was curious as to how much the SPY fluctuated every third Friday of the month (options expiration Friday). Here's a graph of SPY range (high minus low) over the opening price. The percentage is shown on the y, and the x has the month.

For reference, the average range percentage over this period is 1.3%. So it doesn't look like options expiration has recently caused that much more volatility than on other days, at least for the SPY itself. (Sorry, I didn't get July's expiration range in there, albeit ~.5%)


Tuesday, July 31, 2012

Covered Calls

There's a huge push by a certain group of "advisers" for covered call writing. It is always presented, at least it feels this way to me, as a source of free money. These picks, for instance, seem rather poor choices to me.

I haven't gotten around to doing my own in-depth analysis of covered call writing, but I read this one earlier today.  I love this quote:
"As shown, they also partially hedge in downtrends. But the best use of covered calls is if you can see into the future and pick when your stock is going to go sideways."
They are great if you know the stock isn't going to go up too much. It's because writing covered calls limits your upside. It doesn't magically make money appear out of thin air. Plus, if you're not already long the underlier, you're taking on significant downside risk. The stock can still go to zero, and you would be taking that loss. While the stock can go to infinity as well, you're limited by whatever your upside call was sold at.

The strategy is not a magic money maker. It is, if anything, a portfolio volatility reducer.

Monday, July 30, 2012

Charts and Cliff-Hanger Analysis

Can it really be that easy? No. No, it can’t. Of  course, I can’t make any comments on anything but what I see (which appears rather like an advertisement for the premium service that they offer), I have to say what I see appears somewhat misleading.

Now, I know—it is merely a short post exhibiting an interesting result, a result that seems to hold well enough for profit.  What really gets me is the lack of a legitimate disclaimer. And the question at the end. I suppose Bespoke is looking to start conversation. Maybe. Or they’re just not interested in sharing their information. Which is mostly likely the case.

I should be grateful for the small insight they are providing. Of course, I would love to see the data and the process they used to form the chart (which, yes, I could probably do on my own). But, I also would like to see something commenting on the divergence at the beginning of the year (January to April) and what some of the other presidential years looked like. Again, I’m not paying for anything from them, so what can I expect? (Maybe I’ll post some of my own analysis…if I have time).

I guess what really gets me is that I don’t believe this is enough information to go by to make a good decision to go long at the current moment.

And, now that I look at the graph (and the axes labels) I am slightly confused. But with a quick comparison to the SPX's current return anyone could sort that out...

Oh well. An interesting chart in the least--as long as you do your due diligence. 

MATLAB VWAP, Part II: Backtest

After reading some Coding Horror (this specifically), I decided I might as well begin sharing more results. Albeit not terribly useful from a trading perspective--I'll explain shortly--, here are some results utilizing a small database of GS minute values (approximately just short of two weeks).  The backtest uses an extension of the VWAP code from yesterday following the simple rules of long when the 100 minute VWAP > 500 minute VWAP and short when 100 min VWAP < 500 min VWAP. A pretty simple, standard, elementary test.

Here are the results (with image):

Return: +13.36% (rounded)

PLEASE DO NOT TRADE ON THESE RESULTS. 

As I mentioned above, this should not be used from a trading perspective for several reasons:

1) It is run on far too few data points. Perhaps it just happened to work perfectly for this interval. I have no clue if this holds. (If you would like to donate historical data, please let me know!)

2) This is done during earnings, and, as you would see if you ran this for other stocks, could be hit or miss, especially with overnight holding (and, again, especially through earnings).

3) There are plenty of stocks this does not work for. Some with similar sized returns, although negative.

4) My data could be flawed, my routine should be suspect and you should always perform tests read on the internet for yourself!

5) I am sure there are 10 more reasons.
Now, here is the "backtest" code in function form. It's for MATLAB and requires that you have some data in  a MySQL database (very easy if you use my IB Historical Data Downloader/Importer):

function sum_log_change = vwap_backtest(symbol_string)

clear close_mat
clear volume_mat
clear vwap
clear vwap_2
clear buy_sell_matrix
clear trade
clear trade_prices
clear sum_log_change

vwap_length = 100;
vwap_length_2 = 500;

format long

symbol_string

%connect to the database CHANGE PASSWORD FROM ***** TO WHATEVER YOUR PASSWORD IS!!!
conn = database('stocks','root','****','com.mysql.jdbc.Driver','jdbc:mysql://localhost:3306/stocks');

%query the symbols in the database
str_query_close = ['SELECT close, volume FROM stock_prices_minute WHERE symbol = ''',symbol_string,  ''''];
close_cell = fetch(conn, str_query_close);

close_mat = cell2mat(close_cell(:,1));
volume_mat = cell2mat(close_cell(:,2));

close_times_volume = close_mat .* volume_mat;


for i = vwap_length:length(close_times_volume)
    volume_total = 0;
    volume_price_sum = 0;
    for j = 0:(vwap_length - 1)
        volume_total = volume_total + volume_mat(i-j);
        volume_price_sum = volume_price_sum + volume_mat(i-j)*close_mat(i-j);
    end
    vwap(i-(vwap_length - 1)) = volume_price_sum / volume_total;
end


for i = vwap_length_2:length(close_times_volume)
    volume_total = 0;
    volume_price_sum = 0;
    for j = 0:(vwap_length_2 - 1)
        volume_total = volume_total + volume_mat(i-j);
        volume_price_sum = volume_price_sum + volume_mat(i-j)*close_mat(i-j);
    end
    vwap_2(i-(vwap_length_2 - 1)) = volume_price_sum / volume_total;
end


%do comparing
for i = 1:(length(close_times_volume) - (vwap_length_2 - 1))
    if vwap_2(i) < vwap(i+(vwap_length_2 - vwap_length))
        buy_sell_matrix(i) = 1;
    else
        buy_sell_matrix(i) = -1;
    end
end

buy_sell_matrix;

%calculate trade prices
for i = 1:(length(close_times_volume) - (vwap_length_2 - 1))
    if i == 1
        trades(i) = close_mat(i + vwap_length_2);
    else
        if buy_sell_matrix(i) ~= buy_sell_matrix(i-1)
            trades(i) = close_mat(i + vwap_length_2);
        elseif i == (length(close_times_volume)-(vwap_length_2 - 1))
            trades(i) = close_mat(length(close_mat));
        else
            trades(i) = 0;
        end
    end
end

trades;

j = 1;
%matrix of prices trades took place at
for i = 1:length(trades)
    if trades(i) ~= 0
        trade_prices(j) = trades(i);
        j = j + 1;
    end
end


trade_prices;

log_change = 0;
sum_log_change = 0;
%now you can calculate lognormal return
for i = 1:(length(trade_prices) - 1)
    if buy_sell_matrix(1) == 1
        if mod(i,2) == 0
            log_change = -(log(trade_prices(i+1)) - log(trade_prices(i)));
        else
            log_change = log(trade_prices(i+1)) - log(trade_prices(i));
        end
        sum_log_change = sum_log_change + log_change;
    else
        if mod(i,2)==0
            log_change = log(trade_prices(i+1)) - log(trade_prices(i));
        else
            log_change = -(log(trade_prices(i+1)) - log(trade_prices(i)));
        end
        sum_log_change = sum_log_change + log_change;
    end
end

sum_log_change;
            
        
    

x = [1:length(close_mat)];
x2 = [(vwap_length):length(close_mat)];
x3 = [(vwap_length_2):length(close_mat)];
plot(x, close_mat)
hold on
plot(x2, vwap, '-r')
hold on
plot(x3, vwap_2, '-g')

Link to previous article: MATLAB VWAP (Part I)

Saturday, July 28, 2012

MATLAB VWAP

Here's a simple MATLAB script to extract data from a MySQL Database (here, specifically one that was created using my IB historical data extractor) and plot close data along with two VWAPs (volume weighted average prices) of your desired length. It's pretty simple but could be expanded upon to backtest VWAP strategies using the created VWAP matrices. (Also, I think I fixed the way source code is displayed on this site...should now be easier to copy and paste).

Update: Here's part II.

clear close_mat
clear volume_mat
clear vwap
clear vwap_2

vwap_length = 100;
vwap_length_2 = 500;

format long

%connect to the database
conn = database('stocks','root','***','com.mysql.jdbc.Driver','jdbc:mysql://localhost:3306/stocks');

%query the symbols in the database
str_query_close = 'SELECT close, volume FROM stock_prices_minute WHERE symbol = ''GOOG''';
close_cell = fetch(conn, str_query_close);

close_mat = cell2mat(close_cell(:,1))
volume_mat = cell2mat(close_cell(:,2));

close_times_volume = close_mat .* volume_mat;


for i = vwap_length:length(close_times_volume)
    volume_total = 0;
    volume_price_sum = 0;
    for j = 0:(vwap_length - 1)
        volume_total = volume_total + volume_mat(i-j);
        volume_price_sum = volume_price_sum + volume_mat(i-j)*close_mat(i-j);
    end
    vwap(i-(vwap_length - 1)) = volume_price_sum / volume_total;
end

for i = vwap_length_2:length(close_times_volume)
    volume_total = 0;
    volume_price_sum = 0;
    for j = 0:(vwap_length_2 - 1)
        volume_total = volume_total + volume_mat(i-j);
        volume_price_sum = volume_price_sum + volume_mat(i-j)*close_mat(i-j);
    end
    vwap_2(i-(vwap_length_2 - 1)) = volume_price_sum / volume_total;
end


x = [1:length(close_mat)];
x2 = [(vwap_length):length(close_mat)];
x3 = [(vwap_length_2):length(close_mat)];
plot(x, close_mat)
hold on
plot(x2, vwap, '-r')
hold on
plot(x3, vwap_2, '-g')

Thursday, July 26, 2012

WSJ/FT Paywall Bypass

Here's a guide on how to bypass the WSJ paywall. It also works with FT.

Basically, just copy the article name you want to read and paste it into Google. Then select the article from the search results. Now, you can read it as though you had a subscription!

Wednesday, July 25, 2012

Downloading Options Data From Yahoo!

I wanted to do some volatility surfaces using Yahoo! options data. Unfortunately they don't make it as easy to grab options data as it is to grab stock data.

Here's a very nice solution. (To prevent loss, source code follows; although all credit goes to link!)

function DataOut = Get_Yahoo_Options_Data(symbolid)
%Get_Yahoo_Options_Data get Option Chain Data from Yahoo
% Get Options Chain Data from Yahoo
% DataOut = Get_Yahoo_Options_Data(symbol)
% Inputs: Symbol name as a character String
% Output:  A structure with the following fields
%       data : A 1xN cell where N is the number of Expiries available
%       ExpDates : A 1xN cell array of Expiry Dates
%       Calls  : A 1xN cell array of Call Option data for each expiry
%       Puts  : A 1xN cell array of Put Option data
%       CPHeaders : Headers for the calls and puts option data
%       Headers: Headers for the data
%       FullOptionData : A combined cell array of DataOut.data
%       Last : Last Price
% Example:
%           DataOut = Get_Yahoo_Options_Data('LVS');
% (c)tradingwithmatlab.blogspot.com
DataOut = struct;
% Construct and read the URL from Yahoo Finance Website
urlText = urlread(['http://finance.yahoo.com/q/os?s=' symbolid]);
% Try getting the Table Data from URL Text 
TableData = getTableData();
% If Empty return
if(isempty(TableData))
    return
else
    DataOut.data{1} = TableData;
end
% Get the Expiry Date for later use
DataOut.ExpDates{1} = Get_Exp_Dates();
% Get Expiry Dates that are listed in the website to construct separate
% URLS for each month
NextExpiryURL = Get_Next_Expiry_URL();
if(isempty(NextExpiryURL))
   return
end

% Now read Option Tables of each Expiry month
for ik = 1:length(NextExpiryURL)
    urlText = urlread(NextExpiryURL{ik});
    DataOut.ExpDates{ik+1} = Get_Exp_Dates();
    DataOut.data{ik+1} = getTableData();
end
% Clean Up
% Convert the strings into numbers 
f = @(x)[x(:,1) num2cell(str2double(x(:,[2:8]))) x(:,9) num2cell(str2double(x(:,10:end)))];
DataOut.data = cellfun(f,DataOut.data,'uni',false);

goodDataIdx = (~cellfun('isempty',DataOut.data));
DataOut.data = DataOut.data(goodDataIdx );
DataOut.ExpDates = DataOut.ExpDates(goodDataIdx );
% Separate the data into Calls, Puts, Headers
DataOut.Calls = cellfun(@(x) x(:,[1 8 2:7]),DataOut.data,'uni',false);
DataOut.Puts = cellfun(@(x) x(:,[9 8 10:end]),DataOut.data,'uni',false);
DataOut.CPHeaders = {'Symbol','Strike','Last','Change','Bid','Ask','Volume','Open Int'};
DataOut.Headers = {'Symbol','Last','Change','Bid','Ask','Volume','Open Int','Strike',...
    'Symbol','Last','Change','Bid','Ask','Volume','Open Int'};
DataOut.FullOptionData = [DataOut.Headers ; cat(1,DataOut.data{:})];
% Get the Last Price
DataOut.Last = str2num(urlread(['http://download.finance.yahoo.com/d/quotes.csv?s=' symbolid '&f=l1&e=.csv']));

%% Get_Next_Expiry_URL
    function NextExpiry = Get_Next_Expiry_URL()
        % Get the start and end indices and look for a particular text
        Start = regexp(urlText,'View By Expiration:','end');
        end1 = regexp(urlText,'Return to Stacked View...','start');
        
        Data = urlText(Start:end1);
        Data=Data(2:end);
        % Trim the data
        Data=strtrim(Data);
        % Split the data into new lines
        newlines = regexp(Data, '[^\n]*', 'match');
        expr = '<(\w+).*?>.*?</\1>';
        if(isempty(newlines))
            NextExpiry = {};
            return
        end
        % Get the matches of particular expression
        [tok mat] = regexp(newlines{1}, expr, 'tokens', 'match');
        id1= regexp(mat{1},'</b>','start')-1;
        month{1} = mat{1}(4:id1);
        %Month and Next Expiries
        for j = 2:length(mat)-1
            id2 = regexp(mat{j},'">','end');
            id3 = regexp(mat{j},'</a','start');
            if(isempty(id3))
                return
            end
            month{j} = mat{j}(id2+1:id3-1);
            id4 = regexp(mat{j},'"','start');
            NextExpiry{j-1} = ['http://finance.yahoo.com' mat{j}(id4(1)+1:id4(2)-1)]; %#ok<*AGROW>
            NextExpiry{j-1} = regexprep(NextExpiry{j-1},'amp;','');
        end
        
    end
%% Get_Exp_Dates

    function ExpDates = Get_Exp_Dates()
        
        id1 = regexp(urlText,'Options Expiring','end');
        id2 = regexp((urlText(id1+1:id1+51)),'</b>','start');
        ExpDates = strtrim(urlText(id1+1:id1+1+id2-2));
        ExpDates=datestr(datenum(ExpDates,'dddd, mmmm dd,yyyy'));
    end

%% getTableData
    function out = getTableData()
        Main_Pattern = '.*?</table><table[^>]*>(.*?)</table';
        Tables = regexp(urlText, Main_Pattern, 'tokens');
        out = {};
        if(isempty(Tables))
            return
        end
        try
        for TableIdx = 1 : length(Tables)
            
            %Establish a row index
            rowind = 0;
            
            
            % Build cell aray of table data
            
                rows = regexpi(Tables{TableIdx}{:}, '<tr.*?>(.*?)</tr>', 'tokens');
                for rowsIdx = 1:numel(rows)
                    colind = 0;
                    if (isempty(regexprep(rows{rowsIdx}{1}, '<.*?>', '')))
                        continue
                    else
                        rowind = rowind + 1;
                    end
                    
                    headers = regexpi(rows{rowsIdx}{1}, '<th.*?>(.*?)</th>', 'tokens');
                    if ~isempty(headers)
                        for headersIdx = 1:numel(headers)
                            colind = colind + 1;
                            data = regexprep(headers{headersIdx}{1}, '<.*?>', '');
                            if (~strcmpi(data,'&nbsp;'))
                                out{rowind,colind} = strtrim(data);
                            end
                        end
                        continue
                    end
                    cols = regexpi(rows{rowsIdx}{1}, '<td.*?>(.*?)</td>', 'tokens');
                    for colsIdx = 1:numel(cols)
                        if(rowind==1)
                            if(isempty(cols{colsIdx}{1}))
                                continue
                            else
                                colind = colind + 1;
                            end
                        else
                            colind = colsIdx;
                        end
                        % The following code is required to get the sign
                        % of the change in Bid ask prices
                        data = regexprep(cols{colsIdx}{1}, '&nbsp;', ' ');
                        down=false;
                        % If Down is found then it is negative
                        if(~isempty(regexp(data,'"Down"', 'once')))
                            down=true;
                        end
                        data = regexprep(data, '<.*?>', '');
                        if(down)
                            data = ['-' strtrim(data)];
                        end
                        if (~isempty(data))
                            out{rowind,colind} = strtrim(data) ;
                        end
                    end % colsIdx
                end
                
                
        end
        out = out(3:end,:);
        catch %M  %#ok<CTCH> This depends on which version of matlab you are using
               %M.stack
        end
    end
end

Tuesday, July 24, 2012

Import IB Historical Data into MySQL

Here's the code to import the csv files you created using the IB Historical Data Downloader (assuming you put all the files into a folder called "csv", have the below python file one folder above, and are running your MySQL server at localhost, with user root and database "stocks". Make sure to change the password to whatever you're using.) Also, you need to have the MySQLdb module installed. Check out the last post to see an easy way to do that.
import MySQLdb
import os
db = MySQLdb.connect(host='localhost', user='root',passwd='*****', db='stocks')
cur = db.cursor()
path = 'csv/'
listing = os.listdir(path)
for infile in listing:
    cur.execute("load data local infile 'csv/" + infile + "' into table `stocks`.`stock_prices_minute` fields terminated by ',' lines terminated by '\n' (`symbol`,`date`,`open`,`high`,`low`,`close`,`volume`);")
    db.commit()
    print "Symbol: " + infile

Installing MySQLdb Python Module

If you are running Python on Windows and want to install the MySQL python module, check out http://www.lfd.uci.edu/~gohlke/pythonlibs/. Go down to MySQL-python and select the file that corresponds to your version of Python (2.6,2.7,3.2) and Windows (x86 or 64). Double click, install, and you're good to go.

Non-GUI IB Historical Data Downloader

Here's some code to download quotes for the constituents of the S&P500 through Interactive Brokers' API using Python 2.7 and IbPy.

It will pause for 10 seconds after each request so that you won't go over the 60 symbol a minute limit. Also, it keeps track of which symbols you have downloaded. If you need to check for missed symbols go to the "downloaded_symbols.csv" file and match it to the entire S&P500 list. Other than the fact that you need a folder named "csv_day_test" in the same folder as the python script, there's not much to using this. It's pretty simple (and shouldn't hang like the GUI version sometimes does).

It's hard coded to do "1 min" bars over the course of "1 D". Change these if you'd like to download other data. You can also add/subtract symbols as you would expect. If you want it to print data it receives into the Python window, you can remove the # mark before the "print msg.reqId, msg.date," ... etc. This will, however, slow things down to some extent.

Here's the code:

 from time import sleep, strftime, localtime  
 from ib.ext.Contract import Contract  
 from ib.opt import ibConnection, message  
 import _mysql  
   
 new_symbolinput = ['MMM','ACE','AES','AFL','GAS','T','ABT','ANF','ACN','ADBE','AMD','AET','A','APD','ARG','AKAM','AA','ALXN','ATI','AGN','ALL','ANR','ALTR','MO','AMZN','AEE','AEP','AXP','AIG','AMT','AMP','ABC','AMGN','APH','APC','ADI','AON','APA','AIV','APOL','AAPL','AMAT','ADM','AIZ','AN','AZO','ADSK','ADP','AVB','AVY','AVP','BBT','BMC','BHI','BLL','BAC','BCR','BAX','BEAM','BDX','BBBY','BMS','BRK B','BBY','BIG','BIIB','BLK','HRB','BA','BWA','BXP','BSX','BMY','BRCM','BF B','CA','CBG','CBS','CF','CHRW','CMS','CNX','CSX','CVS','CVC','COG','CAM','CPB','COF','CAH','CFN','KMX','CCL','CAT','CELG','CNP','CTL','CERN','CHK','CVX','CME','CMG','CB','CI','CINF','CTAS','CSCO','C','CTXS','CLF','CLX','COH','KO','CCE','CTSH','CL','CMCSA','CMA','CSC','CAG','COP','ED','STZ','CBE','GLW','COST','CVH','COV','CCI','CMI','DTV','DTE','DHR','DRI','DVA','DV','DF','DE','DELL','DNR','XRAY','DVN','DO','DFS','DISCA','DLTR','D','RRD','DOV','DOW','DPS','DD','DUK','DNB','ETFC','EMC','EOG','EQT','EMN','ETN','ECL','EIX','EW','EA','EMR','ETR','EFX','EQR','EL','EXC','EXPE','EXPD','ESRX','XOM','FFIV','FLIR','FMC','FTI','FDO','FAST','FDX','FII','FIS','FITB','FHN','FSLR','FE','FISV','FLS','FLR','F','FRX','FOSL','BEN','FCX','FTR','GME','GCI','GPS','GD','GE','GIS','GPC','GNW','GILD','GS','GR','GT','GOOG','GWW','HCP','HAL','HOG','HAR','HRS','HIG','HAS','HCN','HNZ','HP','HSY','HES','HPQ','HD','HON','HRL','DHI','HSP','HST','HCBK','HUM','HBAN','ITW','IR','TEG','INTC','ICE','IPG','IBM','IFF','IGT','IP','INTU','ISRG','IVZ','IRM','JDSU','JPM','JBL','JEC','JNJ','JCI','JOY','JNPR','KLAC','K','KEY','KMB','KIM','KMI','KSS','KFT','KR','LLL','LSI','LH','LRCX','LM','LEG','LEN','LUK','LXK','LIFE','LLY','LTD','LNC','LLTC','LMT','L','LO','LOW','MTB','M','MRO','MPC','MAR','MMC','MAS','MA','MAT','MKC','MCD','MHP','MCK','MJN','MWV','MDT','MRK','MET','PCS','MCHP','MU','MSFT','MOLX','TAP','MON','MCO','MS','MOS','MSI','MUR','MYL','NKE','NRG','NYX','NBR','NDAQ','NOV','NTAP','NFLX','NWL','NFX','NEM','NWSA','NEE','NI','NE','NBL','JWN','NSC','NU','NTRS','NOC','NUE','NVDA','ORLY','OKE','OXY','OMC','ORCL','OI','PCAR','PCG','PNC','PPG','PPL','PLL','PH','PDCO','PAYX','BTU','JCP','PBCT','POM','PEP','PKI','PRGO','PFE','PM','PSX','PNW','PXD','PBI','PCL','PX','PCP','PCLN','PFG','PLD','PG','PGN','PGR','PRU','PEG','PSA','PHM','QEP','QCOM','PWR','DGX','RL','RRC','RTN','RHT','RF','RSG','RAI','RHI','ROK','COL','ROP','ROST','RDC','R','SAI','SCG','SLM','SWY','CRM','SNDK','SLE','SLB','SCHW','SNI','SEE','SHLD','SRE','SHW','SIAL','SPG','SJM','SNA','SO','LUV','SWN','SE','S','STJ','SWK','SPLS','SBUX','HOT','STT','SRCL','SYK','STI','SUN','SYMC','SYY','TROW','TEL','TE','TJX','TGT','THC','TDC','TER','TSO','TXN','TXT','BK','WMB','TMO','TIF','TWC','TWX','TIE','TMK','TSS','TRV','TRIP','TYC','TSN','USB','UNP','UPS','X','UTX','UNH','UNM','URBN','VFC','VLO','VAR','VTR','VRSN','VZ','VIAB','V','VNO','VMC','WPX','WMT','WAG','DIS','WPO','WM','WAT','WPI','WLP','WFC','WDC','WU','WY','WHR','WFM','WIN','WEC','WYN','WYNN','XL','XEL','XRX','XLNX','XYL','YHOO','YUM','ZMH','ZION','EBAY']  
 newDataList = []  
 dataDownload = []  
   
 def historical_data_handler(msg):  
   global newDataList  
   #print msg.reqId, msg.date, msg.open, msg.high, msg.low, msg.close, msg.volume  
   if ('finished' in str(msg.date)) == False:  
     new_symbol = new_symbolinput[msg.reqId]  
     dataStr = '%s, %s, %s, %s, %s, %s, %s' % (new_symbol, strftime("%Y-%m-%d %H:%M:%S", localtime(int(msg.date))), msg.open, msg.high, msg.low, msg.close, msg.volume)  
     newDataList = newDataList + [dataStr]  
   else:  
     new_symbol = new_symbolinput[msg.reqId]  
     filename = 'minutetrades' + new_symbol + '.csv'  
     csvfile = open('csv_day_test/' + filename,'wb')  
     for item in newDataList:  
       csvfile.write('%s \n' % item)  
     csvfile.close()  
     newDataList = []  
     global dataDownload  
     dataDownload.append(new_symbol)  
   
 con = ibConnection()  
 con.register(historical_data_handler, message.HistoricalData)  
 con.connect()  
   
 symbol_id = 0  
 for i in new_symbolinput:  
   print i  
   qqq = Contract()  
   qqq.m_symbol = i  
   qqq.m_secType = 'STK'  
   qqq.m_exchange = 'SMART'  
   qqq.m_currency = 'USD'  
   con.reqHistoricalData(symbol_id, qqq, '', '1 D', '1 min', 'TRADES', 1, 2)  
   
   symbol_id = symbol_id + 1  
   sleep(10)  
   
 print dataDownload  
 filename = 'downloaded_symbols.csv'  
 csvfile = open('csv_day_test/' + filename,'wb')  
 for item in dataDownload:  
   csvfile.write('%s \n' % item)  
 csvfile.close()  

Note: Downloads everything except SLE and PGN. Might not be symbols on the S&P500; haven't checked yet...

Amazon's Hardware

This article doesn't seem, to me, to give any insight into Amazon's desire to stay in hardware, mostly through Kindle. With a comment like the following:

"I have said this many times, but I believe the Internet is the platform of the future."

I am losing more and more (undeserved) faith in the media industry to provide interesting, informative, insightful, and thought-provoking pieces. 

You believe the Internet is the platform of the future? Well, you must be a fucking visionary.

Monday, July 23, 2012

Some Comments

The Epicurean Dealmaker has to be one of my favorite bloggers and, recently, he has hit 1M unique views. If you have yet to enjoy his writing, I suggest you check him out.

And, this has to be one of the worst articles I have read on "VIX trading". Maybe it's just because I have a friend whose work at a hedge fund deals almost entirely with the VIX. Or maybe it's because I am in a critical mood.

Probably both.

IB Historical Quotes Downloader

Slowly getting to where I want it to be. Here's a workable version that downloads quotes (you can insert multiple tickers with commas separating them--no spaces after the commas) to csv files and stores them in a subdirectory "csv_day". The MySQL part shouldn't be difficult. But I'm working on dealing with IB's quote download limits and some python inefficiency.

Here's the current code:

 from time import sleep, strftime, localtime  
 from ib.ext.Contract import Contract  
 from ib.opt import ibConnection, message  
 from Tkinter import *  
 import _mysql  
 import string  
   
 class App:  
   
   def __init__(self, master):  
   
     #list to keep data series, list for multiple symbols, j to keep track of current symbol  
     self.newDataList = []  
     self.new_symbolinput = []  
     self.j=0  
   
     #connect here to prevent double connections later on...  
     self.con = ibConnection()  
     self.con.register(self.historical_data_handler, message.HistoricalData)  
     self.con.connect()  
   
     #begin gui (labels should make it fairly self-explanatory)  
     frame = Frame(master)  
     frame.pack()  
   
     self.mysqlinfo_label = Label(frame, text='MySQL fields:')  
     self.mysqlinfo_label.grid(row=0)  
   
     self.label_host = Label(frame, text='Host:')  
     self.label_host.grid(row=1)  
   
     host_text = StringVar()  
     host_text.set("127.0.0.1")  
   
     self.entry_host = Entry(frame, textvariable=host_text)  
     self.entry_host.grid(row=1, column=1)  
   
     self.label_user = Label(frame, text='User:')  
     self.label_user.grid(row=2)  
   
     user_text = StringVar()  
     user_text.set("root")  
   
     self.entry_user = Entry(frame, textvariable=user_text)  
     self.entry_user.grid(row=2, column=1)  
   
     self.label_password = Label(frame, text='Password:')  
     self.label_password.grid(row=3)  
   
     self.entry_password = Entry(frame, show="*")  
     self.entry_password.grid(row=3, column=1)  
   
     self.label_database = Label(frame, text='Database:')  
     self.label_database.grid(row=4)  
   
     database_text = StringVar()  
     database_text.set("stocks")  
   
     self.entry_database = Entry(frame, textvariable=database_text)  
     self.entry_database.grid(row=4, column=1)  
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=5)   
   
     self.label_twsfields = Label(frame, text='TWS fields:')  
     self.label_twsfields.grid(row=6)  
   
     self.label_server = Label(frame, text='Server:')  
     self.label_server.grid(row=7)  
   
     twsserver_text = StringVar()  
     twsserver_text.set("127.0.0.1")  
   
     self.entry_server = Entry(frame, textvariable=twsserver_text)  
     self.entry_server.grid(row=7, column=1)  
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=8)   
   
     self.label_twscontractinfo = Label(frame, text='TWS contract info:')  
     self.label_twscontractinfo.grid(row=9)  
   
     self.label_symbol = Label(frame, text='Symbol:')  
     self.label_symbol.grid(row=10)  
   
     self.entry_symbol = Entry(frame)  
     self.entry_symbol.grid(row=10, column=1)  
   
     self.label_barsize = Label(frame, text='Bar Size:')  
     self.label_barsize.grid(row=11)  
   
     self.barsize_selected = StringVar(frame)  
     self.barsize_selected.set("1 min")  
   
     self.optionmenu_barsize = OptionMenu(frame, self.barsize_selected, "30 secs", "1 min", "5 mins", "10 mins", "15 mins", "1 hour", "4 hours", "1 day")  
     self.optionmenu_barsize.grid(row=11, column=1)  
   
     self.label_duration = Label(frame, text='Duration:')  
     self.label_duration.grid(row=12)  
   
     self.duration_selected = StringVar(frame)  
     self.duration_selected.set("1 W")  
   
     self.optionmenu_duration = OptionMenu(frame, self.duration_selected, "1 H", "4 H", "1 D", "1 W", "1 M", "1 Y")  
     self.optionmenu_duration.grid(row=12, column=1)  
       
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=13)      
   
     self.button_download = Button(frame, text="Download", command=self.tws_connect)  
     self.button_download.grid(row=14, column=1)  
   
     self.button_import = Button(frame, text="Import", command=self.mysql_connect)  
     self.button_import.grid(row=15, column=1)  
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=16)   
   
   #function for sending historical data requests  
   def tws_connect(self):  
     print "connecting to tws..."  
     print "tws server: " + self.entry_server.get()  
   
     self.new_symbolinput = string.split(self.entry_symbol.get(), ',')  
   
     print self.new_symbolinput  
   
     #run through all symbols, requesting historical data  
     self.symbol_id = 0  
     for i in self.new_symbolinput:  
       print i  
       qqq = Contract()  
       qqq.m_symbol = i  
       qqq.m_secType = 'STK'  
       qqq.m_exchange = 'SMART'  
       qqq.m_currency = 'USD'  
       endtime = strftime('%Y%m%d %H:%M:%S')  
       durationreq = '%s' % self.duration_selected.get()  
       barsizereq = '%s' % self.barsize_selected.get()  
       self.con.reqHistoricalData(self.symbol_id, qqq, '', durationreq, barsizereq, 'TRADES', 1, 2)  
       self.symbol_id = self.symbol_id + 1  
   
       #if doing more than 60 symbols, this is a simple way to avoid IB's 60symbol/10min limit...  
         
   #function for putting data into csv file and, eventually, into mysql database  
   def mysql_connect(self):  
       
     print "MySQL host: " + self.entry_host.get()  
     print "MySQL user: " + self.entry_user.get()  
     print "MySQL database: " + self.entry_database.get()  
   
   #required for IB API  
   def historical_data_handler(self, msg):  
     print msg.reqId, msg.date, msg.open, msg.high, msg.low, msg.close, msg.volume  
   
     #don't add 'finished...' statement to data list  
     if ('finished' in str(msg.date)) == False:  
       new_symbol = self.new_symbolinput[msg.reqId]  
       dataStr = '%s, %s, %s, %s, %s, %s, %s' % (new_symbol, strftime("%Y-%m-%d %H:%M:%S", localtime(int(msg.date))), msg.open, msg.high, msg.low, msg.close, msg.volume)  
       self.newDataList.append(dataStr)  
     #if 'finished...' appears jump to next symbol (THIS IS BROKEN because IB doesn't necessarily send requests back to you in the order sent to them...)  
     else:  
       new_symbol = self.new_symbolinput[msg.reqId]  
       filename = 'minutetrades' + new_symbol + '.csv'  
       csvfile = open('csv_day/' + filename,'wb')  
       for item in self.newDataList:  
         csvfile.write('%s \n' % item)  
       csvfile.close()  
       self.newDataList = []  
   
 root = Tk()  
 root.title('Historical Data: Download and Import')  
 app = App(root)  
   
 #run gui  
 root.mainloop()  

Saturday, July 21, 2012

Multiple Tickers on IB

I believe I've fixed the problem accessing multiple tickers under my Python IB historical data extractor. Instead of hoping IB will send me the data in the order I sent IB the data, I match the initial tickerId I send IB with the msg.reqId I receive back. All tickers get aligned correctly and if you happen to input an invalid ticker or IB decides not to send you a ticker, it doesn't get matched up to some other ticker's data (you just don't get it in the output file).

I'll post the code shortly...I'd like to get the MySQL part done too.

Friday, July 20, 2012

Super-Size Me?

I skimmed this article earlier today. It's about the growing size of phones. I, instead, think of it more as the shrinking of the computer with the addition of phone capabilities. Perhaps others dislike the size of their phone, but I am much happier with my Samsung Charge than with anything I've had before...and I wouldn't mind an upgrade.

Thursday, July 19, 2012

Apple, Bellagio Comparison

This is an interesting article about why Apple will continue winning. However, I think he cuts his analogy slightly short. With Steve Jobs gone, what is going to drive Apple to uphold its amazing service and continue growing its loyal customer base? Stock holders? I don't think so.

Why Scott Bell doesn't make the connection between the absence of Wynn at the Bellagio and the absence of Jobs (much more recent) at Apple, beats the hell out of me. It's not mentioned. I wouldn't be surprised if someone at Apple started to cut corners.

And, I wouldn't be surprised if the fervent loyalty begins to die off (perhaps slowly) now that Apple's religious leader is dead.

Wednesday, July 18, 2012

People You Need

I'm a tad surprised that Abnormal Returns didn't post a link to this article about the people who you need in "your corner." It's simple. It's good. So many people tend to forget that to grow as an individual you need others. /steps off soap box.

Tuesday, July 17, 2012

Download Historical Data From Interactive Brokers

If you enjoy the following, consider signing up for the Gödel's Market Newsletter.

It's not quite done. I haven't worked out the kinks with downloading multiple symbols at a time (sometimes it works, sometimes it doesn't download one and the symbol names in the csv file get screwed up...). Also, it doesn't connect and import the created csv file into your MySQL database. That shouldn't be too big of a step, especially since it's already programmed for the Yahoo! Importer. Working out the other kink will take more time.

Again, you need certain modules imported. Python-MySQL and IbPy, being the most important.

Here's alpha code v0.01:

 from time import sleep, strftime, localtime  
 from ib.ext.Contract import Contract  
 from ib.opt import ibConnection, message  
 from Tkinter import *  
 import _mysql  
 import csv  
 import string  
   
   
 class App:  
   
   def __init__(self, master):  
   
     self.newDataList = []  
     self.new_symbolinput = []  
     self.j=0  
   
     #connect here to prevent double connections later on...  
     self.con = ibConnection()  
     self.con.register(self.historical_data_handler, message.HistoricalData)  
     self.con.connect()  
   
     frame = Frame(master)  
     frame.pack()  
   
     self.mysqlinfo_label = Label(frame, text='MySQL fields:')  
     self.mysqlinfo_label.grid(row=0)  
   
     self.label_host = Label(frame, text='Host:')  
     self.label_host.grid(row=1)  
   
     host_text = StringVar()  
     host_text.set("127.0.0.1")  
   
     self.entry_host = Entry(frame, textvariable=host_text)  
     self.entry_host.grid(row=1, column=1)  
   
     self.label_user = Label(frame, text='User:')  
     self.label_user.grid(row=2)  
   
     user_text = StringVar()  
     user_text.set("root")  
   
     self.entry_user = Entry(frame, textvariable=user_text)  
     self.entry_user.grid(row=2, column=1)  
   
     self.label_password = Label(frame, text='Password:')  
     self.label_password.grid(row=3)  
   
     self.entry_password = Entry(frame, show="*")  
     self.entry_password.grid(row=3, column=1)  
   
     self.label_database = Label(frame, text='Database:')  
     self.label_database.grid(row=4)  
   
     database_text = StringVar()  
     database_text.set("stocks")  
   
     self.entry_database = Entry(frame, textvariable=database_text)  
     self.entry_database.grid(row=4, column=1)  
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=5)   
   
     self.label_twsfields = Label(frame, text='TWS fields:')  
     self.label_twsfields.grid(row=6)  
   
     self.label_server = Label(frame, text='Server:')  
     self.label_server.grid(row=7)  
   
     twsserver_text = StringVar()  
     twsserver_text.set("127.0.0.1")  
   
     self.entry_server = Entry(frame, textvariable=twsserver_text)  
     self.entry_server.grid(row=7, column=1)  
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=8)   
   
     self.label_twscontractinfo = Label(frame, text='TWS contract info:')  
     self.label_twscontractinfo.grid(row=9)  
   
     self.label_symbol = Label(frame, text='Symbol:')  
     self.label_symbol.grid(row=10)  
   
     self.entry_symbol = Entry(frame)  
     self.entry_symbol.grid(row=10, column=1)  
   
     self.label_barsize = Label(frame, text='Bar Size:')  
     self.label_barsize.grid(row=11)  
   
     self.barsize_selected = StringVar(frame)  
     self.barsize_selected.set("1 min")  
   
     self.optionmenu_barsize = OptionMenu(frame, self.barsize_selected, "30 secs", "1 min", "5 mins", "10 mins", "15 mins", "1 hour", "4 hours", "1 day")  
     self.optionmenu_barsize.grid(row=11, column=1)  
   
     self.label_duration = Label(frame, text='Duration:')  
     self.label_duration.grid(row=12)  
   
     self.duration_selected = StringVar(frame)  
     self.duration_selected.set("1 W")  
   
     self.optionmenu_duration = OptionMenu(frame, self.duration_selected, "1 H", "4 H", "1 D", "1 W", "1 M", "1 Y")  
     self.optionmenu_duration.grid(row=12, column=1)  
       
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=13)      
   
     self.button_download = Button(frame, text="Download", command=self.tws_connect)  
     self.button_download.grid(row=14, column=1)  
   
     self.button_import = Button(frame, text="Import", command=self.mysql_connect)  
     self.button_import.grid(row=15, column=1)  
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=16)   
   
   def say_hi(self):  
     print "loading data..."  
   
   def tws_connect(self):  
     print "connecting to tws..."  
     print "tws server: " + self.entry_server.get()  
   
     self.new_symbolinput = string.split(self.entry_symbol.get(), ',')  
   
     #print raw_symbol_input  
     print self.new_symbolinput  
   
     print self.j  
   
     for i in self.new_symbolinput:  
       print i  
       qqq = Contract()  
       qqq.m_symbol = i  
       qqq.m_secType = 'STK'  
       qqq.m_exchange = 'SMART'  
       qqq.m_currency = 'USD'  
       endtime = strftime('%Y%m%d %H:%M:%S')  
       durationreq = '%s' % self.duration_selected.get()  
       barsizereq = '%s' % self.barsize_selected.get()  
       self.con.reqHistoricalData(0, qqq, '', durationreq, barsizereq, 'TRADES', 1, 2)  
   
   def mysql_connect(self):  
   
     #write newDataList to csv file  
     csvfile = open('minutetrades2.csv','wb')  
     for item in self.newDataList:  
       csvfile.write('%s \n' % item)  
   
     csvfile.close()  
   
     print "Printing dataList..."  
     print self.newDataList  
     print "connecting to mysql..."  
       
     print "MySQL host: " + self.entry_host.get()  
     print "MySQL user: " + self.entry_user.get()  
     print "MySQL database: " + self.entry_database.get()  
   
     self.contract_info()  
   
   def contract_info(self):  
     print "contract info..."  
     print "Symbol: " + self.entry_symbol.get()  
     print "Bar size: " + self.barsize_selected.get()  
   
   def historical_data_handler(self, msg):  
     print msg.date, msg.open, msg.high, msg.low, msg.close, msg.volume  
     if ('finished' in str(msg.date)) == False:  
       new_symbol = self.new_symbolinput[self.j]  
       dataStr = '%s, %s, %s, %s, %s, %s, %s' % (new_symbol, strftime("%Y-%m-%d %H:%M:%S", localtime(int(msg.date))), msg.open, msg.high, msg.low, msg.close, msg.volume)  
       #prevent addition of 'finished...' statement to newDataList  
       self.newDataList.append(dataStr)  
     else:  
       self.j = (self.j)+1  
       print self.j  
   
 root = Tk()  
 root.title('Historical Data: Download and Import')  
 app = App(root)  
   
 root.mainloop()  

(If you've enjoyed this article, consider signing up for the Gödel's Market Newsletter.)

Sunday, July 15, 2012

Synergy

For traders, quants, or anyone who wants to use multiple computers and screens under a single keyboard/mouse combo, check out Synergy. The software works really well, and it's free. Currently I have it  in use across two computers and three monitors.

MATLAB Built-in Financial Functions

I wrote a Black-Scholes option price calculator function in MATLAB and was working on an implied volatility calculator when I came across some built-in MATLAB functions such as blsprice (Black-Scholes price) and blsimpv (to calculate implied volatility using the Black-Scholes equation).

Pretty cool. I'm sure there are countless others as well...

Put-Call Parity Calculator

If you enjoy the following, consider signing up for the Gödel's Market Newsletter.

I've been reading "Numerical Methods in Finance and Economics, A MATLAB Based Introduction 2nd Edition". They obviously go into put-call parity early on. It's an extremely fascinating concept that given the risk free interest rate (r), the distance until expiration (t), the initial stock price (s), and the strike price (k), that you can then solve for the price of the call (c) given you have the price of the put (p) or vice versa.

No arbitrage financial pricing is extremely interesting (and practical). Here's a MATLAB script I wrote that you can use to look for "arbitrage" opportunity in the put-call parity sense. It'll ask you for several inputs (r,t,s,k,c,p) and tell you what the "call side value" (c + k*e(-r*t)) is and what the "put side value" (p + s) is. You can then compare.

(Example with current SPY options; first part is entry, second is output:)



 % arbitrage; put-call parity checker  
   
 %risk free interest rate  
 r = input('What is the risk free rate? ');  
   
 %time period  
 t = input('How long until expiration? ');  
   
 %stock price, initially  
 s = input('What is the initial stock price? ');  
   
 %strike price  
 k = input('What is the strike price? ');  
   
 %grab call price  
 c = input('What is the call price? ');  
   
 %grab put price  
 p = input('What is the put price? ');  
   
 fprintf('Risk free rate: %d \n', r);  
 fprintf('Time period: %d \n', t);  
 fprintf('Initial stock value: %d \n', s);  
 fprintf('Strike price: %d \n', k);  
 fprintf('Call price: %d \n', c);  
 fprintf('Put price: %d \n', p);  
   
 call_side_value = c + k*exp(-r*t)  
 put_side_value = p + s  

(Edit: fixed an error in call_side_value calculation. Should be good to go.)

(If you've enjoyed this article, consider signing up for the Gödel's Market Newsletter.)

Wednesday, July 11, 2012

Matlab Stock Correlation Matrix

If you enjoy the following, consider signing up for the Gödel's Market Newsletter.

So, maybe you used the Python Yahoo! data importer script I posted; or, maybe, you have your own price data. And, perhaps you want to run a script to see what the correlation is among the stocks in your database. This code will do just that.

You need to download the MySQL JDBC connector and go through the hassle of adding the .jar file to your Matlab java path. It's worth the struggle, though.

Here's the code with some comments to hopefully help you along (and remind me what I did later):

   
 %test variables to limit matrix sizes...  
 num_stocks = 500;  
 num_close_values = 20;  
 num_log_values = 19;  
   
 %connect to the database  
 conn = database('stocks','root','*****','com.mysql.jdbc.Driver','jdbc:mysql://localhost:3306/stocks');  
   
 %query the symbols in the database  
 str_query_symbols = 'SELECT distinct symbol FROM stock_prices_day';  
 symbol_cell = fetch(conn, str_query_symbols);  
   
 %create matrices to prevent matrix resizing when inserting cell values..  
 mat_log_change = zeros(num_log_values,num_stocks);  
 mat_values_adj_close = zeros(num_close_values,num_stocks);  
   
 %grab adj_close info for a each symbol; then cell2mat it into a larger  
 %matrix  
 for i = 1:num_stocks  
   symbol = cell2mat(symbol_cell(i));  
   str_query_adj_close = ['SELECT adj_close FROM stock_prices_day WHERE symbol = ''' symbol ''' and date > ''2012-06-01'''];  
   mat_values_adj_close(:,i) = cell2mat(fetch(conn, str_query_adj_close));  
     
   %use this to calculate natrual log changes as you move along  
   for j = 1: num_log_values  
     %mat_values_adj_close(j,i)  
     mat_log_change(j,i) = log((mat_values_adj_close(j,i))/(mat_values_adj_close((j+1),i)));  
   end  
 end  
   
 %next calculate correlation coefficients among the various columns  
 mat_correlations = zeros(num_stocks,num_stocks);  
 for i = 1:num_stocks  
   for j = 1:num_stocks  
     A = mat_log_change(:,i);  
     B = mat_log_change(:,j);  
     R = corrcoef(A,B);  
     mat_correlations(i,j) = R(2);  
   end  
 end  
   
 mat_correlations  

(If you've enjoyed this article, consider signing up for the Gödel's Market Newsletter.)

Correlation Correlations

I really like this article linked by Abnormal Returns. Look at the relatively close correlation of the S&P 500 and Euro 350. And then look at the returns.

Tuesday, July 10, 2012

Simple Version of Yahoo! MySQL Importer

If you enjoy the following, consider signing up for the Gödel's Market Newsletter.

I decided to post the source code for my Yahoo! Stock Quote MySQL Importer Python script.

This is the simple version without the ability to add multiple stock tickers at once and no choice of time limits. Also, it doesn't download economic info or keep track of dividends.

In order to use this, you must have Python 2.7 (or something that can run the code equivalently; I don't think Python 3+ works), the Python-MySQL (MySQLdb) module, and MySQL installed (I'm using server 5.5).

Within your MySQL server you must have a "schema" (or "database") called "stocks". Actually, you can call it whatever you want, because you have the option to change the name in the Python program. But, it is preselected for "stocks".

Within this "schema"/"database" you must have a table called "stock_prices_day". This is hardcoded, but can obviously be changed if you know a little python, or just read through the code and look for any instance of "stock_prices_day" and replace them with whatever you want your table to be called.

Within "stock_prices_day" you must have the following columns with the indicated data types: "symbol" varchar(5), "date" date, "open" decimal(5,3), "high" decimal(5,3), "low" decimal(5,3), "close" decimal(5,3), "volume" int, "adj_close" decimal(5,3).

When you have all of that, you can run the following code, insert your server information (ip, username, password, etc) and the symbol you want to insert. It'll download it and insert the symbol into the database.

BAM! you're on your way to a real hedge fund-esque setup. :D (well, almost).

Rules on the code:
1) Only for personal use by investors with less than $100k in liquid assets (not for use by businesses, corporations, etc).
2) Not for resale and reuse requires my consent.
3) Post a comment if you'd like access to a more complete version (date selection, multiple symbols, etc).


 from time import sleep, strftime, localtime  
 from Tkinter import *  
 import MySQLdb  
 import urllib  
 import csv  
 import os  
   
 class App:  
   
   def __init__(self, master):  
   
     frame = Frame(master)  
     frame.pack()  
   
     self.mysqlinfo_label = Label(frame, text='MySQL fields:')  
     self.mysqlinfo_label.grid(row=0)  
   
     self.label_host = Label(frame, text='Host:')  
     self.label_host.grid(row=1)  
   
     host_text = StringVar()  
     host_text.set("127.0.0.1")  
   
     self.entry_host = Entry(frame, textvariable=host_text)  
     self.entry_host.grid(row=1, column=1)  
   
     self.label_user = Label(frame, text='User:')  
     self.label_user.grid(row=2)  
   
     user_text = StringVar()  
     user_text.set("root")  
   
     self.entry_user = Entry(frame, textvariable=user_text)  
     self.entry_user.grid(row=2, column=1)  
   
     self.label_password = Label(frame, text='Password:')  
     self.label_password.grid(row=3)  
   
     self.entry_password = Entry(frame, show="*")  
     self.entry_password.grid(row=3, column=1)  
   
     self.label_database = Label(frame, text='Database:')  
     self.label_database.grid(row=4)  
   
     database_text = StringVar()  
     database_text.set("stocks")  
   
     self.entry_database = Entry(frame, textvariable=database_text)  
     self.entry_database.grid(row=4, column=1)  
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=5)   
   
     self.label_twscontractinfo = Label(frame, text='Yahoo info:')  
     self.label_twscontractinfo.grid(row=9)  
   
     self.label_symbol = Label(frame, text='Symbol:')  
     self.label_symbol.grid(row=10)  
   
     self.entry_symbol = Entry(frame)  
     self.entry_symbol.grid(row=10, column=1)  
       
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=13)      
   
     self.button_download = Button(frame, text="Download", command=self.tws_connect)  
     self.button_download.grid(row=14, column=1)  
   
     self.button_import = Button(frame, text="Import", command=self.mysql_connect)  
     self.button_import.grid(row=15, column=1)  
   
     self.label_empty = Label(frame, text='')  
     self.label_empty.grid(row=16)   
   
   
   def tws_connect(self):  
     print "downloading csv file from yahoo..."  
     webFile = urllib.urlopen("http://ichart.finance.yahoo.com/table.csv?s=" + self.entry_symbol.get())  
     fileName = self.entry_symbol.get() + ".csv"  
     localFile = open(fileName.split('/')[-1], 'w')  
     localFile.write(webFile.read())  
     webFile.close()  
     localFile.close()  
   
     fieldnames = ['Symbol', 'Date', 'Open', 'High', 'Low', 'Close', 'Volume', 'Adj Close']  
     fileName2 = self.entry_symbol.get() + "2.csv"  
     with open(fileName, 'rb') as csvinput:  
       with open(fileName2, 'wb') as csvoutput:  
         csvwriter = csv.DictWriter(csvoutput, fieldnames, delimiter=',')  
         csvwriter.writeheader()  
         for row in csv.DictReader(csvinput):  
           row['Symbol'] = self.entry_symbol.get()  
           csvwriter.writerow(row)  
   
   
   def mysql_connect(self):  
     print "connecting to mysql..."  
       
     print "MySQL host: " + self.entry_host.get()  
     print "MySQL user: " + self.entry_user.get()  
     print "MySQL database: " + self.entry_database.get()  
   
     db = MySQLdb.connect(host=self.entry_host.get(), user=self.entry_user.get(),passwd=self.entry_password.get(), db=self.entry_database.get())  
     cur = db.cursor()  
   
     current_path = os.path()  
   
     print current_path  
   
     cur.execute("load data local infile '" + current_path + self.entry_symbol.get() + "2.csv' into table `stocks`.`stock_prices_day` fields terminated by ',' lines terminated by '\n' ignore 1 lines (`symbol`,`date`,`open`,`high`,`low`,`close`,`volume`,`adj_close`);")  
     db.commit()  
     self.contract_info()  
   
   def contract_info(self):  
     print "contract info..."  
     print "Symbol: " + self.entry_symbol.get()  
   
 root = Tk()  
 root.title('Historical Data: Download and Import')  
 app = App(root)  
   
 root.mainloop()  


(If you've enjoyed this, consider signing up for the Gödel's Market Newsletter.)

Monday, July 9, 2012

Those Who Finish Second

Here's a really good article. Yeah, it may not always be true that those who finish first were mostly lucky or bold; but, it can certainly be the case. Think Warren Buffet, Bill Gates, or Mark Zuckerberg. They're all intelligent, skillful, and successful. But do you do yourself a favor following their strategies? Probably not.

Poker Notes

Bovada.lv deposited a free $20 in my account. I haven't played for quite some time, but I'm up to >$170 (playing $25 buyin .10/.25). Here are a few notes on the game of poker that have crossed my mind:


-create a model of other players' hands

-determine best bet to further refine player hands

-use further betting to extract as much value based on others' hand

-all while keeping your hand in mind
although no need for it to win, if strategy typically
leads to others folding

-keep appearances in mind (not too much raising, folding, etc)

-always try to play with the 'soft'
why make your life harder than it has to be?

I think something similar can be used for trading.

Sunday, July 8, 2012

Yahoo!

So, I still haven't gotten around to packing everything into a self-installing exe. I've been extremely busy with job interviews, sharpening my programming skills, and hanging out with friends before I most likely move some 400 miles away.

I have been able to use the MySQL server I setup on a new very inexpensive server. I've downloaded Yahoo! data on the companies currently on the S&P500 and have ran several back tests with one of Charles D. Kirkpatrick's books in mind. I don't have any definitive results yet. But it looks promising.

I'm thinking of rewriting the Yahoo! program in Java for easier distribution. Or maybe C/C++. I don't know. I know I'm not ready to release it open source.

More soon.

Saturday, June 30, 2012

Yahoo! Import Update

If you enjoy the following, consider signing up for the Gödel's Market Newsletter.

I still haven't wrapped everything up into an installer. Here, however, is an up-to-date screenshot. I don't think these features will be available in the free version, but I added:

- Multiple symbols (separated by " ,") imported at once
- A way to choose how far back the data goes (jan = 00...dec = 11) so that you can update daily without inserting repeat data (I'll probably add something to make sure you're not adding repeats later).


That's it for the moment. I do want to add a txt file loader (with symbol info) and a way to capture dividend data. Perhaps I'll make one for economic and earnings data too.

(If you've enjoyed this, consider signing up for the Gödel's Market Newsletter.)

Import Yahoo Stock CSV Files into MySQL -- Program

If you enjoy this article, consider signing up for the Gödel's Market Newsletter.

This is a very preliminary version (Windows 7 tested) of what I intend to create. But, I thought I would share it.

What the program does:
- Downloads the CSV file of the specified stock symbol from Yahoo! Finance.
- Adds a column in the CSV file with symbol info so that you can insert multiple stock tickers in the same table and sort them by "Symbol."
- Connects to the selected database and inserts the data into a table called "stock_prices_day" with column values "Symbol", "Date", "Open", "High", "Low", "Close", "Volume", "Adj_Close".

-There are simple modifications that may need to be made in order for this to run on machines besides my own. I'll try to get around to it as soon as I find the errors.
- There are more complicated modifications that would obviously be nice, which I will (and have) work on. These will likely be kept to myself, unless there is substantial interest. Donations would be nice given that even the simple version took me quite some time to put together... :D (plus the benefits of this program to a quantifiable trading setup are large).

License Requirements:
- This is for personal use only.
- I do not guarantee it will work.
- I don't believe it will damage anything you own, but it possibly could.
- It gives no investment advice; it is merely a tool.
- It does not come with source.

If you would like the source, I am willing to talk. I'm going to update it somewhat, because it will probably malfunction on a system that isn't my own (hard written directories, perhaps, and dll file requirements).

Here is a screenshot followed by the download.


Download - Sorry, I have to find somewhere to upload the file...I guess blogger doesn't have anything built in. (Try This)  I'm going to have to build this into an installer...hmm. 

(If you've enjoyed this article, consider signing up for the Gödel's Market Newsletter.)