Part 2 – Technical Analysis
So you’ve officially made it to Part 2. We learned some exciting things together in Part 1, and I hope you made some cash in the process. In fact, Part 1 was so successful that Part 2 has to be at least 10x more exciting right? Well my friend, you’ve never been more wrong. This portion is only for people who like a little math in their diet. This is for the people who like a little adding, a little multiplication, and even a couple mentions of standard deviation. If that stuff isn’t for you, I’m going to try to help you through this as well as I can. For every method, I will include pictures to show visually how to determine when to buy and when to sell.
Intro to Technical Analysis
Technical analysis can get pretty complicated in the real world. Luckily, I find that the simple techniques I’m going to explain have worked fairly well on the Grand Exchange, and the best part is that the math is not anything harder than what you learned in middle school. Technical analysis is a fancy term for the study of supply and demand to help predict future price movements. The basic assumptions of technical analysis are:
1. The price of an item reflects everything about the item. This includes all economic factors (supply and demand) as well as market psychology. This means that the price of an item is the only thing we care about when doing technical analysis.
2. Prices move in trends.
3. History tends to repeat itself.
The biggest limitation on this type of analysis is that all items must have significant trade volumes. This is a requirement, or there is a much higher chance that your item does not follow the above 3 assumptions because it is subject to more individual manipulation. This also means that technical analysis is not reliable for predicting the price of rares or other items which are infrequently traded.
The only time that volume information is available in the GE is for the top 100 traded items. If you’re working with those, you have a slight advantage compared to the rest of the items. The price of an item will normally follow volume. If there is a sudden spike in trading volume, the price of an item will increase while if the traded volume suddenly decreases, you should expect the price to decrease.
When using technical analysis, the #1 piece of advice I can give you is to always use common sense when trading. Don’t rely on one method only. Each method is subject to different types of false-positives and no prediction method is 100% accurate. Hopefully by considering the recommendations of the methods below with your experience from Part 1, you can find the perfect merchanting items and reap the benefits.
How to Manage Data
In the previous section, I covered the fact that the price of an item is the only data point that we care about. That’s pretty easy, right? All we need is the price of an item and we already know everything. In order to get the trend of an item though, we need many price points. Jagex is nice enough to provide the historical data from the GE for us, so we can simply use Simba to gather all the data and present it for us. The snippet below (credit to slacky for the good parts, me for the rest) is how I’ve gathered prices for use in my MerchantAid script.
Simba Code:
function RS_GetPricesOf(Item : String; numSamples: Int32 = 100): TIntegerArray;
var
i : Integer;
ID, Page, fullPage, RSpage, str, link: string;
items: TStringArray;
begin
item := Replace(item, ' ', '_', [rfReplaceAll]);
fullPage := GetPage('http://runescape.wikia.com/wiki/Exchange:' + Item);
ID := extractFromStr(between('"GEDBID">', '</span>', fullPage), Numbers);
link := 'http://services.runescape.com/m=itemdb_rs/'+ Item + '/viewitem.ws?obj=' + ID;
rsPage := getPage(link);
Items := MultiBetween(RSPage, 'average180.daily.push(',');');
if numSamples > Length(items) then
WriteLn(Format('[Hint] NumSamples overflow: Could only gather %d samples', [High(items)]));
for i := Max(High(items) - numSamples, 0) to High(items) do
Result.Append(strToInt(items[i]));
end;
We have a couple options in terms of working with the price data; either we code the math into Simba and work there, or we export the prices into Excel. Both of these methods have their benefits. Excel can display data which makes it easier to visually determine trends, but Simba can run through many items much faster. The below code will grab the prices of an item and export to a .csv file which can be imported by Excel.
Simba Code:
program priceToExcel;
{$i srl-6/srl.simba}
const
theItem = 'Lobster'; // What item you want to grab prices for
amountOfPrices = 160; //160 is the max
var
scriptStartTime : String;
function RS_GetPricesOf(Item : String; numSamples: Int32 = 100): TIntegerArray;
var
i : Integer;
ID, Page, fullPage, RSpage, str, link: string;
items: TStringArray;
begin
item := Replace(item, ' ', '_', [rfReplaceAll]);
fullPage := GetPage('http://runescape.wikia.com/wiki/Exchange:' + Item);
ID := extractFromStr(between('"GEDBID">', '</span>', fullPage), Numbers);
link := 'http://services.runescape.com/m=itemdb_rs/'+ Item + '/viewitem.ws?obj=' + ID;
rsPage := getPage(link);
Items := MultiBetween(RSPage, 'average180.daily.push(',');');
if numSamples > Length(items) then
WriteLn(Format('[Hint] NumSamples overflow: Could only gather %d samples', [High(items)]));
for i := Max(High(items) - numSamples, 0) to High(items) do
Result.Append(strToInt(items[i]));
end;
procedure startTime;
var
Hour, Mins, Sec, MSec: Word;
Year, Month, Day : UInt16;
suffix : String;
begin
DecodeTime(Now, Hour, Mins, Sec, MSec);
DecodeDate(Now, Year, Month, Day);
if Hour < 12 then Suffix := 'AM' else Suffix := 'PM'
if Hour > 12 then Hour -= 12;
scriptStartTime := (' ' + toStr(Day) + '-' + toStr(Month) + '-' + toStr(Year) + ' at ' + Padz(IntToStr(Hour), 2) + Padz(IntToStr(Mins), 2) + Suffix);
end;
procedure WritetoFile(theArray : TIntegerArray; Item : String);
var
path, str, txt: String;
theFile, i, j : Integer;
begin
try
path := ScriptPath + Item + ' ';
theFile := RewriteFile(path + scriptStartTime + '.csv', False);
txt := 'Day:,';
for i := 0 to high(theArray)-1 do
txt := txt + toStr(i) + ',';
txt := txt + toStr(high(theArray));
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + Item + ',';
for i := 0 to high(theArray)-1 do
txt := txt + toStr(theArray[i]) + ',';
txt := txt + toStr(theArray[high(theArray)]);
WriteFileString(theFile, Txt);
CloseFile(theFile);
except
Writeln('Debug saving - error occurred');
end;
end;
begin
startTime();
WriteToFile(RS_GetPricesOf(theItem, amountofPrices), theItem);
end.
To use this code, simply indicate the item you want to work with and the amount of values you want and press run. You might assume that because Jagex says they keep 180 days of price data, you would be able to grab 180 price points for analysis. They’re actually lying to you and only providing about 165 data points though, so keep your requests to 160 and below and you should be fine.
Methods
The following is a list of the methods I will be describing in the following sections.
1. Moving Averages – Comparing a short-term average against current prices.
2. Relative Strength Index (RSI) – This indicates over-sold and over-bought items.
3. Aroon Indicators – This indicates the direction and strength of a trend.
4. Stochastic Oscillators – Shows trend momentum. Can be used as a ‘trigger’ indicating buy and sell points.
5. Bollinger Bands – Measures the volatility of an item. Can also be used to indicate buy/sell points.
How to Use the Following Sections
The sections describing each indicator will be broken down into 2 mini-sections. The first will describe the usage and theory of why it works with a graph showing how I would recommend determining if you should buy or sell. The second will describe the math required to produce them and show you the code I’ve used for it. If you’re not interested in the math, hopefully you can still get something out of the theory and graphs and then just copy and paste the Simba code if you’d like to use it. To save me from having to retype math, I’ll simply be linking to what I consider a really good description of how to use the methods in the real world. At the end of the sections describing all methods, I will provide the entire script which accepts an item and number of days and outputs the Excel file with the results of all calculations. The Excel files from this script are used for all examples in the sections below.
The idea I’ve used is to combine the “buy” or “sell” recommendations for each item. I then make my decision based on the total recommendation. No single indicator is “better” than the others, and all are useful in one way or another. To make the best decision and avoid false-positives, combining all methods into a single buy/sell indicator is recommended. To help you along following these ideas, I’ll be using a graph for Lobster over the last 160 days. The price graph for Lobster over this time period looks like this:
Moving Average Usage
The simple moving average is a good tool to determine the momentum of an item, that is; what direction it’s going in and how “strong” the trend is. By taking a short-term average, say the three day average of an item, we can quickly determine the direction of the item by comparing the current price to the moving average. If the current price is above the moving average, then the item is trending up and if the current price is below the moving average then the item is trending down. This is the simplest of the technical indicators, and isn’t something that you would use in a real life stock market, but it provides a quick snapshot of the item’s recent trend.
Because of the simplicity of this method, you simply buy when the price is above the moving average and sell when the price is below the moving average. Below I’ve graphed the 3 day moving average against the price of Lobster. In the graph, you'll see the blue price line and the orange Moving Average line (values on the left Y axis) and the grey Moving Average Signal line (values on the right axis). Remember that the buy signal is a value of 1 and the sell signal is a value of -1. I've drawn black lines on the diagram showing when every signal indicates to buy or sell, and I've labelled a few of the signals to be clear which means which.
You can see from this graph that the Moving Average method is very responsive when 3 days are used. There are a few false positives, like selling in the middle of the large uptrend.
Moving Average Math
This link shows a very good and detailed explanation of how to calculate and use moving averages for technical analysis.
Below I’ve implemented the math into a loop to calculate the moving average over a period of X days (where “MADays” = X in this code) for every day in the dataset. It will recalculate the average over the last X days for each day (hence moving average, because it moves for each day).
Simba Code:
procedure doMA();
var
i, h: Integer;
avg, comp: Extended;
tempA: TIntegerArray;
begin
for i := 0 to MADays - 2 do
MARec[i] := 0; // You need at least 2 days to get data
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
for i := MADays - 1 to h do //For the rest of the prices, figure out the MA
begin
tempA := Prices.slice(i - (MADays - 1), i); // This creates a temporary array from a slice of the price array
avg := TempA.Mean(); // Grab the average of the values in the temporary slice
comp := avg - Prices[i]; // The comparison here is between the average and the current day's price
if comp > 0 then // If the average is higher than today's prices (we're in a down trend), so rec is -1 (Sell)
MARec[i] := -1
else if comp = 0 then // If the average is the same as today's prices there is no new recommendation (Hold)
MARec[i] := 0
else
MARec[i] := 1; // If the average is below today's prices (we're in an uptrend), the rec is 1 (Buy)
end;
end;
RSI Usage
RSI is a measure of the “momentum” of an item. It is a scale from 0-100 which indicates if an item is “overbought” or “oversold”. These terms are pretty self explanatory; if an item is overbought then it is expected to have lower demand soon (and crashing prices), while an oversold item is expected to soon have higher demand (and higher prices). On the scale, “oversold” is anything below 30 and “overbought” is anything above 70. Anywhere in between and the item is trading roughly as expected. If you feel more conservative, you can also use cut-offs of 20 and 80 for oversold and overbought, respectively.
The RSI method works on crossovers of the overbought and oversold lines. If the RSI score crosses from below the oversold line to above it, we treat it as a buy signal. If the score drops from above the overbought line to below it, we treat it as a sell signal. Below I’ve graphed the 4 day RSI value and shown where the buy and sell signals are. Like the Moving Average graph, the prices are in blue (left Y Axis) and the RSI Value is in orange (right Y Axis). The overbought and oversold boundaries are in grey/yellow, respectively. I've marked all buy and sell signals in black and labelled a few of them.
From this graph you can see that there are less signals with the RSI method than the Moving Average method, as well as less false-positives, but it misses some of the smaller gains and is a little slower to respond on the large ones due to the 4 day parameter instead of the 3 days of moving average.
RSI Math
This link gives a good explanation of how to calculate RSI and a good description of the buy and sell signals that can be generated.
In the below code I’ve got the method to calculate the X-day RSI score (where RSIDays = X) for an entire set of prices.
Simba Code:
procedure doRSI();
var
i, h, change : Integer;
Gains, Losses, tempG, tempL : TIntegerArray;
avgG, avgL, RSI: Extended;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
SetLength(Gains, h+1);
SetLength(Losses, h+1);
Gains[0] := 0; // The first day you can't take a comparison of gain or loss, set both to 0
Losses[0] := 0;
for i := 1 to h do // You can do comparision from day 1 (second day) to the last day, do comparisons
begin
Change := (Prices[i] - Prices[i-1]); // The change from Day[i-1] to Day[i]
if Change > 0 then Gains[i] := Change else Gains[i] := 0; //If change is positive, add it to gains (else gains is 0)
if Change < 0 then Losses[i] := (0-Change) else Losses[i] := 0; //If change is negative, add it to losses (else losses is 0)
end;
for i := 0 to RSIDays - 2 do // This is where we start assigning scores
RSIRec[i] := 0; // Can't have scores for the first (RSIDays - 2) Days (not enough data points)
for i := RSIDays - 1 to h do
begin
tempG := Gains.Slice(i - (RSIDays - 1) , i); // The temporary gains and losses arrays are generated for processing
tempL := Losses.Slice(i - (RSIDays - 1), i);
avgG := tempG.Mean(); //Take the average of each slice
avgL := tempL.Mean();
if avgL = 0 then // This saves us from dividing by 0, the RSI Score is set to 100
RSIValue[i] := 100
else
RSIValue[i] := 100 - (100 / (1 + (avgG/avGL))); // This is the main calculation after the above processing
if (RSIValue[i-1] > RSIUpper) and (RSIValue[i] < RSIUpper) then RSIRec[i] := -1 // If we cross from above overbought to below, that's a sell signal
else if (RSIValue[i-1] < RSILower) and (RSIValue[i] > RSILower) then RSIRec[i] := 1 // If we cross from below oversold to above, buy signal
else RSIRec[i] := 0; // If neither of those options, no rec (Hold)
end;
end;
Aroon Indicator Usage
There are actually two parts to the Aroon Indicator; the “Aroon Up” and “Aroon Down” indicators combine together to create a single number. The Aroon Up indicator shows the “strength” of an uptrend in the data, while the Aroon Down indicator shows the “strength” of a downtrend in the data. Aroon Up is represented by 0-100, Aroon Down is -100-0, and when the two are added together you get a single value between -100 and 100.
The simplest interpretation for the Aroon Indicator is that when the value is greater than 0, the item is in an uptrend and when it is below 0, the item is in a downtrend. You should buy when in an uptrend, and sell when in a downtrend. Values close to 0 indicate “sideways movement” meaning there is no significant trend in either direction. Below I’ve graphed the 7 day Aroon score over 160 days of Lobster prices. Again, the prices are in blue (left Y Axis), the Aroon Indicator is in orange, and the zero line is in grey (both right Y Axis).
You can see that by using the 7 day parameter, there is a significant lag in the Aroon indicator compared to the 3 and 4 day Moving Average and RSI algorithms. The indicator is slower to pick up changes but has less false positive readings.
Aroon Indicator Math
This link has a very good explanation of how to calculate the Aroon Indicator and how to use it as a buy/sell signal. Below is the code I’ve used to follow this method of calculation. It will calculate the Aroon Indicator value for a set of prices
Simba Code:
procedure doAroon();
var
i, h, maxDay, minDay: Integer;
upDir, downDir, Aroon: Extended;
tempA: TIntegerArray;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
for i := 0 to AroonDays - 2 do
AroonRec[i] := 0;
for i := AroonDays - 1 to h do
begin
tempA := Prices.Slice(i - (AroonDays - 1), i, False); //Reverse so that newest days are 0 and increasing, so high today gives 0 as index (0 days since high)
maxDay := tempA.argMax(); //Gets the index of the maximum price (days since high price) in the slice
minDay := tempA.argMin(); //Gets the index for the low price (days since low price) in the slice
upDir := 100 * (((AroonDays - 1) - MaxDay) / (AroonDays - 1)); // The Aroon Up score
downDir := 100 * (((AroonDays - 1) - MinDay) / (AroonDays - 1)); // The Aroon Down score
AroonValue[i] := upDir - downDir; // Gives the overall Aroon score
if upDir > downDir then // If we are net-positive score (uptrend) then buy
AroonRec[i] := 1
else if upDir < downDir then // If we are net-negative score (downtrend) then sell
AroonRec[i] := -1
else
AroonRec[i] := 0; // If the score is 0 then no rec (hold)
end;
end;
Stochastic Oscillator Usage
The Stochastic Oscillator is two lines on a graph, the %K line and the %D line. The K line is the ‘fast’ line and the D line is the ‘slow’ line. Because the K line responds to price faster than the D line, we can create two main signals from this method. A buy signal is created when the K line goes from below to above the D line, suggesting that the price is starting to move up. A sell signal is created when the K line goes from above the D line to below it, suggesting the price is starting to go down. Some people also add the element of overbought and oversold in this context, and only buy when both the D and K line are below 30 and the K line goes above D. They then only sell when the D and K line are above 70 and the K line goes below D. I do not use this method though.
Suggested interpretation: When K goes from below D to above D, buy. When K goes from above D to below D, sell. The Stochastic Oscillator and the RSI go very well together, and a buy signal from both is to be taken more seriously than other combinations, because they indicate the same things (overbought/oversold) in different ways. Below I’ve graphed the 6 day (3 day %K and 3 day %D) Aroon Indicator for Lobster over 160 days. Prices are in blue (left Y Axis), the %K (grey) and %D (orange) lines are shown and their values are on the right Y Axis. You can see that using 3 day parameters causes many buy/sell signals for the Stochastic Oscillator. It is very responsive but falls victim to many false positive readings. I've indicated where every signal is in black and labelled the few signals which had room.
Stochastic Oscillator Math
This link contains an explanation of the methods of using stochastic oscillators and how to calculate them. Below is the code I use to calculate the “fast” stochastic oscillator (which is graphed above).
Simba Code:
procedure doStoch();
var
i, h : Integer;
minP, maxP: Integer;
d: Extended;
k: TExtendedArray;
tempA : TIntegerArray;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
for i := 0 to (StoKDays + StoDDays) - 3 do // It works out that we can't have recs for this range
StoRec[i] := 0;
for i := 0 to StoKDays - 2 do // There can be no K values for this range
StoKValue[i] := 0;
for i := (StoKDays - 1) to h do // Generate the k values
begin
tempA := Prices.Slice(i - (StoKDays - 1), i); // Generate our temporary range
minP := MinA(tempA); // Gets the lowest value (price)
maxP := MaxA(tempA); // Gets the highest value (price)
if (maxP - minP) <= 0 then // Saves us from divide by 0 errors
StoKValue[i] := 100 // Maximum k is 100
else
StoKValue[i] := 100 * ((tempA[high(tempA)] - minP) / (maxP - minP)); // The actual formula for the k values
end;
for i := (StoKDays + StoDDays - 2) to h do
begin
StoDValue[i] := StoKValue.Slice(i - (stoDDays - 1), i).Mean(); // Takes the moving average of k values
if StoDValue[i] > StoKValue[i] then // If current k value is under moving average (going below the average), sell
StoRec[i] := -1
else if StoDValue[i] < StoKValue[i] then // If current k value is over moving average (going above the average), buy
StoRec[i] := 1
else
StoRec[i] := 0;
end;
end;
Bollinger Band Usage
Bollinger Bands ® (yes they are registered) are a series of three lines; a center (average) line and two “channels/bands”. One band is above the center and one is below. The center line is typically an exponential moving average (although I just use the price) and the price channels show the standard deviation of the stock. The channels will be narrow for very stable stocks and will be very wide for volatile stocks.
There are many uses for Bollinger Bands in the real world stocks, but in RS I’ve used the simplest way to get buy/sell signals. The channels are used as overbought and oversold indicators, which can then be treated the same as most of the above signals I’ve explained. When the price of a stock touches (or goes over) a channel, you prepare for the signal. When it moves away from the channel, coming back towards the average, you have a signal. If it touches the lower band and comes back towards the average, the signal is to buy. If the price touches the upper band and comes back to average, the signal is to sell. Below is a graph showing the 6 day Bollinger Band for Lobster over 160 days.
As usual, the price is in blue (left Y Axis), the upper and lower channels of the Bollinger Bands are orange and green, respectively (also left Y Axis), and the buy/sell signals are shown in grey (right Y Axis). I've indicated the buy and sell signals in black and labelled one pair for clarity.
You can see from this graph that the bands were good at indicating buy/sell signals in the first 30 days, but were generally bad after that. This is why I don't recommend using only one indicator, because sometimes they'll be good and sometimes they'll be bad.
Bollinger Band Math
This link is an excellent description of how to calculate Bollinger Bands in Excel. In a great twist of events, this tutorial specifically warns not to use the Bollinger Bands in the way that I’ve chosen to. I have found that while the method I’ve used is not the most active signal (not very many signals), it serves very well to help verify the rest of the methods.
Below is the code I’ve used to determine the Bollinger Band signals for a given price array.
Simba Code:
procedure doBollinger();
var
i, h : Integer;
tempA: TIntegerArray;
avg, SD, SDMult: Extended;
touchUpper, touchLower: TBoolArray;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
SetLength(touchUpper, h + 1);
SetLength(touchLower, h + 1);
for i := 0 to BollDays - 2 do
begin
BollRec[i] := 0;
BollUpper[i] := 0;
BollLower[i] := 0;
touchUpper[i] := False;
touchLower[i] := False;
end;
for i := BollDays - 1 to h do
begin
tempA := Prices.Slice(i - (BollDays - 1), i); // Grabs our temporary array
avg := tempA.Mean(); // Takes the average
SD := tempA.StdDev(); // Takes the standard deviation
BollUpper[i] := (Avg + (SD * 1.9)); // This calculates the upper band
BollLower[i] := (Avg - (SD * 1.9)); // This calculates the lower band
touchUpper[i] := (Prices[i] >= (BollUpper[i] * (1 - (1 / 100)))); // Are we touching the upper band? Within 1%
touchLower[i] := (Prices[i] <= (BollLower[i] * (1 + (1 / 100)))); // Are we touching the lower band? Within 1%
end;
for i := BollDays - 1 to h do
begin
if touchLower[i-1] and (not touchLower[i]) then // If we were touching lower and aren't anymore, buy signal
BollRec[i] := 1
else if touchUpper[i-1] and (not touchUpper[i]) then // If we were touching upper and aren't anymore, sell signal
BollRec[i] := -1
else
BollRec[i] := (0);
end;
end;
What We Learned
From these methods, you’ve hopefully learned a few important things. First, you can see that the amount of days you use to determine each signal has a huge impact on your results. Having a higher number of days will create less buy/sell signals and can be slower to react to price changes, causing you to potentially miss out on profit, like seen on the Bollinger Band graph. Using too low an amount of days can create false-positives and too many buy/sell signals, which can also cause you to miss profit, like on the Stochastic Oscillator graph. The rule of thumb here is to set the “days” as the amount of days you want to hold an item for, except for with Bollinger Bands when you should use double that amount. I recommend not going less than 3 days for anything, because you will have too many signals to be useful at all.
The other thing you should notice is that while all signals are roughly in the same area, some signals perform better in some areas and some signals perform better in others. This is why I combine signals to determine the best buy and sell times. Let’s see what happens when we combine all signals from the above sections into a single graph. In the graph below, the sum of all signals is shown. Remember that all buy signals were given a value of 1 and all sell signals were given a value of -1, so any value above 0 is a buy and any value below 0 is a sell.
You can see from this graph that there are still many false-positive signals that could be smoothed out. This is likely because I’ve used different values for different algorithms, so some methods are aimed at 3 day trading and others are for 7 day trading. I have not yet figured out a way to determine the best day settings, mainly because there are trillions of combinations you could have, and looping that many values is extremely slow in Simba. Even though the combination is not optimized, we still can tell there is a much higher profitable trade ratio than any of the individual methods.
With all of the learning out of the way, here is the code I’ve used. It will save .csv into the same folder as the script. The .csv can be opened in Excel and will contain all of the information I’ve used to create the graphs in this section. Thanks for reading, and best of luck! If you have any questions, comments, etc. please post and let me know.
Simba Code:
program priceToExcel;
{$i srl-6/srl.simba}
const
theItem = 'Lobster'; // What item you want to grab prices for
amountOfPrices = 160; //160 is the max
MADays = 3;
RSIDays = 4;
StoKDays = 3;
StoDDays = 3;
AroonDays = 7;
BollDays = 6;
RSIUpper = 70;
RSILower = 30;
var
scriptStartTime : String;
Prices : TIntegerArray;
MAValue, RSIValue, StoKValue, StoDValue, AroonValue, BollUpper, BollLower : TExtendedArray;
MARec, RSIRec, StoRec, AroonRec, BollRec, TotalRec : TIntegerArray;
{----------------------------------------------------------------)
(function tIntegerArray.Slice )
(Returns a slice of the TIA between given indices (inclusive) )
(----------------------------------------------------------------}
function TIntegerArray.Slice(indexFrom, indexTo : Integer; Forwards : Boolean = True) : TIntegerArray;
var
i, x : Integer;
begin
if (high(self)) < indexTo then
begin
Writeln('TIntegerArray.Slice: Array high is ' + toStr(high(self)) + ', index ' + toStr(indexTo) + ' cannot be reached.');
Exit();
end;
Result := Copy(Self, indexFrom, (indexTo-indexFrom)+1);
if not Forwards then
result.invert();
end;
{----------------------------------------------------------------)
(function tExtendedArray.Slice )
(Returns a slice of the TIA between given indices (inclusive) )
(----------------------------------------------------------------}
function TExtendedArray.Slice(indexFrom, indexTo: Integer; Forwards : Boolean = True) : TExtendedArray;
var
i, x : Integer;
begin
if (length(self) - 1) < indexTo then
begin
Writeln('TIntegerArray.Slice: Array high is ' + toStr(high(self)) + ', index ' + toStr(indexTo) + ' cannot be reached.');
Exit();
end;
Result := Copy(Self, indexFrom, (indexTo-indexFrom)+1);
if not Forwards then
result.invert()
end;
{----------------------------------------------------------------)
(function tIntegerArray.Mean )
(Returns the mean of the given TIA )
(----------------------------------------------------------------}
function TIntegerArray.Mean() : Extended;
var
i, h, sum : Integer;
begin
h := high(Self);
for i := 0 to h do
sum += Self[i];
Result := sum/length(Self);
end;
{----------------------------------------------------------------)
(function tExtendedArray.Mean )
(Returns the mean of the given TEA )
(----------------------------------------------------------------}
function TExtendedArray.Mean() : Extended;
var
i, h : Integer;
sum : Extended;
begin
h := high(Self);
for i := 0 to h do
sum += Self[i];
Result := sum/length(Self);
end;
{----------------------------------------------------------------)
(function tIntegerArray.argMax )
(Returns the index which contains the highest value )
(----------------------------------------------------------------}
function TIntegerArray.argMax() : Integer;
var
i, h, tempInt : Integer;
begin
h := high(Self);
tempInt := self[0];
Result := 0;
for i := 0 to h do
if self[i] > tempInt then
begin
Result := i;
tempInt := Self[i];
end;
end;
{----------------------------------------------------------------)
(function tIntegerArray.argMin )
(Returns the index which contains the lowest value )
(----------------------------------------------------------------}
function TIntegerArray.argMin() : Integer;
var
i, h, tempInt : Integer;
begin
h := high(Self);
tempInt := self[0];
Result := 0;
for i := 0 to h do
if self[i] < tempInt then
begin
Result := i;
tempInt := Self[i];
end;
end;
{-------------------END TYPE FUNCTION SECTION--------------------)
(ALL CODE WRITTEN BELOW IS BY GARRETT UNLESS OTHERWISE INDICATED )
(----------------------------------------------------------------}
{----------------------------------------------------------------)
(TIntegerArray.StdDev() - Returns the Std Deviation of an array )
(Credit: SuperUser, Modified by Slacky )
(----------------------------------------------------------------}
function TIntegerArray.StdDev(): extended;
var
avg, total: extended;
i: integer;
begin
for i := 0 to High(self) do
total := total + self[i];
avg := total / Length(self);
total := 0;
for i := 0 to High(self) do
total := total + Sqr(self[i] - avg);
result := Sqrt(total / Length(self));
end;
procedure TBoolArray.Append(value:Boolean);
begin
Self := Self + value;
end;
procedure RS_GetPricesOf(Item : String; numSamples: Int32 = 100);
var
i : Integer;
ID, Page, fullPage, RSpage, str, link: string;
items: TStringArray;
begin
item := Replace(item, ' ', '_', [rfReplaceAll]);
fullPage := GetPage('http://runescape.wikia.com/wiki/Exchange:' + Item);
ID := extractFromStr(between('"GEDBID">', '</span>', fullPage), Numbers);
link := 'http://services.runescape.com/m=itemdb_rs/'+ Item + '/viewitem.ws?obj=' + ID;
rsPage := getPage(link);
Items := MultiBetween(RSPage, 'average180.daily.push(',');');
if numSamples > Length(items) then
WriteLn(Format('[Hint] NumSamples overflow: Could only gather %d samples', [High(items)]));
for i := Max(High(items) - numSamples, 0) to High(items) do
//Prices[i] := (strToInt(items[i]));
Prices.append(strToInt(items[i]));
end;
{-----------------------------------------------------------------)
(function doMA )
(Determines the Moving Average Score )
(-----------------------------------------------------------------}
procedure doMA();
var
i, h: Integer;
avg, comp: Extended;
tempA: TIntegerArray;
begin
for i := 0 to MADays - 2 do
MARec[i] := 0; // You need at least 2 days to get data
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
for i := MADays - 1 to h do //For the rest of the prices, figure out the MA
begin
tempA := Prices.slice(i - (MADays - 1), i); // This creates a temporary array from a slice of the price array
avg := TempA.Mean(); // Grab the average of the values in the temporary slice
comp := avg - Prices[i]; // The comparison here is between the average and the current day's price
MAValue[i] := avg;
if comp > 0 then // If the average is higher than today's prices (we're in a down trend), so rec is -1 (Sell)
MARec[i] := -1
else if comp = 0 then // If the average is the same as today's prices there is no new recommendation (Hold)
MARec[i] := 0
else
MARec[i] := 1; // If the average is below today's prices (we're in an uptrend), the rec is 1 (Buy)
end;
end;
{-----------------------------------------------------------------)
(function doRSI )
(Determines the Relative Strength Index (RSI) Score )
(-----------------------------------------------------------------}
procedure doRSI();
var
i, h, change : Integer;
Gains, Losses, tempG, tempL : TIntegerArray;
avgG, avgL, RSI: Extended;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
SetLength(Gains, h+1);
SetLength(Losses, h+1);
Gains[0] := 0; // The first day you can't take a comparison of gain or loss, set both to 0
Losses[0] := 0;
for i := 1 to h do // You can do comparision from day 1 (second day) to the last day, do comparisons
begin
Change := (Prices[i] - Prices[i-1]); // The change from Day[i-1] to Day[i]
if Change > 0 then Gains[i] := Change else Gains[i] := 0; //If change is positive, add it to gains (else gains is 0)
if Change < 0 then Losses[i] := (0-Change) else Losses[i] := 0; //If change is negative, add it to losses (else losses is 0)
end;
for i := 0 to RSIDays - 2 do // This is where we start assigning scores
RSIRec[i] := 0; // Can't have scores for the first (RSIDays - 2) Days (not enough data points)
for i := RSIDays - 1 to h do
begin
tempG := Gains.Slice(i - (RSIDays - 1) , i); // The temporary gains and losses arrays are generated for processing
tempL := Losses.Slice(i - (RSIDays - 1), i);
avgG := tempG.Mean(); //Take the average of each slice
avgL := tempL.Mean();
if avgL = 0 then // This saves us from dividing by 0, the RSI Score is set to 100
RSIValue[i] := 100
else
RSIValue[i] := 100 - (100 / (1 + (avgG/avGL))); // This is the main calculation after the above processing
if (RSIValue[i-1] > RSIUpper) and (RSIValue[i] < RSIUpper) then RSIRec[i] := -1 // If we cross from above overbought to below, that's a sell signal
else if (RSIValue[i-1] < RSILower) and (RSIValue[i] > RSILower) then RSIRec[i] := 1 // If we cross from below oversold to above, buy signal
else RSIRec[i] := 0; // If neither of those options, no rec (Hold)
end;
end;
{-----------------------------------------------------------------)
(function doStoch )
(Uses Stochastic Oscillator to determine if Over or Under Bought )
(-----------------------------------------------------------------}
procedure doStoch();
var
i, h : Integer;
minP, maxP: Integer;
d: Extended;
k: TExtendedArray;
tempA : TIntegerArray;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
for i := 0 to (StoKDays + StoDDays) - 3 do // It works out that we can't have recs for this range
StoRec[i] := 0;
for i := 0 to StoKDays - 2 do // There can be no K values for this range
StoKValue[i] := 0;
for i := (StoKDays - 1) to h do // Generate the k values
begin
tempA := Prices.Slice(i - (StoKDays - 1), i); // Generate our temporary range
minP := MinA(tempA); // Gets the lowest value (price)
maxP := MaxA(tempA); // Gets the highest value (price)
if (maxP - minP) <= 0 then // Saves us from divide by 0 errors
StoKValue[i] := 100 // Maximum k is 100
else
StoKValue[i] := 100 * ((tempA[high(tempA)] - minP) / (maxP - minP)); // The actual formula for the k values
end;
for i := (StoKDays + StoDDays - 2) to h do
begin
StoDValue[i] := StoKValue.Slice(i - (stoDDays - 1), i).Mean(); // Takes the moving average of k values
if StoDValue[i] > StoKValue[i] then // If current k value is under moving average (going below the average), sell
StoRec[i] := -1
else if StoDValue[i] < StoKValue[i] then // If current k value is over moving average (going above the average), buy
StoRec[i] := 1
else
StoRec[i] := 0;
end;
end;
{-----------------------------------------------------------------)
(function doAroon )
(Determines the Aroon Indicator Score )
(-----------------------------------------------------------------}
procedure doAroon();
var
i, h, maxDay, minDay: Integer;
upDir, downDir, Aroon: Extended;
tempA: TIntegerArray;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
for i := 0 to AroonDays - 2 do
AroonRec[i] := 0;
for i := AroonDays - 1 to h do
begin
tempA := Prices.Slice(i - (AroonDays - 1), i, False); //Reverse so that newest days are 0 and increasing, so high today gives 0 as index (0 days since high)
maxDay := tempA.argMax(); //Gets the index of the maximum price (days since high price) in the slice
minDay := tempA.argMin(); //Gets the index for the low price (days since low price) in the slice
upDir := 100 * (((AroonDays - 1) - MaxDay) / (AroonDays - 1)); // The Aroon Up score
downDir := 100 * (((AroonDays - 1) - MinDay) / (AroonDays - 1)); // The Aroon Down score
AroonValue[i] := upDir - downDir; // Gives the overall Aroon score
if upDir > downDir then // If we are net-positive score (uptrend) then buy
AroonRec[i] := 1
else if upDir < downDir then // If we are net-negative score (downtrend) then sell
AroonRec[i] := -1
else
AroonRec[i] := 0; // If the score is 0 then no rec (hold)
end;
end;
{-----------------------------------------------------------------)
(function doBollinger )
(Defines the Bollinger Bands and predicts trends )
(-----------------------------------------------------------------}
procedure doBollinger();
var
i, h : Integer;
tempA: TIntegerArray;
avg, SD, SDMult: Extended;
touchUpper, touchLower: TBoolArray;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
SetLength(touchUpper, h + 1);
SetLength(touchLower, h + 1);
for i := 0 to BollDays - 2 do
begin
BollRec[i] := 0;
BollUpper[i] := 0;
BollLower[i] := 0;
touchUpper[i] := False;
touchLower[i] := False;
end;
for i := BollDays - 1 to h do
begin
tempA := Prices.Slice(i - (BollDays - 1), i); // Grabs our temporary array
avg := tempA.Mean(); // Takes the average
SD := tempA.StdDev(); // Takes the standard deviation
BollUpper[i] := (Avg + (SD * 1.9)); // This calculates the upper band
BollLower[i] := (Avg - (SD * 1.9)); // This calculates the lower band
touchUpper[i] := (Prices[i] >= (BollUpper[i] * (1 - (1 / 100)))); // Are we touching the upper band? Within 1%
touchLower[i] := (Prices[i] <= (BollLower[i] * (1 + (1 / 100)))); // Are we touching the lower band? Within 1%
end;
for i := BollDays - 1 to h do
begin
if touchLower[i-1] and (not touchLower[i]) then // If we were touching lower and aren't anymore, buy signal
BollRec[i] := 1
else if touchUpper[i-1] and (not touchUpper[i]) then // If we were touching upper and aren't anymore, sell signal
BollRec[i] := -1
else
BollRec[i] := (0);
end;
end;
{-----------------------------------------------------------------)
(function totalScore )
(Totals the score from the above 5 algorithms )
(-----------------------------------------------------------------}
procedure TotalScore();
var
i, h : Integer;
begin
h := high(Prices); // Defined when we grab prices - we want the same amount of calculations as there are prices
for i := 0 to h do
TotalRec[i] := (MARec[i] + RSIRec[i] + StoRec[i] + AroonRec[i] + BollRec[i]);
end;
procedure Setup();
begin
SetLength(MAValue, AmountofPrices + 1);
SetLength(RSIValue, AmountofPrices + 1);
SetLength(StoKValue, AmountofPrices + 1);
SetLength(StoDValue, AmountofPrices + 1);
SetLength(AroonValue, AmountofPrices + 1);
SetLength(BollUpper, AmountofPrices + 1);
SetLength(BollLower, AmountofPrices + 1);
SetLength(MARec, AmountofPrices + 1);
SetLength(RSIRec, AmountofPrices + 1);
SetLength(StoRec, AmountofPrices + 1);
SetLength(AroonRec, AmountofPrices + 1);
SetLength(BollRec, AmountofPrices + 1);
SetLength(TotalRec, AmountofPrices + 1);
end;
procedure startTime;
var
Hour, Mins, Sec, MSec: Word;
Year, Month, Day : UInt16;
suffix : String;
begin
DecodeTime(Now, Hour, Mins, Sec, MSec);
DecodeDate(Now, Year, Month, Day);
if Hour < 12 then Suffix := 'AM' else Suffix := 'PM'
if Hour > 12 then Hour -= 12;
scriptStartTime := (' ' + toStr(Day) + '-' + toStr(Month) + '-' + toStr(Year) + ' at ' + Padz(IntToStr(Hour), 2) + Padz(IntToStr(Mins), 2) + Suffix);
end;
procedure WritetoFile();
var
path, str, txt: String;
theFile, i, j : Integer;
begin
try
path := ScriptPath + theItem + ' ';
theFile := RewriteFile(path + scriptStartTime + '.csv', False);
txt := 'Day:,';
for i := 0 to high(Prices)-1 do
txt := txt + toStr(i) + ',';
txt := txt + toStr(high(Prices));
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + theItem + ','; // Writes Prices
for i := 0 to high(Prices)-1 do
txt := txt + toStr(Prices[i]) + ',';
txt := txt + toStr(Prices[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'MAValue,';
for i := 0 to high(MAValue)-1 do
txt := txt + toStr(MAValue[i]) + ',';
txt := txt + toStr(MAValue[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'MARec,';
for i := 0 to high(MARec)-1 do
txt := txt + toStr(MARec[i]) + ',';
txt := txt + toStr(MARec[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'RSIValue,';
for i := 0 to high(RSIValue)-1 do
txt := txt + toStr(RSIValue[i]) + ',';
txt := txt + toStr(RSIValue[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'RSIRec,';
for i := 0 to high(RSIRec)-1 do
txt := txt + toStr(RSIRec[i]) + ',';
txt := txt + toStr(RSIRec[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'StoKValue,';
for i := 0 to high(StoKValue)-1 do
txt := txt + toStr(StoKValue[i]) + ',';
txt := txt + toStr(StoKValue[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'StoDValue,';
for i := 0 to high(StoDValue)-1 do
txt := txt + toStr(StoDValue[i]) + ',';
txt := txt + toStr(StoDValue[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'StoRec,';
for i := 0 to high(StoRec)-1 do
txt := txt + toStr(StoRec[i]) + ',';
txt := txt + toStr(StoRec[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'AroonValue,';
for i := 0 to high(AroonValue)-1 do
txt := txt + toStr(AroonValue[i]) + ',';
txt := txt + toStr(AroonValue[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'AroonRec,';
for i := 0 to high(AroonRec)-1 do
txt := txt + toStr(AroonRec[i]) + ',';
txt := txt + toStr(AroonRec[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'BollUpper,';
for i := 0 to high(BollUpper)-1 do
txt := txt + toStr(BollUpper[i]) + ',';
txt := txt + toStr(BollUpper[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'BollLower,';
for i := 0 to high(BollLower)-1 do
txt := txt + toStr(BollLower[i]) + ',';
txt := txt + toStr(BollLower[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'BollRec,';
for i := 0 to high(BollRec)-1 do
txt := txt + toStr(BollRec[i]) + ',';
txt := txt + toStr(BollRec[high(Prices)]);
txt := txt + [URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 3[URL=https://villavu.com/forum/usertag.php?do=list&action=hash&hash=1]#1[/URL] 0 + 'TotalRec,';
for i := 0 to high(TotalRec)-1 do
txt := txt + toStr(TotalRec[i]) + ',';
txt := txt + toStr(TotalRec[high(Prices)]);
WriteFileString(theFile, Txt);
CloseFile(theFile);
except
Writeln('Debug saving - error occurred');
end;
end;
begin
startTime();
Setup();
RS_GetPricesOf(theItem, amountofPrices);
doMA();
doRSI();
doStoch();
doAroon();
doBollinger();
totalScore();
writetoFile();
end.