zygimantus
4/6/2016 - 4:19 PM

timeseries-analysis.js

var 			_ = require("underscore");
var gimage 		= require('google-image-chart').charts;

var timeseries = function(data, options) {
	/*
		Data Format:
		[
			[Date Object, value],
			[Date Object, value]
		]
	*/
	this.options 	= _.extend({
		
	}, options);
	
	this.data 		= data;
	this.original	= data.slice(0);
	this.buffer 	= [];
	this.saved 		= [];
	
	return this;
}


// Output the data
timeseries.prototype.output = function() {
	return this.data;
}


// Save the data
timeseries.prototype.save = function(name, options) {
	options = _.extend({
		color:	'AUTO'
	}, options);
	
	this.saved.push({
		name:	name,
		color:	options.color,
		data:	this.data.slice(0)
	});
	return this;
}

// Chart the data
timeseries.prototype.chart = function(options) {
	
	options = _.extend({
		main:		false,
		width:		800,
		height:		200,
		bands:		[],
		lines:		[],
		points:		[]
	}, options);
	
	// Google Chart
	var chart = new gimage.line({
		width:	options.width,
		height:	options.height,
		bands:	options.bands,
		hlines:	options.lines,
		points:	options.points,
		autoscale:	true
	});
	chart.fromTimeseries(this.data);
	// Include the original data
	if (options.main) {
		chart.fromTimeseries(this.original);
	}
	
	// Include saved data
	_.each(this.saved, function(saved) {
		chart.fromTimeseries(saved.data);
	});
	
	return chart.render();
}


// Basic utilities: Array fill, data cloning...
// Returns an array filled with the specified value.
timeseries.prototype.fill = function(value, n) {
	var array = [];
	var i;
	for (i=0;i<n;i++) {
		array.push(value);
	}
	return array;
}

// Returns a clone of the data
timeseries.prototype.clone = function() {
	var buffer = _.map(this.data, function(point) {
		return [
			point[0],
			point[1]*1
		];
	});
	return buffer;
}

// Reset the data to its original dataset
timeseries.prototype.reset = function() {
	this.data = this.original;
	return this;
}

// Convert the data to a 1D array
timeseries.prototype.toArray = function() {
	return _.map(this.data, function(datapoint) {
		return  datapoint[1];
	});
}

// Stats: Min, Max, Mean, Stdev
timeseries.prototype.min = function() {
	var array = this.toArray();
	return _.min(array);
}
timeseries.prototype.max = function() {
	var array = this.toArray();
	return _.max(array);
}
timeseries.prototype.mean = function(data) {
	if (!data) {
		var data = this.data;
	}
	var sum 	= 0;
	var n 		= 0;
	_.each(data, function(datapoint) {
		sum += datapoint[1];
		n++;
	});
	return sum/n;
}
timeseries.prototype.stdev = function(data) {
	if (!data) {
		var data = this.data;
	}
	var sum 	= 0;
	var n 		= 0;
	var mean 	= this.mean();
	_.each(data, function(datapoint) {
		sum += (datapoint[1]-mean)*(datapoint[1]-mean);
		n++;
	});
	return Math.sqrt(sum/n);
}


// Offet the data
timeseries.prototype.offset = function(value, data, ret) {
	if (!data) {
		var data = this.data;
	}
	var i;
	var j;
	var l 	= data.length;
	var sum	= 0;
	
	// Reset the buffer
	this.buffer 	= data.slice(0);
	
	for (i=0;i<l;i++) {
		this.buffer[i] = [
			this.buffer[i][0],
			this.buffer[i][1]+value
		];
	}
	if (!ret) {
		this.data = this.buffer;
		return this;
	} else {
		return this.buffer;
	}
}





// Moving Average
timeseries.prototype.ma = function(options) {
	options = _.extend({
		period:		12
	}, options);
	var i;
	var j;
	var l 	= this.data.length;
	var sum	= 0;
	
	// Reset the buffer
	this.buffer 	= [];
	
	// Leave the datapoints [0;period[ intact
	this.buffer = this.data.slice(0, options.period);
	
	for (i=options.period;i<l;i++) {
		sum	= 0;
		for (j=options.period;j>0;j--) {
			sum += this.data[i-j][1];
		}
		this.buffer[i] = [this.data[i][0], sum/options.period];
	}
	this.data = this.buffer;
	return this;
}
timeseries.prototype.ema = function(options) {
	options = _.extend({
		period:		12
	}, options);
	var i;
	var j;
	var l 	= this.data.length;
	var sum	= 0;
	
	// Reset the buffer
	this.buffer 	= [];
	
	// Leave the datapoints [0;period[ intact
	this.buffer = this.data.slice(0, options.period);
	
	var m	= 2/(options.period+1);	// Multiplier
	
	for (i=options.period;i<l;i++) {
		this.buffer[i] = [
			this.data[i][0],
			(this.data[i][1]-this.data[i-1][1])*m+this.data[i-1][1]
		];
	}
	this.data = this.buffer;
	return this;
}
timeseries.prototype.lwma = function(options) {
	options = _.extend({
		period:		12
	}, options);
	var i;
	var j;
	var l 	= this.data.length;
	var sum	= 0;
	var n	= 0;
	
	// Reset the buffer
	this.buffer 	= [];
	
	// Leave the datapoints [0;period[ intact
	this.buffer = this.data.slice(0, options.period);
	
	for (i=options.period;i<l;i++) {
		sum	= 0;
		n	= 0;
		for (j=options.period;j>0;j--) {
			sum += this.data[i-j][1]*j;
			n += j;
		}
		this.buffer[i] = [this.data[i][0], sum/n];
	}
	this.data = this.buffer;
	return this;
}



// DSL, iTrend
timeseries.prototype.dsp_itrend = function(options) {
	// By Ehler
	// http://www.davenewberg.com/Trading/TS_Code/Ehlers_Indicators/iTrend_Ind.html
	options = _.extend({
		alpha:		0.7,
		use:		'main'
	}, options);
	var i;
	var j;
	var l 	= this.data.length;
	
	var trigger 	= [];
	
	// Reset the buffer
	this.buffer 	= [];
	
	// Leave the datapoints [0;period[ intact
	this.buffer 	= this.data.slice(0, 3);
	this.trigger 	= this.data.slice(0, 3);
	
	for (i=3;i<l;i++) {
		this.buffer[i] = [
			this.data[i][0],
			(options.alpha-(options.alpha*options.alpha)/4)*this.data[i][1] + (0.5*(options.alpha*options.alpha)*this.data[i-1][1]) - (options.alpha - 0.75*(options.alpha*options.alpha)) * this.data[i-2][1] + 2*(1-options.alpha)*this.buffer[i-1][1] - (1-options.alpha)*(1-options.alpha)*this.buffer[i-2][1]
		];
		this.trigger[i] = [
			this.data[i][0],
			2*this.buffer[i][1]-this.buffer[i-2][1]
		]
	}
	if (options.use == 'trigger') {
		this.data = this.trigger;
	} else{
		this.data = this.buffer;
	}
	
	return this;
}


// Pixelize - Domain reduction
timeseries.prototype.pixelize = function(options) {
	options = _.extend({
		grid:		20
	}, options);
	
	// Calculate the grid values
	var min 	= this.min();
	var max 	= this.max();
	var tile	= (max-min)/options.grid;
	
	this.buffer	= _.map(this.data, function(datapoint) {
		datapoint[1] = Math.round(datapoint[1]/tile)*tile;
		return datapoint;
	});
	this.data = this.buffer;
	return this;
}


// Iterative Noise Removal
timeseries.prototype.smoother = function(options) {
	options = _.extend({
		period:		1
	}, options);
	var i;
	var j;
	var l 	= this.data.length;
	var sum	= 0;
	
	// Reset the buffer
	this.buffer 	= this.data.slice(0);
	
	for (j=0;j<options.period;j++) {
		for (i=3;i<l;i++) {
			this.buffer[i-1] = [
				this.buffer[i-1][0],
				(this.buffer[i-2][1]+this.buffer[i][1])/2
			];
		}
	}
	this.data = this.buffer;
	return this;
}


// Extract the noise out of the data
timeseries.prototype.noiseData = function() {
	var i;
	var j;
	var l 	= this.data.length;
	var sum	= 0;
	
	// Reset the buffer
	this.buffer 	= [];
	
	for (i=0;i<l;i++) {
		this.buffer[i] = [
			this.data[i][0],
			this.original[i][1]-this.data[i][1]
		];
	}
	this.data = this.buffer;
	return this;
}


// Oscillator function
timeseries.prototype.osc = function() {
	var i;
	var j;
	var l 	= this.data.length;
	var sum	= 0;
	
	// Reset the buffer
	this.buffer 	= [];
	
	for (i=0;i<l;i++) {
		if (i<=1) {
			this.buffer[i] = [
				this.data[i][0],
				0
			];
		} else {
			this.buffer[i] = [
				this.data[i][0],
				this.data[i][1]-this.data[i-1][1]
			];
		}
	}
	this.data = this.buffer;
	return this;
}



// Find the supports and resistances. Wrong algorithm.
timeseries.prototype.supports = function(options) {
	options = _.extend({
		grid:		40,
		threshold:	10
	}, options);
	
	// Calculate the grid values
	var min 	= this.min();
	var max 	= this.max();
	var tile	= (max-min)/options.grid;
	
	var prices = {
		
	};
	
	_.each(this.data, function(datapoint) {
		var val = Math.round(datapoint[1]/tile)*tile;
		if (!prices[val]) {
			prices[val] = 0;
		}
		prices[val]++;
	});
	
	var ordered = [];
	var i;
	for (i in prices) {
		ordered.push({
			price:	i,
			count:	prices[i]
		});
	}
	ordered = ordered.sort(function(a,b) {
		return b.count-a.count;
	});
	ordered	= _.filter(ordered, function(support) {
		return support.count >= options.threshold;
	});
	if (options.stats) {
		return 	ordered;
	}
	
	return _.map(ordered, function(support) {
		return support.price;
	});
}


// Standardize the data
timeseries.prototype.standardize = function(options) {
	options = _.extend({}, options);
	
	var stdev	= this.stdev();
	var mean	= this.mean();
	
	this.data = _.map(this.data, function(datapoint) {
		datapoint[1] = (datapoint[1]-mean)/stdev;
		return datapoint;
	});
	
	return this;
}


// Slice the data
timeseries.prototype.slice = function(from, to) {
	if (!from) {
		from = 0;
	}
	if (!to) {
		to = this.data.length-1;
	}
	
	this.data = this.data.splice(from, to)
	
	return this;
}


// Find the cycle in the data
timeseries.prototype.cycle = function(options) {
	options = _.extend({
		period:		10,
		forecast:	false,
		forecast_length:	20
	}, options);
	
	// Smooth the data
	this.smoother(options);
	
	
	
	// Copy the data
	var buffer 				= [];
	var buffer_forecast 	= [];
	
	var i;
	var j;
	var l = this.data.length;
	for (i=0;i<2;i++) {
		buffer[i] = ([
			this.data[i][0],
			this.data[i][1]
		]);
		buffer_forecast[i] = ([
			this.data[i][0],
			this.data[i][1]
		]);
	}
	for (i=2;i<l;i++) {
		// We find the ratio
		var d1 		= this.data[i][1]-this.data[i-1][1];
		var d2 		= this.data[i][1]-this.data[i-2][1];
		var ratio	= d1/d2;
		console.log("ratio",ratio, d1, d2);
		buffer[i] = ([
			this.data[i][0],
			this.data[i][1]
		]);
		
		buffer_forecast[i] = ([
			this.data[i][0],
			this.data[i][1],
			ratio,
			d1>0,
			d2>0
		]);
		
	}
	
	if (options.forecast) {
		for (i=2;i<l;i++) {
			if (options.forecast == i) {
				
				// Generate a two cycles sin wave
				var sin = [];
				for (j=0;j<720;j++) {
					sin.push(Math.sin(j*Math.PI/180));
				}
				console.log("sin",sin);
				
				// Find the closest sin wave
				var MSE = [];
				var minMSE	= 10000000;
				var pos;
				for (j=2;j<720;j++) {
					var d1 		= sin[j]-sin[j-1];
					var d2 		= sin[j]-sin[j-2];
					var ratio	= d1/d2;
					var mse		= (ratio-buffer_forecast[i][2])*(ratio-buffer_forecast[i][2]);
					if (mse <= minMSE && ((d1>0)==buffer_forecast[i][3]) && ((d2>0)==buffer_forecast[i][3])) {
						minMSE 	= mse;
						pos		= j;
					}
				}
				console.log("minMSE",minMSE, pos);
				
				for (j=0;j<=options.forecast_length;j++) {
					buffer_forecast[i+j][1] = Math.sin((pos+j)*Math.PI/180);
					
					//buffer_forecast[i+j][1] = sin[pos+j];
					
					console.log("buffer_forecast["+(i+j)+"]", pos+j, buffer_forecast[i+j][1]);
				}
				
				break;
			}
		}
		this.data = buffer_forecast;
	} else {
		this.data = buffer;
	}
	
	return this;
}


// Get the outliers from the dataset
timeseries.prototype.outliers = function(options) {
	// Original code by Professor Hossein Arsham - http://home.ubalt.edu/ntsbarsh/Business-stat/otherapplets/Outlier.htm
	// Re-written for timeseries-analysis.
	
	options = _.extend({
		threshold:	2.5
	}, options);
	
	
	// Create a copy of the data;
	this.buffer 	= this.data.slice(0);
	
	// standardize the data
	this.standardize();
	
	var outliers = [];
	
	_.each(this.data, function(datapoint) {
		if (Math.abs(datapoint[1]) > options.threshold) {
			outliers.push(datapoint);
		}
	});
	
	// restore the data
	this.data = this.buffer.slice(0);
	delete this.buffer;
	
	return outliers;
}


/* EXPERIMENTAL - AutoRegression Analysis */

timeseries.prototype.regression_forecast = function(options) {
	options = _.extend({
		method:		'ARMaxEntropy',	// ARMaxEntropy | ARLeastSquare
		sample:		50,		// points int he sample
		start:		100,	// Where to start
		n:			5,		// How many points to forecast
		degree:		5
	},options);
	
	var i;
	var j;
	var l = this.data.length;
	
	var mean	= this.mean();
	this.offset(-mean);
	var backup 	= this.clone();
	var buffer 	= this.clone();
	
	var sample 		= buffer.slice(options.start-1-options.sample, options.start);
	
	// The current data to process is only a sample of the real data.
	this.data		= sample;
	// Get the AR coeffs
	var coeffs 		= this[options.method]({degree: options.degree});
	console.log("coeffs",coeffs);
	
	for (i=options.start;i<options.start+options.n;i++) {
		buffer[i][1]	= 0;
		for (j=0;j<coeffs.length;j++) {
			if (options.method == 'ARMaxEntropy') {
				buffer[i][1] -= buffer[i-1-j][1]*coeffs[j];
			} else {
				buffer[i][1] += buffer[i-1-j][1]*coeffs[j];
			}
		}
		console.log("buffer["+i+"][1]",buffer[i][1]);
	}
	this.data = buffer;
	this.offset(mean);
	
	return this;
}

timeseries.prototype.regression_forecast_optimize = function(options) {
	options = _.extend({
		data:		this.data,
		maxPct:		0.2,
		maxSampleSize:	false
	},options);
	
	var l 				= options.data.length;
	
	var maxSampleSize	= Math.round(l*options.maxPct);
	if (options.maxSampleSize) {
		maxSampleSize = Math.min(maxSampleSize, options.maxSampleSize);
	}
	
	var maxDegree		= Math.round(maxSampleSize);
	var methods			= ['ARMaxEntropy', 'ARLeastSquare'];
	var ss;		// sample size
	var deg;	// degree
	var MSEData = [];
	var i;
	for (i=0;i<methods.length;i++) {
		for (ss=3;ss<=maxSampleSize;ss++) {
			for (deg=1;deg<=maxDegree;deg++) {
				if (deg<=ss) {
					var mse = this.regression_forecast_mse({
						method:	methods[i],
						sample:	ss,
						degree:	deg,
						data:	options.data
					});
					console.log("Trying method("+methods[i]+") degree("+deg+") sample("+ss+")\t"+mse);
					if (!isNaN(mse)) {
						MSEData.push({
							MSE:	mse,
							method:	methods[i],
							degree:	deg,
							sample:	ss
						});
					}
				} else {
					break;
				}
			}
		}
	}
	
	// Now we sort by MSE
	MSEData = MSEData.sort(function(a,b) {
		return a.MSE>b.MSE;
	});
	
	console.log("Best Settings: ",MSEData[0]);
	
	// Return the best settings
	return MSEData[0];
	
}
// Calculate the MSE for a forecast, for a set of parameters
timeseries.prototype.regression_forecast_mse = function(options) {
	options = _.extend({
		method:		'ARMaxEntropy',	// ARMaxEntropy | ARLeastSquare
		sample:		50,
		degree:		5,
		data:		this.data
	},options);
	
	
	var i;
	var j;
	var l 			= options.data.length;
	
	var mean		= this.mean(options.data);
	options.data 	= this.offset(-mean, options.data, true);
	
	var backup 		= _.map(options.data, function(item) {
		return [
			item[0],
			item[1]*1
		];
	});
	var buffer 		= _.map(options.data, function(item) {
		return [
			item[0],
			item[1]*1
		];
	});
	
	var MSE	= 0;
	var n = 0;
	for (i=options.sample;i<l-1;i++) {
		var sample 		= buffer.slice(i-options.sample, i);
		// Get the AR coeffs
		var coeffs 		= this[options.method]({degree:options.degree, data:sample});
		var knownValue 	= buffer[i+1][1]*1;
		buffer[i+1][1]	= 0;
		for (j=0;j<coeffs.length;j++) {
			if (options.method == 'ARMaxEntropy') {
				buffer[i+1][1] -= backup[i-j][1]*coeffs[j];
			} else {
				buffer[i+1][1] += backup[i-j][1]*coeffs[j];
			}
		}
		
		MSE += (knownValue-buffer[i+1][1])*(knownValue-buffer[i+1][1]);
		n++;
	}
	
	MSE /= n;
	
	
	//this.data = buffer;
	
	// Put back the mean
	//this.offset(mean);
	
	return MSE;
}
timeseries.prototype.sliding_regression_forecast = function(options) {
	options = _.extend({
		method:		'ARMaxEntropy',	// ARMaxEntropy | ARLeastSquare
		sample:		50,
		degree:		5
	},options);
	
	var i;
	var j;
	var l = this.data.length;
	
	var mean	= this.mean();
	this.offset(-mean);
	var backup 	= this.clone();
	var buffer 	= this.clone();
	
	for (i=options.sample;i<l-1;i++) {
		var sample 		= buffer.slice(i-options.sample, i);
		// The current data to process is only a sample of the real data.
		this.data		= sample;
		// Get the AR coeffs
		var coeffs 		= this[options.method]({degree:options.degree});
		buffer[i+1][1]	= 0; //backup[i][1]*1;
		for (j=0;j<coeffs.length;j++) {
			if (options.method == 'ARMaxEntropy') {
				buffer[i+1][1] -= backup[i-j][1]*coeffs[j];
			} else {
				buffer[i+1][1] += backup[i-j][1]*coeffs[j];
			}
		}
		//buffer[i+1][1] -
	}
	
	this.data = buffer;
	
	// Put back the mean
	this.offset(mean);
	
	return this;
}



// Autoregression method: MaxEntropy
timeseries.prototype.ARMaxEntropy = function(options) {
	// Credits to Alex Sergejew, Nick Hawthorn, Rainer Hegger (1998)
	// Zero-Indexed arrays modification by Paul Sanders (the arrays were One-indexed, FORTRAN style)
	// Ported to Javascript by Julien Loutre for timeseries-analysis, from Paul Bourke's C code.
	
	options = _.extend({
		degree:			5,
		data:			this.data,
		intermediates:	false	// Generates and returns the intermediates, a 2D array, instead of the coefficients.
	}, options);
	
	var scope	= this;
	var i;
	var length 	= options.data.length;
	var pef 	= this.fill(0, length);
	var per 	= this.fill(0, length);
	var ar 		= this.fill([], options.degree+1);
	ar			= _.map(ar, function(d1) {
		return scope.fill(0, options.degree+1);
	});
	var h 		= this.fill(0, length);
	var g		= this.fill(0, options.degree+2);
	
	var t1, t2;
	var n;
	
	var coef	= [];
	
	for (n=1; n <= options.degree; n++)
	{
		var sn = 0.0;
		var sd = 0.0;
		var j;
		var jj = length - n;
	
		for (j = 0; j < jj; j++)
		{
			t1 = options.data[j + n][1] + pef[j];
			t2 = options.data[j][1] + per[j];
			sn -= 2.0 * t1 * t2;
			sd += (t1 * t1) + (t2 * t2);
		}
	
		t1 = g[n] = sn / sd;
		if (n != 1)
		{
			for (j = 1; j < n; j++) {
				h[j] = g[j] + t1 * g[n - j];
			}
			for (j = 1; j < n; j++) {
				g[j] = h[j];
			}
			jj--;
		}
	
		for (j = 0; j < jj; j++)
		{
			per [j] += t1 * pef[j] + t1 * options.data[j + n][1];
			pef [j] = pef[j + 1] + t1 * per[j + 1] + t1 * options.data[j + 1][1];
		}
	
		if (options.intermediates) {
			for (j = 0; j < n; j++) {
				ar[n][j] = g[j + 1];
			}
		}
		
	}
	if (!options.intermediates) {
		for (n = 0; n < options.degree; n++) {
			coef[n] = g[n + 1];
		}
		return coef;
	} else {
		return ar;
	}
	
}


// Autoregression method: Least Square
timeseries.prototype.ARLeastSquare = function(options) {
	// Credits to Rainer Hegger (1998)
	// Ported to Javascript by Julien Loutre for timeseries-analysis, from Paul Bourke's C code.
	var scope = this;
	
	options = _.extend({
		degree:			5,
		data:			this.data
	}, options);
	
	var i,j,k,hj,hi;
	var coefficients = [];
	
	var length 	= options.data.length;
	var mat 	= this.fill([], options.degree);
	mat			= _.map(mat, function(d1) {
		return scope.fill(0, options.degree);
	});
	
	for (i=0;i < options.degree;i++) {
		coefficients[i] = 0.0;
		for (j=0;j< options.degree;j++) {
			mat[i][j] = 0.0;
		}
	}
	for (i=options.degree-1;i < length-1;i++) {
		hi = i + 1;
		for (j=0;j < options.degree;j++) {
			hj = i - j;
			coefficients[j] += (options.data[hi][1] * options.data[hj][1]);
			for (k=j;k < options.degree;k++) {
				mat[j][k] += (options.data[hj][1] * options.data[i-k][1]);
			}
		}
	}
	for (i=0;i < options.degree;i++) {
		coefficients[i] /= (length - options.degree);
		for (j=i;j < options.degree;j++) {
			mat[i][j] /= (length - options.degree);
			mat[j][i] = mat[i][j];
		}
	}
	
	var solved = this.SolveLE(mat,coefficients,options.degree);
	
	return coefficients;
	
}

timeseries.prototype.SolveLE = function(mat, vec, n) {
	// Gaussian elimination solver.
	// Use the coefficients from the Least Square method and make it into the real AR coefficients.
	// Original code by Rainer Hegger (1998). Modified by Paul Bourke.
	// Ported to Javascript by Julien Loutre for timeseries-analysis, from Paul Bourke's C code.
	
	var i,j,k,maxi;
	var vswap 		= [];
	var mswap 		= [];
	var hvec 		= [];
	var max,h,pivot,q;
	
	for (i=0;i<n-1;i++) {
		max = Math.abs(mat[i][i]);
		maxi = i;
		for (j=i+1;j<n;j++) {
			if ((h = Math.abs(mat[j][i])) > max) {
				max = h;
				maxi = j;
			}
		}
		if (maxi != i) {
			mswap     = mat[i];
			mat[i]    = mat[maxi];
			mat[maxi] = mswap;
			vswap     = vec[i];
			vec[i]    = vec[maxi];
			vec[maxi] = vswap;
		}
	
		hvec = mat[i];
		pivot = hvec[i];
		if (Math.abs(pivot) == 0.0) {
			console.log("Singular matrix - fatal!");
			return false;
		}
		for (j=i+1;j<n;j++) {
			q = - mat[j][i] / pivot;
			mat[j][i] = 0.0;
			for (k=i+1;k<n;k++) {
				mat[j][k] += q * hvec[k];
			}
			vec[j] += (q * vec[i]);
		}
	}
	vec[n-1] /= mat[n-1][n-1];
	for (i=n-2;i>=0;i--) {
		hvec = mat[i];
		for (j=n-1;j>i;j--) {
			vec[i] -= (hvec[j] * vec[j]);
		}
		vec[i] /= hvec[i];
	}
	
	return vec;
}

// Regression analysis. Will most likely be re-written in the future.
timeseries.prototype.regression_analysis = function(options) {
	// Original code by Professor Hossein Arsham - http://home.ubalt.edu/ntsbarsh/Business-stat/otherapplets/Trend.htm
	// Re-written for timeseries-analysis.
	
	options = _.extend({
		threshold:	2.5
	}, options);
	
	var output 	= {};
	
	var i;
	var j;
	var E 		= this.data.length;  //total number of input spaces
	var N 		= 0;
	var N1 		= 0;
	var N2 		= 0;
	var SUM 	= 0.0;
	var R 		= 1;
	var Median	= 0;
	var theList = new Array();
	var cval 	= new Array();
	// Run through all the input, add those that have valid values
	var a		= 0;
	for(i=0;i < E;i++) 	{
		SUM 		+= this.data[i][1];
		theList[a] 	= this.data[i][1];
		cval[a] 	= this.data[i][1];
		N++;
		a++;
	}
	//check for insufficient data
	if(N <= 10) {
		console.log("Insufficient data (min 10)");
		return false;
	}
	//sort the list
	for(i=0; i<theList.length-1; i++) {
		for(j=i+1;j<theList.length; j++) {
			if (theList[j] < theList[i])  {
				temp 		= theList[i];
				theList[i] 	= theList[j];
				theList[j] 	= temp;
			}
		}
	}
	//calculate Median
	var aux = 0;
	if(N%2 == 1) {
		aux 	= Math.floor(N/2);
		Median 	= theList[aux];
	} else {
		Median 	= (theList[N/2]+theList[((N/2)-1)])/2;
	}
	
	// Do the math
	var x = Median;
	var y = Math.round(100000*x);
	var z = y/100000;
	// run through each value and compare it with mean
	for(i = 0; i < E; i++)     {
		//check if a value is present and discard the ties
		if(this.data[i][1] != x)  {
			//check if it is greater than mean then adds one
			if (this.data[i][1] > x)		 {
				N1++;
				a = i;
				while (a > 0)  {
					a--;
					if(this.data[a][1] != x) {
						break;
					}
				}
				if (this.data[a][1] < x) {
					R++;
				}
			}
			//if it is less than mean
			else if (this.data[i][1] < x)   {
				N2++;
				a = i;
				while (a > 0) {
					a--;
					if(this.data[a][1] != x)   {
						break;
					}
				}
				if (this.data[a][1] > x)  {
					R++;
				}
			}
		}
	}
	//form.NR.value = R;     //value of x or "Scores"
	// What is the runs' statistic? I don't know...
	// Is it http://en.wikipedia.org/wiki/Wald%E2%80%93Wolfowitz_runs_test ?
	output.runs	= R;
	

	//compute the expected mean and variance of R
	var EM 	= 1 + (2*N1*N2)/(N1+N2);           //Mean "Mu"
	var SD1 = [2*N1*N2*(2*N1*N2-N1-N2)];
	var SD2 = Math.pow( (N1 + N2), 2);
	var SD3 = N1 + N2 - 1;
	var SD4 = SD1 / (SD2 * SD3);           //Standard deviation "Sigma"
	var SD 	= Math.sqrt(SD4);
	//calculating P value MStyle
	var z1 	= (R - EM)/SD;
	var z2 	= Math.abs(z1);
	var z 	= z2;
	
	/* Thanks to Jan de Leeuw for the following function */
	var t 	= (z > 0) ? z : (-z);
	var P1 	= Math.pow((1+t*(0.049867347 + t*(0.0211410061 + t*(0.0032776263 + t*(0.0000380036 + t*(0.0000488906 + t*(0.000005383))))))), -16);
	var p 	= 1 - P1 / 2;
	var t 	= 1-((z > 0) ? p : 1-p);         //this is P-value
	
	//rounding the value
	var t1 	= Math.round(100000*t);
	var t2 	= t1/100000;                  //this is P-value too
	//form.PV.value = t2;

	//determine the conclusion
	// Encoding the trend value from 0 (no trend) to 3 (strong strend evidence)
	if (t2 < 0.01)   {
		//form.CON.value = "Strong evidence for trend";
		output.trend	= 3;
	} else if (t2 < 0.05 && t2 >= 0.01)  {
		//form.CON.value = "Moderate evidence for trend";
		output.trend	= 2;
	} else if (t2 < 0.10 && t2 >= 0.05)  {
		//form.CON.value = "Suggestive evidence for trend";
		output.trend	= 1;
	} else if (t2 >= 0.10)   {
		//form.CON.value = "Little or no real evidences for trend";
		output.trend	= 0;
	} else {
		//form.CON.value = "Strong evidence for trend";
		output.trend	= 3;
	}

	//AUTO CORRELATION
	var DWNN = 0;
	var DWND = (cval[0]*cval[0]);
	for (i=1; i<cval.length; i++)  {
		DWNN = DWNN +(cval[i]- cval[i-1])*(cval[i]-cval[i-1]) ;
		DWND = DWND +(cval[i]*cval[i]);
	}
	var DW = DWNN/DWND;
	DW = Math.round(DW*100000)/100000;
	//form.DW.value = DW;
	output.durbinWatson	= DW;
	
	var Q01 	= 2-4.6527/(Math.sqrt(N+2));
	var Q05 	= 2-3.2897/(Math.sqrt(N+2));
	
	//determine the conclusion
	// Encode the correlation between 1 and 3
	if((DW>=Q01) || (DW<=(4 - Q01)))  {
		//form.COND.value = "Moderate evidence againt autocorrelation";
		output.autocorrelation	= 2;
	} else if((DW >= Q05)&&(DW<=(4 - Q05))) {
		//form.COND.value = "Strong evidences against autocorrelation";
		output.autocorrelation	= 3;
	} else {
		//form.COND.value = "Suggestive evidences for autocorrelation";
		output.autocorrelation	= 1;
	}
	
	return output;
}

// Get the Durbin-Watson statistic
// http://en.wikipedia.org/wiki/Durbin%E2%80%93Watson_statistic
timeseries.prototype.durbinWatson = function() {
	return this.regression_analysis().durbinWatson;
}

// Get the Durbin-Watson statistic
// http://en.wikipedia.org/wiki/Durbin%E2%80%93Watson_statistic
timeseries.prototype.regression_analysis = function() {
	return this.regression_analysis().durbinWatson;
}




// Data adapters
var adapter = {
	
};
adapter.fromDB = function(data, options) {
	options = _.extend({
		value:		'close',
		date:		'date'
	}, options);
	
	return _.map(data, function(datapoint) {
		return [new Date(datapoint[options.date]).getTime(), datapoint[options.value]];
	});
};
adapter.fromArray = function(data) {
	return _.map(data, function(datapoint) {
		return [new Date(), datapoint];
	});
};
adapter.geometric = function(options) {
	options = _.extend({
	}, options);
	
	
	var i;
	var j;
	var output = [];
	for (i=0;i<128;i++) {
		output.push([
			new Date(),
			Math.cos(i*0.01)+0.75*Math.cos(i*0.03)+0.5*Math.cos(i*0.05)+0.25*Math.cos(i*0.11)
		]);
	}
	return output;
};
adapter.complex = function(options) {
	options = _.extend({
		cycles:		10,
		quality:	1,
		inertia:	0
	}, options);
	
	
	var i;
	var j;
	var output = [];
	for (i=0;i<options.cycles;i++) {
		for (j=0;j<360;j+=options.quality) {
			output.push([
				new Date(),
				(Math.sin(j*Math.PI/180)+Math.cos(j*3*Math.PI/180)-Math.sin(j*2.4*Math.PI/180))*100
			]);
			options.quality += options.inertia;
		}
	}
	return output;
};
adapter.sin = function(options) {
	options = _.extend({
		cycles:		4,
		quality:	2,
		inertia:	0	
	}, options);
	
	var i;
	var j;
	var output 	= [];
	for (i=0;i<options.cycles;i++) {
		for (j=0;j<360;j+=options.quality) {
			output.push([
				new Date(),
				Math.cos(j*Math.PI/180)*100
			]);
			options.quality += options.inertia;
		}
		console.log("options.quality",options.quality);
	}
	return output;
};
adapter.tan = function(options) {
	options = _.extend({
		cycles:		1
	}, options);
	var i;
	var j;
	var output = [];
	for (i=0;i<options.cycles;i++) {
		for (j=0;j<360;j++) {
			output.push([
				new Date(),
				Math.tan(j*Math.PI/180)
			]);
		}
	}
	return output;
};


exports.main		= timeseries;
exports.adapter		= adapter;
exports.version		= "1.0.11";