diff --git a/README.md b/README.md index 049a07e..4eafa7e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,18 @@ -UDF-compatible Yahoo datafeed +UDF-compatible Quandl/Yahoo data server ============== -This repository contains a sample implementation of server-side UDF-compatible data source. -Use nodejs to launch yahoo.js \ No newline at end of file +This repository contains a sample of UDF-compatible data server. + +Register for free at www.quandl.com to get a free API key. + +Use NodeJS to launch `yahoo.js` with your Quandl key: + +```bash +QUANDL_API_KEY=YOUR_KEY nodejs yahoo.js +``` +Change the source URL in `index.html` file of the Charting Library: + +```javascript +datafeed: new Datafeeds.UDFCompatibleDatafeed("http://localhost:8888") +``` +Save the file and restart the Charting Library server. diff --git a/logos.js b/logos.js new file mode 100644 index 0000000..9cbaf27 --- /dev/null +++ b/logos.js @@ -0,0 +1,138 @@ +const exchangeLogos = { + NasdaqNM: 'https://s3-symbol-logo.tradingview.com/country/US.svg', + NYSE: 'https://s3-symbol-logo.tradingview.com/country/US.svg', + NCM: 'https://s3-symbol-logo.tradingview.com/country/US.svg', +}; + +exports.getExchangeLogoUrl = function (exchangeName) { + return exchangeLogos[exchangeName]; +}; + +const baseUrl = 'https://s3-symbol-logo.tradingview.com/'; + +const symbolLogoNames = { + A: 'agilent-technologies.svg', + AA: 'alcoa.svg', + AAL: 'american-airlines-group.svg', + AAPL: 'apple.svg', + ABBV: 'abbvie.svg', + ABT: 'abbott.svg', + // ACHN: '.svg', + ACI: 'albertsons.svg', + ACN: 'accenture.svg', + ADBE: 'adobe.svg', + ADSK: 'autodesk.svg', + AEO: 'american-eagle-outfitters.svg', + AGNC: 'agnc-investment.svg', + AIG: 'american-international-group.svg', + AKAM: 'akamai.svg', + // ALXN: '.svg', + AMAT: 'applied-materials.svg', + AMD: 'advanced-micro-devices.svg', + AMGN: 'amgen.svg', + AMZN: 'amazon.svg', + ANF: 'abercrombie-and-fitch.svg', + // ANR: '.svg', + APA: 'apa-corporation.svg', + // APC: '.svg', + ARC: 'arc-document-solutions.svg', + // ARIA: '.svg', + // ARNA: '.svg', + ARR: 'armour-residential-reit-7-series-c-cumulative-redeemable-preferred-stock-liquidation-preference-2500-per-share.svg', + // AUXL: '.svg', + AVGO: 'broadcom.svg', + // AVNR: '.svg', + // AWAY: '.svg', + AXP: 'american-express.svg', + AZO: 'autozone.svg', + BA: 'boeing.svg', + BAC: 'bank-of-america.svg', + BAX: 'baxter.svg', + BBBY: 'bed-bath-and-beyond.svg', + // BBT: '.svg', + BBY: 'best-buy.svg', + BIDU: 'baidu.svg', + BIIB: 'biogen.svg', + BK: 'bank-of-new-york-mellon.svg', + BLK: 'blackrock.svg', + BMY: 'bristol-myers-squibb.svg', + BP: 'bp.svg', + // BRCD: '.svg', + // BRCM: '.svg', + BTU: 'peabody-energy.svg', + C: 'citigroup.svg', + CHK: 'chesapeake-energy.svg', + CNP: 'centerpoint-energy.svg', + CSCO: 'cisco.svg', + D: 'dominion-energy.svg', + DAL: 'delta-air-lines.svg', + // DBD: '.svg', + DD: 'dupont-de-nemours.svg', + DDD: '3-d-systems.svg', + DE: 'deere.svg', + DECK: 'deckers-outdoor.svg', + DEI: 'douglas-emmett.svg', + DHI: 'dr-horton.svg', + DIS: 'walt-disney.svg', + DLTR: 'dollar-tree.svg', + // DNDN: '.svg', + DO: 'diamond-offshore-drilling.svg', + DOV: 'dover.svg', + DOW: 'dow.svg', + DRI: 'darden.svg', + DV: 'doubleverify.svg', + DVN: 'devon-energy.svg', + EA: 'electronic-arts.svg', + EBAY: 'ebay.svg', + EBIX: 'ebix.svg', + // ECYT: '.svg', + ED: 'consolidated-edison.svg', + // EMC: '.svg', + // ENT: '.svg', + ESI: 'element-solutions.svg', + // ESRX: '.svg', + // ETFC: '.svg', + EXC: 'exelon.svg', + EXPE: 'expedia.svg', + F: 'ford.svg', + FCEL: 'fuelcell-energy.svg', + // GALE: '.svg', + GD: 'general-dynamics.svg', + GE: 'general-electric.svg', + // GTAT: '.svg', + HD: 'home-depot.svg', + IBM: 'international-bus-mach.svg', + INTC: 'intel.svg', + JPM: 'jpmorgan-chase.svg', + // KERX: '.svg', + KO: 'coca-cola.svg', + LLY: 'eli-lilly.svg', + LUV: 'southwest.svg', + MCD: 'mcdonalds.svg', + MNST: 'monster-beverage.svg', + MO: 'altria.svg', + MSFT: 'microsoft.svg', + NLY: 'annaly-capital-management.svg', + NUS: 'nu-skin-enterprises.svg', + OLED: 'universal-display.svg', + // PNRA: '.svg', + RAD: 'rite-aid.svg', + SAM: 'boston-beer-co.svg', + // SCTY: '.svg', + SD: 'sandridge-energy.svg', + STZ: 'constellation-brands.svg', + T: 'at-and-t.svg', + UA: 'under-armour.svg', + USB: 'us-bancorp.svg', + VZ: 'verizon.svg', + WDC: 'western-digital.svg', + WFC: 'wells-fargo.svg', + // WLT: '.svg', + XOM: 'exxon.svg', +}; + +exports.getSymbolLogos = function (name) { + const image = symbolLogoNames[name]; + if (!image) return undefined; + return [baseUrl + image]; +}; diff --git a/request-processor.js b/request-processor.js new file mode 100644 index 0000000..c2393c6 --- /dev/null +++ b/request-processor.js @@ -0,0 +1,724 @@ +/* + This file is a node.js module. + + This is a sample implementation of UDF-compatible datafeed wrapper for Quandl (historical data) and yahoo.finance (quotes). + Some algorithms may be incorrect because it's rather an UDF implementation sample + then a proper datafeed implementation. +*/ + +/* global require */ +/* global console */ +/* global exports */ +/* global process */ + +"use strict"; + +var version = '2.1.0'; + +var https = require("https"); +var http = require("http"); + +var logos = require("./logos"); + +var quandlCache = {}; + +var quandlCacheCleanupTime = 24 * 60 * 60 * 1000; // 24 hours +var quandlKeysValidateTime = 15 * 60 * 1000; // 15 minutes +var quandlMinimumDate = '1970-01-01'; + +// this cache is intended to reduce number of requests to Quandl +setInterval(function () { + quandlCache = {}; + console.warn(dateForLogs() + 'Quandl cache invalidated'); +}, quandlCacheCleanupTime); + +function dateForLogs() { + return (new Date()).toISOString() + ': '; +} + +var defaultResponseHeader = { + "Content-Type": "text/plain", + 'Access-Control-Allow-Origin': '*' +}; + +function sendJsonResponse(response, jsonData) { + response.writeHead(200, defaultResponseHeader); + response.write(JSON.stringify(jsonData)); + response.end(); +} + +function dateToYMD(date) { + var obj = new Date(date); + var year = obj.getFullYear(); + var month = obj.getMonth() + 1; + var day = obj.getDate(); + return year + "-" + month + "-" + day; +} + +var quandlKeys = process.env.QUANDL_API_KEY.split(','); // you should create a free account on quandl.com to get this key, you can set some keys concatenated with a comma +var invalidQuandlKeys = []; + +function getValidQuandlKey() { + for (var i = 0; i < quandlKeys.length; i++) { + var key = quandlKeys[i]; + if (invalidQuandlKeys.indexOf(key) === -1) { + return key; + } + } + return null; +} + +function markQuandlKeyAsInvalid(key) { + if (invalidQuandlKeys.indexOf(key) !== -1) { + return; + } + + invalidQuandlKeys.push(key); + + console.warn(dateForLogs() + 'Quandl key invalidated ' + key); + + setTimeout(function() { + console.log(dateForLogs() + "Quandl key restored: " + invalidQuandlKeys.shift()); + }, quandlKeysValidateTime); +} + +function sendError(error, response) { + response.writeHead(200, defaultResponseHeader); + response.write("{\"s\":\"error\",\"errmsg\":\"" + error + "\"}"); + response.end(); +} + +function httpGet(datafeedHost, path, callback) { + var options = { + host: datafeedHost, + path: path + }; + + function onDataCallback(response) { + var result = ''; + + response.on('data', function (chunk) { + result += chunk; + }); + + response.on('end', function () { + if (response.statusCode !== 200) { + callback({ status: 'ERR_STATUS_CODE', errmsg: response.statusMessage || '' }); + return; + } + + callback({ status: 'ok', data: result }); + }); + } + + var req = https.request(options, onDataCallback); + + req.on('socket', function (socket) { + socket.setTimeout(5000); + socket.on('timeout', function () { + console.log(dateForLogs() + 'timeout'); + req.abort(); + }); + }); + + req.on('error', function (e) { + callback({ status: 'ERR_SOCKET', errmsg: e.message || '' }); + }); + + req.end(); +} + +function convertQuandlHistoryToUDFFormat(data) { + function parseDate(input) { + var parts = input.split('-'); + return Date.UTC(parts[0], parts[1] - 1, parts[2]); + } + + function columnIndices(columns) { + var indices = {}; + for (var i = 0; i < columns.length; i++) { + indices[columns[i].name] = i; + } + + return indices; + } + + var result = { + t: [], + c: [], + o: [], + h: [], + l: [], + v: [], + s: "ok" + }; + + try { + var json = JSON.parse(data); + var datatable = json.datatable; + var idx = columnIndices(datatable.columns); + + datatable.data + .sort(function (row1, row2) { + return parseDate(row1[idx.date]) - parseDate(row2[idx.date]); + }) + .forEach(function (row) { + result.t.push(parseDate(row[idx.date]) / 1000); + result.o.push(row[idx.open]); + result.h.push(row[idx.high]); + result.l.push(row[idx.low]); + result.c.push(row[idx.close]); + result.v.push(row[idx.volume]); + }); + + } catch (error) { + return null; + } + + return result; +} + +function proxyRequest(controller, options, response) { + controller.request(options, function (res) { + var result = ''; + + res.on('data', function (chunk) { + result += chunk; + }); + + res.on('end', function () { + if (res.statusCode !== 200) { + response.writeHead(200, defaultResponseHeader); + response.write(JSON.stringify({ + s: 'error', + errmsg: 'Failed to get news' + })); + response.end(); + return; + } + response.writeHead(200, defaultResponseHeader); + response.write(result); + response.end(); + }); + }).end(); +} + +function RequestProcessor(symbolsDatabase) { + this._symbolsDatabase = symbolsDatabase; +} + +function filterDataPeriod(data, fromSeconds, toSeconds, countback) { + if (!data || !data.t) { + return data; + } + + var countbackInt = parseInt(countback, 10); + var countbackValid = !Number.isNaN(countbackInt) && countbackInt > 0; + + if (data.t[data.t.length - 1] < fromSeconds && !countbackValid) { + return { + s: 'no_data', + nextTime: data.t[data.t.length - 1] + }; + } + + var fromIndex = null; + var toIndex = null; + var times = data.t; + for (var i = 0; i < times.length; i++) { + var time = times[i]; + if (fromIndex === null && time >= fromSeconds) { + fromIndex = i; + } + if (toIndex === null && time >= toSeconds) { + toIndex = time > toSeconds ? i - 1 : i; + } + if (fromIndex !== null && toIndex !== null) { + break; + } + } + + fromIndex = fromIndex || 0; + toIndex = toIndex ? toIndex + 1 : times.length; + + var s = data.s; + + if (toSeconds < times[0]) { + s = 'no_data'; + } + + if (countbackValid) { + fromIndex = Math.max(0, toIndex - countbackInt); + } + + /** + * ! Do not send more than 1000 bars for server capacity reasons ! + * + * We are limiting the number of data points returned by sending the latest portion (newest dates). + * The datafeed should be aware of this behavior, so it can request the earlier data again. + * (CL won't ask for the missing data again, so the datafeed API needs to handle bundling the multiple requests together) + */ + fromIndex = Math.max(toIndex - 1000, fromIndex); + + return { + t: data.t.slice(fromIndex, toIndex), + o: data.o.slice(fromIndex, toIndex), + h: data.h.slice(fromIndex, toIndex), + l: data.l.slice(fromIndex, toIndex), + c: data.c.slice(fromIndex, toIndex), + v: data.v.slice(fromIndex, toIndex), + s: s + }; +} + +RequestProcessor.prototype._sendConfig = function (response) { + + var config = { + supports_search: true, + supports_group_request: false, + supports_marks: true, + supports_timescale_marks: true, + supports_time: true, + exchanges: [ + { + value: "", + name: "All Exchanges", + desc: "" + }, + { + value: "NasdaqNM", + name: "NasdaqNM", + desc: "NasdaqNM" + }, + { + value: "NYSE", + name: "NYSE", + desc: "NYSE" + }, + { + value: "NCM", + name: "NCM", + desc: "NCM" + }, + { + value: "NGM", + name: "NGM", + desc: "NGM" + }, + ], + symbols_types: [ + { + name: "All types", + value: "" + }, + { + name: "Stock", + value: "stock" + }, + { + name: "Index", + value: "index" + } + ], + supported_resolutions: ["D", "2D", "3D", "W", "3W", "M", '6M'] + }; + + response.writeHead(200, defaultResponseHeader); + response.write(JSON.stringify(config)); + response.end(); +}; + + +RequestProcessor.prototype._sendMarks = function (response) { + var lastMarkTimestamp = 1522108800; + var day = 60 * 60 * 24; + + var marks = { + id: [0, 1, 2, 3, 4, 5], + time: [ + lastMarkTimestamp, + lastMarkTimestamp - day * 4, + lastMarkTimestamp - day * 7, + lastMarkTimestamp - day * 7, + lastMarkTimestamp - day * 15, + lastMarkTimestamp - day * 30 + ], + color: ["red", "blue", "green", "red", "blue", "green"], + text: ["Red", "Blue", "Green + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Red again", "Blue", "Green"], + label: ["A", "B", "CORE", "D", "EURO", "F"], + labelFontColor: ["white", "white", "red", "#FFFFFF", "white", "#000"], + minSize: [14, 28, 7, 40, 7, 14] + }; + + response.writeHead(200, defaultResponseHeader); + response.write(JSON.stringify(marks)); + response.end(); +}; + +RequestProcessor.prototype._sendTime = function (response) { + var now = new Date(); + response.writeHead(200, defaultResponseHeader); + response.write(Math.floor(now / 1000) + ''); + response.end(); +}; + +RequestProcessor.prototype._sendTimescaleMarks = function (response) { + var lastMarkTimestamp = 1522108800; + var day = 60 * 60 * 24; + + var marks = [ + { + id: "tsm1", + time: lastMarkTimestamp, + color: "#F23645", + label: "A", + tooltip: "" + }, + { + id: "tsm2", + time: lastMarkTimestamp - day * 4, + color: "#2962FF", + label: "D", + tooltip: ["Dividends: $0.56", "Date: " + new Date((lastMarkTimestamp - day * 4) * 1000).toDateString()] + }, + { + id: "tsm3", + time: lastMarkTimestamp - day * 7, + color: "#089981", + label: "D", + tooltip: ["Dividends: $3.46", "Date: " + new Date((lastMarkTimestamp - day * 7) * 1000).toDateString()] + }, + { + id: "tsm4", + time: lastMarkTimestamp - day * 15, + color: "#F23645", + label: "E", + tooltip: ["Earnings: $3.44", "Estimate: $3.60"], + shape: 'earningDown', + }, + { + id: "tsm7", + time: lastMarkTimestamp - day * 30, + color: "#089981", + label: "E", + tooltip: ["Earnings: $5.40", "Estimate: $5.00"], + shape: 'earningUp', + }, + { + id: "tsm8", + time: lastMarkTimestamp - day * 30, + color: "#FF9800", + label: "S", + tooltip: ["Split: 4/1", "Date: " + new Date((lastMarkTimestamp - day * 30) * 1000).toDateString()], + }, + ]; + + response.writeHead(200, defaultResponseHeader); + response.write(JSON.stringify(marks)); + response.end(); +}; + + +RequestProcessor.prototype._sendSymbolSearchResults = function (query, type, exchange, maxRecords, response) { + if (!maxRecords) { + throw "wrong_query"; + } + + var result = this._symbolsDatabase.search(query, type, exchange, maxRecords); + + response.writeHead(200, defaultResponseHeader); + response.write(JSON.stringify(result)); + response.end(); +}; + +RequestProcessor.prototype._prepareSymbolInfo = function (symbolName) { + var symbolInfo = this._symbolsDatabase.symbolInfo(symbolName); + + if (!symbolInfo) { + throw "unknown_symbol " + symbolName; + } + + var result = { + "name": symbolInfo.name, + "exchange-traded": symbolInfo.exchange, + "exchange-listed": symbolInfo.exchange, + "timezone": "America/New_York", + "minmov": 1, + "minmov2": 0, + "pointvalue": 1, + "session": "0930-1630", + "has_intraday": false, + "visible_plots_set": symbolInfo.type !== "stock" ? 'ohlc' : 'ohlcv', + "description": symbolInfo.description.length > 0 ? symbolInfo.description : symbolInfo.name, + "type": symbolInfo.type, + "supported_resolutions": ["D", "2D", "3D", "W", "3W", "M", "6M"], + "pricescale": 100, + "ticker": symbolInfo.name.toUpperCase() + }; + + var logoUrls = logos.getSymbolLogos(symbolInfo.name); + if (logoUrls) { + result.logo_urls = logoUrls; + } + var exchangeLogo = logos.getExchangeLogoUrl(symbolInfo.exchange); + if (exchangeLogo) { + result.exchange_logo = exchangeLogo; + } + + return result; +}; + +RequestProcessor.prototype._sendSymbolInfo = function (symbolName, response) { + var info = this._prepareSymbolInfo(symbolName); + + response.writeHead(200, defaultResponseHeader); + response.write(JSON.stringify(info)); + response.end(); +}; + +RequestProcessor.prototype._sendSymbolHistory = function (symbol, startDateTimestamp, endDateTimestamp, resolution, countback, response) { + function sendResult(content) { + var header = Object.assign({}, defaultResponseHeader); + header["Content-Length"] = content.length; + response.writeHead(200, header); + response.write(content, null, function () { + response.end(); + }); + } + + function secondsToISO(sec) { + if (sec === null || sec === undefined) { + return 'n/a'; + } + return (new Date(sec * 1000).toISOString()); + } + + function logForData(data, key, isCached) { + var fromCacheTime = data && data.t ? data.t[0] : null; + var toCacheTime = data && data.t ? data.t[data.t.length - 1] : null; + console.log(dateForLogs() + "Return QUANDL result" + (isCached ? " from cache" : "") + ": " + key + ", from " + secondsToISO(fromCacheTime) + " to " + secondsToISO(toCacheTime)); + } + + symbol = (symbol || '').trim(); + + if (symbol.length === 0) { + console.log(dateForLogs() + "Invalid symbol=" + symbol); + sendError('Invalid symbol', response); + return; + } + + console.log(dateForLogs() + "Got history request for " + symbol + ", " + resolution + " from " + secondsToISO(startDateTimestamp)+ " to " + secondsToISO(endDateTimestamp) + (countback ? " [countback: " + countback + "]": "")); + + // always request all data to reduce number of requests to quandl + var from = quandlMinimumDate; + var to = dateToYMD(Date.now()); + + var key = symbol + "|" + from + "|" + to; + + if (quandlCache[key]) { + var dataFromCache = filterDataPeriod(quandlCache[key], startDateTimestamp, endDateTimestamp, countback); + logForData(dataFromCache, key, true); + sendResult(JSON.stringify(dataFromCache)); + return; + } + + var quandlKey = getValidQuandlKey(); + + if (quandlKey === null) { + console.log(dateForLogs() + "No valid quandl key available"); + sendError('No valid API Keys available', response); + return; + } + + var address = "/api/v3/datatables/WIKI/PRICES.json" + + "?api_key=" + quandlKey + // you should create a free account on quandl.com to get this key + "&ticker=" + symbol + + "&date.gte=" + from + // this is the min quandl data (so we will get the full history and reduce number of requests) + "&date.lte=" + to; + + console.log(dateForLogs() + "Sending request to quandl " + key + ". url=" + address); + + httpGet("www.quandl.com", address, function (result) { + if (response.finished) { + // we can be here if error happened on socket disconnect + return; + } + + if (result.status !== 'ok') { + if (result.status === 'ERR_SOCKET') { + console.log('Socket problem with request: ' + result.errmsg); + sendError("Socket problem with request " + result.errmsg, response); + return; + } + + console.error(dateForLogs() + "Error response from quandl for key " + key + ". Message: " + result.errmsg); + markQuandlKeyAsInvalid(quandlKey); + sendError("Error quandl response " + result.errmsg, response); + return; + } + + console.log(dateForLogs() + "Got response from quandl " + key + ". Try to parse."); + var data = convertQuandlHistoryToUDFFormat(result.data); + if (data === null) { + var dataStr = typeof result === "string" ? result.slice(0, 100) : result; + console.error(dateForLogs() + " failed to parse: " + dataStr); + sendError("Invalid quandl response", response); + return; + } + + if (data.t.length !== 0) { + console.log(dateForLogs() + "Successfully parsed and put to cache " + data.t.length + " bars."); + quandlCache[key] = data; + } else { + console.log(dateForLogs() + "Parsing returned empty result."); + } + + var filteredData = filterDataPeriod(data, startDateTimestamp, endDateTimestamp, countback); + logForData(filteredData, key, false); + sendResult(JSON.stringify(filteredData)); + }); +}; + +RequestProcessor.prototype._quotesQuandlWorkaround = function (tickersMap) { + var from = quandlMinimumDate; + var to = dateToYMD(Date.now()); + + var result = { + s: "ok", + d: [], + source: 'Quandl', + }; + + Object.keys(tickersMap).forEach(function(symbol) { + var key = symbol + "|" + from + "|" + to; + var ticker = tickersMap[symbol]; + + var data = quandlCache[key]; + var length = data === undefined ? 0 : data.c.length; + + if (length > 0) { + var lastBar = { + o: data.o[length - 1], + h: data.o[length - 1], + l: data.o[length - 1], + c: data.o[length - 1], + v: data.o[length - 1], + }; + + result.d.push({ + s: "ok", + n: ticker, + v: { + ch: 0, + chp: 0, + + short_name: symbol, + exchange: '', + original_name: ticker, + description: ticker, + + lp: lastBar.c, + ask: lastBar.c, + bid: lastBar.c, + + open_price: lastBar.o, + high_price: lastBar.h, + low_price: lastBar.l, + prev_close_price: length > 1 ? data.c[length - 2] : lastBar.o, + volume: lastBar.v, + } + }); + } + }); + + return result; +}; + +RequestProcessor.prototype._sendQuotes = function (tickersString, response) { + var tickersMap = {}; // maps YQL symbol to ticker + + var tickers = tickersString.split(","); + [].concat(tickers).forEach(function (ticker) { + var yqlSymbol = ticker.replace(/.*:(.*)/, "$1"); + tickersMap[yqlSymbol] = ticker; + }); + + sendJsonResponse(response, this._quotesQuandlWorkaround(tickersMap)); + console.log("Quotes request : " + tickersString + ' processed from quandl cache'); +}; + +RequestProcessor.prototype._sendNews = function (symbol, response) { + var options = { + host: "feeds.finance.yahoo.com", + path: "/rss/2.0/headline?s=" + symbol + "®ion=US&lang=en-US", + headers: { + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + }, + }; + + proxyRequest(https, options, response); +}; + +RequestProcessor.prototype._sendTVNews = function (response) { + proxyRequest(https, 'https://www.tradingview.com/key-events/feed/', response); +}; + +RequestProcessor.prototype._sendFuturesmag = function (response) { + var options = { + host: "www.oilprice.com", + path: "/rss/main" + }; + + proxyRequest(http, options, response); +}; + +RequestProcessor.prototype.processRequest = function (action, query, response) { + try { + if (action === "/config") { + this._sendConfig(response); + } + else if (action === "/symbols" && !!query["symbol"]) { + this._sendSymbolInfo(query["symbol"], response); + } + else if (action === "/search") { + this._sendSymbolSearchResults(query["query"], query["type"], query["exchange"], query["limit"], response); + } + else if (action === "/history") { + this._sendSymbolHistory(query["symbol"], query["from"], query["to"], (query["resolution"] || "").toLowerCase(), query["countback"], response); + } + else if (action === "/quotes") { + this._sendQuotes(query["symbols"], response); + } + else if (action === "/marks") { + this._sendMarks(response); + } + else if (action === "/time") { + this._sendTime(response); + } + else if (action === "/timescale_marks") { + this._sendTimescaleMarks(response); + } + else if (action === "/news") { + this._sendNews(query["symbol"], response); + } + else if (action === "/tv_news") { + this._sendTVNews(response); + } + else if (action === "/futuresmag") { + this._sendFuturesmag(response); + } else { + response.writeHead(200, defaultResponseHeader); + response.write('Datafeed version is ' + version + + '\nValid keys count is ' + String(quandlKeys.length - invalidQuandlKeys.length) + + '\nCurrent key is ' + (getValidQuandlKey() || '').slice(0, 3) + + (invalidQuandlKeys.length !== 0 ? '\nInvalid keys are ' + invalidQuandlKeys.reduce(function(prev, cur) { return prev + cur.slice(0, 3) + ','; }, '') : '')); + response.end(); + } + } + catch (error) { + sendError(error, response); + console.error('Exception: ' + error); + } +}; + +exports.RequestProcessor = RequestProcessor; diff --git a/symbols_database.js b/symbols_database.js index 51f094a..45450d9 100644 --- a/symbols_database.js +++ b/symbols_database.js @@ -1,199 +1,199 @@ /* This file is a node.js module intended for use in different UDF datafeeds. */ -// This list should contain all the symbosl available through your datafeed. -// The current version is extremely incomplete (as it's just a sample): Yahoo has much more of them. - - -var symbols = [ -{ name: "^GDAXI", description:"DAX", exchange:"XETRA", type:"index" }, -{ name: "^NSEI", description:"CNX NIFTY", exchange:"NSE", type:"index" }, -{ name: "A", description:"Agilent Technologies Inc.", exchange:"NYSE", type:"stock" }, -{ name: "AA", description:"Alcoa Inc.", exchange:"NYSE", type:"stock" }, -{ name: "AAL", description:"American Airlines Group Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AAPL", description:"Apple Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ABB", description:"ABB Ltd.", exchange:"NYSE", type:"stock" }, -{ name: "ABBV", description:"AbbVie Inc.", exchange:"NYSE", type:"stock" }, -{ name: "ABT", description:"Abbott Laboratories", exchange:"NYSE", type:"stock" }, -{ name: "ABX", description:"Barrick Gold Corporation", exchange:"NYSE", type:"stock" }, -{ name: "ACHN", description:"Achillion Pharmaceuticals, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ACI", description:"Arch Coal Inc.", exchange:"NYSE", type:"stock" }, -{ name: "ACN", description:"Accenture plc", exchange:"NYSE", type:"stock" }, -{ name: "ACT", description:"Actavis plc", exchange:"NYSE", type:"stock" }, -{ name: "ADBE", description:"Adobe Systems Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ADSK", description:"Autodesk, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AEO", description:"American Eagle Outfitters, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "AGNC", description:"American Capital Agency Corp.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AIG", description:"American International Group, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "AKAM", description:"Akamai Technologies, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ALU", description:"Alcatel-Lucent", exchange:"NYSE", type:"stock" }, -{ name: "ALXN", description:"Alexion Pharmaceuticals, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AMAT", description:"Applied Materials, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AMD", description:"Advanced Micro Devices, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "AMGN", description:"Amgen Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AMZN", description:"Amazon.com Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ANF", description:"Abercrombie & Fitch Co.", exchange:"NYSE", type:"stock" }, -{ name: "ANR", description:"Alpha Natural Resources, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "APA", description:"Apache Corp.", exchange:"NYSE", type:"stock" }, -{ name: "APC", description:"Anadarko Petroleum Corporation", exchange:"NYSE", type:"stock" }, -{ name: "ARC", description:"ARC Document Solutions, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "ARIA", description:"Ariad Pharmaceuticals Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ARNA", description:"Arena Pharmaceuticals, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ARR", description:"ARMOUR Residential REIT, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "AUXL", description:"Auxilium Pharmaceuticals Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AVGO", description:"Avago Technologies Limited", exchange:"NasdaqNM", type:"stock" }, -{ name: "AVNR", description:"Avanir Pharmaceuticals, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AWAY", description:"HomeAway, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "AXP", description:"American Express Company", exchange:"NYSE", type:"stock" }, -{ name: "AZO", description:"AutoZone, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "BA", description:"The Boeing Company", exchange:"NYSE", type:"stock" }, -{ name: "BAC", description:"Bank of America Corporation", exchange:"NYSE", type:"stock" }, -{ name: "BAX", description:"Baxter International Inc.", exchange:"NYSE", type:"stock" }, -{ name: "BBBY", description:"Bed Bath & Beyond Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "BBRY", description:"BlackBerry Limited", exchange:"NasdaqNM", type:"stock" }, -{ name: "BBT", description:"BB&T Corporation", exchange:"NYSE", type:"stock" }, -{ name: "BBY", description:"Best Buy Co., Inc.", exchange:"NYSE", type:"stock" }, -{ name: "BIDU", description:"Baidu, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "BIIB", description:"Biogen Idec Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "BK", description:"The Bank of New York Mellon Corporation", exchange:"NYSE", type:"stock" }, -{ name: "BLK", description:"BlackRock, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "BMO", description:"Bank of Montreal", exchange:"NYSE", type:"stock" }, -{ name: "BMY", description:"Bristol-Myers Squibb Company", exchange:"NYSE", type:"stock" }, -{ name: "BP", description:"BP plc", exchange:"NYSE", type:"stock" }, -{ name: "BRCD", description:"Brocade Communications Systems, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "BRCM", description:"Broadcom Corp.", exchange:"NasdaqNM", type:"stock" }, -{ name: "BRK-B", description:"Berkshire Hathaway Inc.", exchange:"NYSE", type:"stock" }, -{ name: "BTU", description:"Peabody Energy Corp.", exchange:"NYSE", type:"stock" }, -{ name: "BX", description:"The Blackstone Group L.P.", exchange:"NYSE", type:"stock" }, -{ name: "C", description:"Citigroup Inc.", exchange:"NYSE", type:"stock" }, -{ name: "CHK", description:"Chesapeake Energy Corporation", exchange:"NYSE", type:"stock" }, -{ name: "CII.TA", description:"", exchange:"Tel Aviv", type:"stock" }, -{ name: "CNP", description:"CenterPoint Energy, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "COLE", description:"Cole Real Estate Investments, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "CSCO", description:"Cisco Systems, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "D", description:"Dominion Resources, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "DAL", description:"Delta Air Lines Inc.", exchange:"NYSE", type:"stock" }, -{ name: "DANG", description:"E-Commerce China Dangdang Inc.", exchange:"NYSE", type:"stock" }, -{ name: "DBD", description:"Diebold, Incorporated", exchange:"NYSE", type:"stock" }, -{ name: "DCM", description:"NTT DOCOMO, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "DD", description:"E. I. du Pont de Nemours and Company", exchange:"NYSE", type:"stock" }, -{ name: "DDD", description:"3D Systems Corp.", exchange:"NYSE", type:"stock" }, -{ name: "DE", description:"Deere & Company", exchange:"NYSE", type:"stock" }, -{ name: "DE.AS", description:"", exchange:"Amsterdam", type:"stock" }, -{ name: "DECK", description:"Deckers Outdoor Corp.", exchange:"NYSE", type:"stock" }, -{ name: "DEI", description:"Douglas Emmett Inc", exchange:"NYSE", type:"stock" }, -{ name: "DHI", description:"DR Horton Inc.", exchange:"NYSE", type:"stock" }, -{ name: "DIS", description:"The Walt Disney Company", exchange:"NYSE", type:"stock" }, -{ name: "DLTR", description:"Dollar Tree, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "DNDN", description:"Dendreon Corp.", exchange:"NasdaqNM", type:"stock" }, -{ name: "DO", description:"Diamond Offshore Drilling, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "DOV", description:"Dover Corporation", exchange:"NYSE", type:"stock" }, -{ name: "DOW", description:"The Dow Chemical Company", exchange:"NYSE", type:"stock" }, -{ name: "DPM", description:"DCP Midstream Partners LP", exchange:"NYSE", type:"stock" }, -{ name: "DRI", description:"Darden Restaurants, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "DRX.IR", description:"", exchange:"Irish", type:"stock" }, -{ name: "DRYS", description:"DryShips, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "DV", description:"DeVry Education Group Inc.", exchange:"NYSE", type:"stock" }, -{ name: "DVN", description:"Devon Energy Corporation", exchange:"NYSE", type:"stock" }, -{ name: "EA", description:"Electronic Arts Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "EBAY", description:"eBay Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "EBIX", description:"Ebix Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ECYT", description:"Endocyte, Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "ED", description:"Consolidated Edison, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "EMC", description:"EMC Corporation", exchange:"NYSE", type:"stock" }, -{ name: "ENT", description:"Global Eagle Entertainment Inc.", exchange:"NCM", type:"stock" }, -{ name: "ESI", description:"ITT Educational Services Inc.", exchange:"NYSE", type:"stock" }, -{ name: "ESRX", description:"Express Scripts Holding Company", exchange:"NasdaqNM", type:"stock" }, -{ name: "ETFC", description:"E*TRADE Financial Corporation", exchange:"NasdaqNM", type:"stock" }, -{ name: "ETP", description:"Energy Transfer Partners, L.P.", exchange:"NYSE", type:"stock" }, -{ name: "EXC", description:"Exelon Corporation", exchange:"NYSE", type:"stock" }, -{ name: "EXPE", description:"Expedia Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "EZCH", description:"EZchip Semiconductor Ltd.", exchange:"NasdaqNM", type:"stock" }, -{ name: "F", description:"Ford Motor Co.", exchange:"NYSE", type:"stock" }, -{ name: "FCEL", description:"FuelCell Energy Inc.", exchange:"NGM", type:"stock" }, -{ name: "FRE.AX", description:"Freshtel Holdings Limited", exchange:"ASX", type:"stock" }, -{ name: "GALE", description:"Galena Biopharma, Inc.", exchange:"NCM", type:"stock" }, -{ name: "GD", description:"General Dynamics Corp.", exchange:"NYSE", type:"stock" }, -{ name: "GE", description:"General Electric Company", exchange:"NYSE", type:"stock" }, -{ name: "GTAT", description:"GT Advanced Technologies Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "HD", description:"The Home Depot, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "IBM", description:"International Business Machines Corporation", exchange:"NYSE", type:"stock" }, -{ name: "INTC", description:"Intel Corporation", exchange:"NasdaqNM", type:"stock" }, -{ name: "JPM", description:"JPMorgan Chase & Co.", exchange:"NYSE", type:"stock" }, -{ name: "KERX", description:"Keryx Biopharmaceuticals Inc.", exchange:"NCM", type:"stock" }, -{ name: "KMP", description:"Kinder Morgan Energy Partners, L.P.", exchange:"NYSE", type:"stock" }, -{ name: "KO", description:"The Coca-Cola Company", exchange:"NYSE", type:"stock" }, -{ name: "LINE", description:"Linn Energy, LLC", exchange:"NasdaqNM", type:"stock" }, -{ name: "LLY", description:"Eli Lilly and Company", exchange:"NYSE", type:"stock" }, -{ name: "LULU", description:"Lululemon Athletica Inc.", exchange:"NasdaqNM", type:"stock" }, -{ name: "LUV", description:"Southwest Airlines Co.", exchange:"NYSE", type:"stock" }, -{ name: "LYG", description:"Lloyds Banking Group plc", exchange:"NYSE", type:"stock" }, -{ name: "MCD", description:"McDonald's Corp.", exchange:"NYSE", type:"stock" }, -{ name: "MNST", description:"Monster Beverage Corporation", exchange:"NasdaqNM", type:"stock" }, -{ name: "MO", description:"Altria Group Inc.", exchange:"NYSE", type:"stock" }, -{ name: "MPEL", description:"Melco Crown Entertainment Limited", exchange:"NasdaqNM", type:"stock" }, -{ name: "MSFT", description:"Microsoft Corporation", exchange:"NasdaqNM", type:"stock" }, -{ name: "NLY", description:"Annaly Capital Management, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "NUS", description:"Nu Skin Enterprises Inc.", exchange:"NYSE", type:"stock" }, -{ name: "OLED", description:"Universal Display Corp.", exchange:"NasdaqNM", type:"stock" }, -{ name: "PNRA", description:"Panera Bread Company", exchange:"NasdaqNM", type:"stock" }, -{ name: "PRAN", description:"Prana Biotechnology Limited", exchange:"NCM", type:"stock" }, -{ name: "RAD", description:"Rite Aid Corporation", exchange:"NYSE", type:"stock" }, -{ name: "SAM", description:"Boston Beer Co. Inc.", exchange:"NYSE", type:"stock" }, -{ name: "SAN", description:"Banco Santander, S.A.", exchange:"NYSE", type:"stock" }, -{ name: "SCTY", description:"SolarCity Corporation", exchange:"NasdaqNM", type:"stock" }, -{ name: "SD", description:"SandRidge Energy, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "STZ", description:"Constellation Brands Inc.", exchange:"NYSE", type:"stock" }, -{ name: "SU", description:"Suncor Energy Inc.", exchange:"NYSE", type:"stock" }, -{ name: "T", description:"AT&T, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "TEG.AX", description:"Triangle Energy (Global) Limited", exchange:"ASX", type:"stock" }, -{ name: "TTW.V", description:"TIMES THREE WIRELESS INC", exchange:"CDNX", type:"stock" }, -{ name: "UA", description:"Under Armour, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "UCD.SG", description:"UC RESOURCES", exchange:"Stuttgart", type:"stock" }, -{ name: "USB", description:"U.S. Bancorp", exchange:"NYSE", type:"stock" }, -{ name: "VLG.MC", description:"", exchange:"NYSE", type:"stock" }, -{ name: "VZ", description:"Verizon Communications Inc.", exchange:"NYSE", type:"stock" }, -{ name: "WDC", description:"Western Digital Corporation", exchange:"NasdaqNM", type:"stock" }, -{ name: "WFC", description:"Wells Fargo & Company", exchange:"NYSE", type:"stock" }, -{ name: "WLT", description:"Walter Energy, Inc.", exchange:"NYSE", type:"stock" }, -{ name: "XOM", description:"Exxon Mobil Corporation", exchange:"NYSE", type:"stock" }, -]; +// This list should contain all the symbols available through your datafeed. +// The current version is extremely incomplete (as it's just a sample): Quandl has much more of them. + +"use strict"; + +var logos = require("./logos"); + +/* global exports */ + +var symbols = [{"name":"A","description":"Agilent Technologies Inc.","exchange":"NYSE","type":"stock"}, +{"name":"AA","description":"Alcoa Inc.","exchange":"NYSE","type":"stock"}, +{"name":"AAL","description":"American Airlines Group Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AAPL","description":"Apple Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ABBV","description":"AbbVie Inc.","exchange":"NYSE","type":"stock"}, +{"name":"ABT","description":"Abbott Laboratories","exchange":"NYSE","type":"stock"}, +{"name":"ACHN","description":"Achillion Pharmaceuticals, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ACI","description":"Arch Coal Inc.","exchange":"NYSE","type":"stock"}, +{"name":"ACN","description":"Accenture plc","exchange":"NYSE","type":"stock"}, +{"name":"ADBE","description":"Adobe Systems Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ADSK","description":"Autodesk, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AEO","description":"American Eagle Outfitters, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"AGNC","description":"American Capital Agency Corp.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AIG","description":"American International Group, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"AKAM","description":"Akamai Technologies, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ALXN","description":"Alexion Pharmaceuticals, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AMAT","description":"Applied Materials, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AMD","description":"Advanced Micro Devices, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"AMGN","description":"Amgen Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AMZN","description":"Amazon.com Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ANF","description":"Abercrombie & Fitch Co.","exchange":"NYSE","type":"stock"}, +{"name":"ANR","description":"Alpha Natural Resources, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"APA","description":"Apache Corp.","exchange":"NYSE","type":"stock"}, +{"name":"APC","description":"Anadarko Petroleum Corporation","exchange":"NYSE","type":"stock"}, +{"name":"ARC","description":"ARC Document Solutions, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"ARIA","description":"Ariad Pharmaceuticals Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ARNA","description":"Arena Pharmaceuticals, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ARR","description":"ARMOUR Residential REIT, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"AUXL","description":"Auxilium Pharmaceuticals Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AVGO","description":"Avago Technologies Limited","exchange":"NasdaqNM","type":"stock"}, +{"name":"AVNR","description":"Avanir Pharmaceuticals, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AWAY","description":"HomeAway, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"AXP","description":"American Express Company","exchange":"NYSE","type":"stock"}, +{"name":"AZO","description":"AutoZone, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"BA","description":"The Boeing Company","exchange":"NYSE","type":"stock"}, +{"name":"BAC","description":"Bank of America Corporation","exchange":"NYSE","type":"stock"}, +{"name":"BAX","description":"Baxter International Inc.","exchange":"NYSE","type":"stock"}, +{"name":"BBBY","description":"Bed Bath & Beyond Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"BBT","description":"BB&T Corporation","exchange":"NYSE","type":"stock"}, +{"name":"BBY","description":"Best Buy Co., Inc.","exchange":"NYSE","type":"stock"}, +{"name":"BIDU","description":"Baidu, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"BIIB","description":"Biogen Idec Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"BK","description":"The Bank of New York Mellon Corporation","exchange":"NYSE","type":"stock"}, +{"name":"BLK","description":"BlackRock, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"BMY","description":"Bristol-Myers Squibb Company","exchange":"NYSE","type":"stock"}, +{"name":"BP","description":"BP plc","exchange":"NYSE","type":"stock"}, +{"name":"BRCD","description":"Brocade Communications Systems, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"BRCM","description":"Broadcom Corp.","exchange":"NasdaqNM","type":"stock"}, +{"name":"BTU","description":"Peabody Energy Corp.","exchange":"NYSE","type":"stock"}, +{"name":"C","description":"Citigroup Inc.","exchange":"NYSE","type":"stock"}, +{"name":"CHK","description":"Chesapeake Energy Corporation","exchange":"NYSE","type":"stock"}, +{"name":"CNP","description":"CenterPoint Energy, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"CSCO","description":"Cisco Systems, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"D","description":"Dominion Resources, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"DAL","description":"Delta Air Lines Inc.","exchange":"NYSE","type":"stock"}, +{"name":"DBD","description":"Diebold, Incorporated","exchange":"NYSE","type":"stock"}, +{"name":"DD","description":"E. I. du Pont de Nemours and Company","exchange":"NYSE","type":"stock"}, +{"name":"DDD","description":"3D Systems Corp.","exchange":"NYSE","type":"stock"}, +{"name":"DE","description":"Deere & Company","exchange":"NYSE","type":"stock"}, +{"name":"DECK","description":"Deckers Outdoor Corp.","exchange":"NYSE","type":"stock"}, +{"name":"DEI","description":"Douglas Emmett Inc","exchange":"NYSE","type":"stock"}, +{"name":"DHI","description":"DR Horton Inc.","exchange":"NYSE","type":"stock"}, +{"name":"DIS","description":"The Walt Disney Company","exchange":"NYSE","type":"stock"}, +{"name":"DLTR","description":"Dollar Tree, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"DNDN","description":"Dendreon Corp.","exchange":"NasdaqNM","type":"stock"}, +{"name":"DO","description":"Diamond Offshore Drilling, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"DOV","description":"Dover Corporation","exchange":"NYSE","type":"stock"}, +{"name":"DOW","description":"The Dow Chemical Company","exchange":"NYSE","type":"stock"}, +{"name":"DRI","description":"Darden Restaurants, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"DV","description":"DeVry Education Group Inc.","exchange":"NYSE","type":"stock"}, +{"name":"DVN","description":"Devon Energy Corporation","exchange":"NYSE","type":"stock"}, +{"name":"EA","description":"Electronic Arts Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"EBAY","description":"eBay Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"EBIX","description":"Ebix Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ECYT","description":"Endocyte, Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"ED","description":"Consolidated Edison, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"EMC","description":"EMC Corporation","exchange":"NYSE","type":"stock"}, +{"name":"ENT","description":"Global Eagle Entertainment Inc.","exchange":"NCM","type":"stock"}, +{"name":"ESI","description":"ITT Educational Services Inc.","exchange":"NYSE","type":"stock"}, +{"name":"ESRX","description":"Express Scripts Holding Company","exchange":"NasdaqNM","type":"stock"}, +{"name":"ETFC","description":"E*TRADE Financial Corporation","exchange":"NasdaqNM","type":"stock"}, +{"name":"EXC","description":"Exelon Corporation","exchange":"NYSE","type":"stock"}, +{"name":"EXPE","description":"Expedia Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"F","description":"Ford Motor Co.","exchange":"NYSE","type":"stock"}, +{"name":"FCEL","description":"FuelCell Energy Inc.","exchange":"NGM","type":"stock"}, +{"name":"GALE","description":"Galena Biopharma, Inc.","exchange":"NCM","type":"stock"}, +{"name":"GD","description":"General Dynamics Corp.","exchange":"NYSE","type":"stock"}, +{"name":"GE","description":"General Electric Company","exchange":"NYSE","type":"stock"}, +{"name":"GTAT","description":"GT Advanced Technologies Inc.","exchange":"NasdaqNM","type":"stock"}, +{"name":"HD","description":"The Home Depot, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"IBM","description":"International Business Machines Corporation","exchange":"NYSE","type":"stock"}, +{"name":"INTC","description":"Intel Corporation","exchange":"NasdaqNM","type":"stock"}, +{"name":"JPM","description":"JPMorgan Chase & Co.","exchange":"NYSE","type":"stock"}, +{"name":"KERX","description":"Keryx Biopharmaceuticals Inc.","exchange":"NCM","type":"stock"}, +{"name":"KO","description":"The Coca-Cola Company","exchange":"NYSE","type":"stock"}, +{"name":"LLY","description":"Eli Lilly and Company","exchange":"NYSE","type":"stock"}, +{"name":"LUV","description":"Southwest Airlines Co.","exchange":"NYSE","type":"stock"}, +{"name":"MCD","description":"McDonald's Corp.","exchange":"NYSE","type":"stock"}, +{"name":"MNST","description":"Monster Beverage Corporation","exchange":"NasdaqNM","type":"stock"}, +{"name":"MO","description":"Altria Group Inc.","exchange":"NYSE","type":"stock"}, +{"name":"MSFT","description":"Microsoft Corporation","exchange":"NasdaqNM","type":"stock"}, +{"name":"NLY","description":"Annaly Capital Management, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"NUS","description":"Nu Skin Enterprises Inc.","exchange":"NYSE","type":"stock"}, +{"name":"OLED","description":"Universal Display Corp.","exchange":"NasdaqNM","type":"stock"}, +{"name":"PNRA","description":"Panera Bread Company","exchange":"NasdaqNM","type":"stock"}, +{"name":"RAD","description":"Rite Aid Corporation","exchange":"NYSE","type":"stock"}, +{"name":"SAM","description":"Boston Beer Co. Inc.","exchange":"NYSE","type":"stock"}, +{"name":"SCTY","description":"SolarCity Corporation","exchange":"NasdaqNM","type":"stock"}, +{"name":"SD","description":"SandRidge Energy, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"STZ","description":"Constellation Brands Inc.","exchange":"NYSE","type":"stock"}, +{"name":"T","description":"AT&T, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"UA","description":"Under Armour, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"USB","description":"U.S. Bancorp","exchange":"NYSE","type":"stock"}, +{"name":"VZ","description":"Verizon Communications Inc.","exchange":"NYSE","type":"stock"}, +{"name":"WDC","description":"Western Digital Corporation","exchange":"NasdaqNM","type":"stock"}, +{"name":"WFC","description":"Wells Fargo & Company","exchange":"NYSE","type":"stock"}, +{"name":"WLT","description":"Walter Energy, Inc.","exchange":"NYSE","type":"stock"}, +{"name":"XOM","description":"Exxon Mobil Corporation","exchange":"NYSE","type":"stock"}]; function searchResultFromDatabaseItem(item) { - return { + const result = { symbol: item.name, full_name: item.name, description: item.description, exchange: item.exchange, - type: item.type + type: item.type, }; + const logoUrls = logos.getSymbolLogos(item.name); + if (logoUrls) { + result.logo_urls = logoUrls; + } + const exchangeLogo = logos.getExchangeLogoUrl(item.exchange); + if (exchangeLogo) { + result.exchange_logo = exchangeLogo; + } + return result; } -exports.search = function (searchText, type, exchange, maxRecords) { +exports.search = function (searchString, type, exchange, maxRecords) { var MAX_SEARCH_RESULTS = !!maxRecords ? maxRecords : 50; - var results = []; - var queryIsEmpty = !searchText || searchText.length == 0; + var results = []; // array of WeightedItem { item, weight } + var queryIsEmpty = !searchString || searchString.length === 0; + var searchStringUpperCase = searchString.toUpperCase(); for (var i = 0; i < symbols.length; ++i) { var item = symbols[i]; + if (type && type.length > 0 && item.type != type) { continue; } if (exchange && exchange.length > 0 && item.exchange != exchange) { continue; } - if (queryIsEmpty || item.name.indexOf(searchText) == 0) { - results.push(searchResultFromDatabaseItem(item)); - } - if (results.length >= MAX_SEARCH_RESULTS) { - break; + + var positionInName = item.name.toUpperCase().indexOf(searchStringUpperCase); + var positionInDescription = item.description.toUpperCase().indexOf(searchStringUpperCase); + + if (queryIsEmpty || positionInName >= 0 || positionInDescription >= 0) { + var found = false; + for (var resultIndex = 0; resultIndex < results.length; resultIndex++) { + if (results[resultIndex].item == item) { + found = true; + break; + } + } + if (!found) { + var weight = positionInName >= 0 ? positionInName : 8000 + positionInDescription; + results.push({ item: item, weight: weight }); + } } } - return results; -} + return results + .sort(function (weightedItem1, weightedItem2) { return weightedItem1.weight - weightedItem2.weight; }) + .map(function (weightedItem) { return searchResultFromDatabaseItem(weightedItem.item); }) + .slice(0, Math.min(results.length, MAX_SEARCH_RESULTS)); +}; + + +exports.addSymbols = function(newSymbols) { + symbols = symbols.concat(newSymbols); +}; exports.symbolInfo = function (symbolName) { @@ -204,10 +204,10 @@ exports.symbolInfo = function (symbolName) { for (var i = 0; i < symbols.length; ++i) { var item = symbols[i]; - if (item.name.toUpperCase() == symbol && (exchange.length == 0 || exchange == item.exchange.toUpperCase())) { + if (item.name.toUpperCase() == symbol && (exchange.length === 0 || exchange == item.exchange.toUpperCase())) { return item; } } return null; -} +}; diff --git a/yahoo.js b/yahoo.js index 5784af7..8ffd2b7 100644 --- a/yahoo.js +++ b/yahoo.js @@ -1,365 +1,23 @@ /* This file is a node.js module. - This is a sample implementation of UDF-compatible datafeed wrapper for yahoo.finance. - Some algorithms may be icorrect because it's rather an UDF implementation sample + This is a sample implementation of UDF-compatible datafeed wrapper for Quandl (historical data) and yahoo.finance (quotes). + Some algorithms may be incorrect because it's rather an UDF implementation sample then a proper datafeed implementation. */ -var http = require("http"), - url = require("url"), - symbolsDatabase = require("./symbols_database"); - -var datafeedHost = "chartapi.finance.yahoo.com"; -var defaultResponseHeader = {"Content-Type": "text/plain", 'Access-Control-Allow-Origin': '*'}; - - -function httpGet(path, callback) -{ - var options = { - host: datafeedHost, - path: path - }; - - onDataCallback = function(response) { - var result = ''; - - response.on('data', function (chunk) { - result += chunk - }); - - response.on('end', function () { - callback(result) - }); - } - - http.request(options, onDataCallback).end(); -} - - -function convertYahooHistoryToUDFFormat(data) { - - // input: string "yyyy-mm-dd" (UTC) - // output: milliseconds from 01.01.1970 00:00:00.000 UTC - function parseDate(input) { - var parts = input.split('-'); - return Date.UTC(parts[0], parts[1]-1, parts[2]); - } - - var result = { - t: [], c: [], o: [], h: [], l: [], v: [], - s: "ok" - }; - - var lines = data.split('\n'); - - for (var i = lines.length - 2; i > 0; --i) { - var items = lines[i].split(","); - - var time = parseDate(items[0]) / 1000; - - result.t.push(time); - result.o.push(parseFloat(items[1])); - result.h.push(parseFloat(items[2])); - result.l.push(parseFloat(items[3])); - result.c.push(parseFloat(items[4])); - result.v.push(parseFloat(items[5])); - } - - return result; -} - -function convertYahooQuotesToUDFFormat(tickersMap, data) { - if (!data.query || !data.query.results) { - var errmsg = "ERROR: empty quotes response: " + JSON.stringify(data); - console.log(errmsg); - return { s: "error", errmsg: errmsg }; - } - - var result = { s: "ok", d: [] }; - [].concat(data.query.results.quote).forEach(function(quote) { - var ticker = tickersMap[quote.symbol]; - - // this field is an error token - if (quote["ErrorIndicationreturnedforsymbolchangedinvalid"]) { - result.d.push({ s: "error", n: ticker, v: {} }); - return; - } - - result.d.push({ - s: "ok", - n: ticker, - v: { - ch: quote.ChangeRealtime || quote.Change, - chp: (quote.PercentChange || quote.ChangeinPercent).replace(/[+-]?(.*)%/, "$1"), - - short_name: quote.Symbol, - exchange: quote.StockExchange, - original_name: quote.StockExchange + ":" + quote.Symbol, - description: quote.Name, - - lp: quote.LastTradePriceOnly, - ask: quote.AskRealtime, - bid: quote.BidRealtime, - - open_price: quote.Open, - high_price: quote.DaysHigh, - low_price: quote.DaysLow, - prev_close_price: quote.PreviousClose, - volume: quote.Volume, - } - }); - }); - return result; -} - -RequestProcessor = function(action, query, response) { - - this.sendError = function(error, response) { - response.writeHead(200, defaultResponseHeader); - response.write("{\"s\":\"error\",\"errmsg\":\"" + error + "\"}"); - response.end(); - - console.log(error); - } - - - this.sendConfig = function(response) { - - var config = { - supports_search: true, - supports_group_request: false, - supports_marks: true, - exchanges: [ - {value: "", name: "All Exchanges", desc: ""}, - {value: "XETRA", name: "XETRA", desc: "XETRA"}, - {value: "NSE", name: "NSE", desc: "NSE"}, - {value: "NasdaqNM", name: "NasdaqNM", desc: "NasdaqNM"}, - {value: "NYSE", name: "NYSE", desc: "NYSE"}, - {value: "CDNX", name: "CDNX", desc: "CDNX"}, - {value: "Stuttgart", name: "Stuttgart", desc: "Stuttgart"}, - ], - symbolsTypes: [ - {name: "All types", value: ""}, - {name: "Stock", value: "stock"}, - {name: "Index", value: "index"} - ], - supportedResolutions: [ "D", "2D", "3D", "W", "3W", "M", '6M' ] - }; - - response.writeHead(200, defaultResponseHeader); - response.write(JSON.stringify(config)); - response.end(); - } - - - this.sendMarks = function(response) { - var now = new Date().valueOf() / 1000; - var day = 60 * 60 * 24; - - var marks = { - id: [0, 1, 2, 3, 4, 5], - time: [now, now - day * 4, now - day * 7, now - day * 7, now - day * 15, now - day * 30], - color: ["red", "blue", "green", "red", "blue", "green"], - text: ["Today", "4 days back", "7 days back + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "7 days back once again", "15 days back", "30 days back"], - label: ["A", "B", "CORE", "D", "EURO", "F"], - labelFontColor: ["white", "white", "red", "#FFFFFF", "white", "#000"], - minSize: [14, 28, 7, 40, 7, 14] - }; - - response.writeHead(200, defaultResponseHeader); - response.write(JSON.stringify(marks)); - response.end(); - } +/* global require */ +/* global console */ +/* global process */ +"use strict"; - this.sendSymbolSearchResults = function(query, type, exchange, maxRecords, response) { - if (!maxRecords) { - throw "wrong_query"; - } - - var result = symbolsDatabase.search(query, type, exchange, maxRecords); - - response.writeHead(200, defaultResponseHeader); - response.write(JSON.stringify(result)); - response.end(); - } - - - this._pendingRequestType = ""; - this._lastYahooResponse = null; - - this.finance_charts_json_callback = function(data) { - if (_pendingRequestType == "data") { - _lastYahooResponse = data.series; - } - else if (_pendingRequestType == "meta") { - _lastYahooResponse = data.meta; - } - } - - - this.sendSymbolInfo = function(symbolName, response) { - var symbolInfo = symbolsDatabase.symbolInfo(symbolName); - - if (symbolInfo == null) { - throw "unknown_symbol " + symbolName; - } - - var address = "/instrument/1.0/" + encodeURIComponent(symbolInfo.name) + "/chartdata;type=quote;/json"; - var that = this; - - console.log(datafeedHost + address); - - httpGet(address, function(result) { - _pendingRequestType = "meta"; - - try { - with (that) { - eval(result); - } - } - catch (error) { - that.sendError("invalid symbol", response); - return; - } - - var lastPrice = _lastYahooResponse["previous_close"] + ""; - - // BEWARE: this `pricescale` parameter computation algorithm is wrong and works - // for symbols with 10-based minimal movement value only - var pricescale = lastPrice.indexOf('.') > 0 - ? Math.pow(10, lastPrice.split('.')[1].length) - : 10; - - var info = { - "name": symbolInfo.name, - "exchange-traded": symbolInfo.exchange, - "exchange-listed": symbolInfo.exchange, - "timezone": "America/New_York", - "minmov": 1, - "minmov2": 0, - "pricescale": pricescale, - "pointvalue": 1, - "timezone": "UTC", - "session": "0930-1630", - "has_intraday": false, - "has_no_volume": symbolInfo.type != "stock", - "ticker": _lastYahooResponse["ticker"].toUpperCase(), - "description": symbolInfo.description.length > 0 ? symbolInfo.description : symbolInfo.name, - "type": symbolInfo.type - }; - - response.writeHead(200, defaultResponseHeader); - response.write(JSON.stringify(info)); - response.end(); - }); - } - - - this.sendSymbolHistory = function(symbol, startDateTimestamp, resolution, response) { - - var symbolInfo = symbolsDatabase.symbolInfo(symbol); - - if (symbolInfo == null) { - throw "unknown_symbol"; - } - - var requestLeftDate = new Date(startDateTimestamp * 1000); - console.log(requestLeftDate); - - var year = requestLeftDate.getFullYear(); - var month = requestLeftDate.getMonth(); - var day = requestLeftDate.getDate(); - - if (resolution != "d" && resolution != "w" && resolution != "m") { - throw "Unsupported resolution: " + resolution; - } - - var address = "ichart.finance.yahoo.com/table.csv?s=" + symbolInfo.name + - "&a=" + month + - "&b=" + day + - "&c=" + year + - "&g=" + resolution + - "&ignore=.csv"; - - console.log("Requesting " + address); - - var that = this; - - httpGet(address, function(result) { - response.writeHead(200, defaultResponseHeader); - response.write(JSON.stringify(convertYahooHistoryToUDFFormat(result))); - response.end(); - }); - } - - this.sendQuotes = function(tickersString, response) { - var tickersMap = {}; // maps YQL symbol to ticker - - var tickers = tickersString.split(","); - [].concat(tickers).forEach(function(ticker) { - var yqlSymbol = ticker.replace(/.*:(.*)/, "$1"); - tickersMap[yqlSymbol] = ticker; - }); - - var yql = "select * from yahoo.finance.quotes where symbol in ('" + Object.keys(tickersMap).join("','") + "')"; - console.log("Quotes query: " + yql); - - var options = { - host: "query.yahooapis.com", - path: "/v1/public/yql?q=" + encodeURIComponent(yql) - + "&format=json" - + "&env=store://datatables.org/alltableswithkeys", - }; - // for debug purposes - // console.log(options.host + options.path); - - http.request(options, function(res) { - var result = ''; - - res.on('data', function (chunk) { - result += chunk; - }); - - res.on('end', function () { - response.writeHead(200, defaultResponseHeader); - response.write(JSON.stringify(convertYahooQuotesToUDFFormat( - tickersMap, JSON.parse(result)))); - response.end(); - }); - }).end(); - } - - try - { - if (action == "/config") { - this.sendConfig(response); - } - else if (action == "/symbols" && !!query["symbol"]) { - this.sendSymbolInfo(query["symbol"], response); - } - else if (action == "/search") { - this.sendSymbolSearchResults(query["query"], query["type"], query["exchange"], query["limit"], response); - } - else if (action == "/history") { - this.sendSymbolHistory(query["symbol"], query["from"], query["resolution"].toLowerCase(), response); - } - else if (action == "/quotes") { - this.sendQuotes(query["symbols"], response); - } - else if (action == "/marks") { - this.sendMarks(response); - } - else { - throw "wrong_request_format"; - } - } - catch (error) { - this.sendError(error, response) - } -} +var http = require("http"), + url = require("url"), + symbolsDatabase = require("./symbols_database"), + RequestProcessor = require("./request-processor").RequestProcessor; +var requestProcessor = new RequestProcessor(symbolsDatabase); // Usage: // /config @@ -367,7 +25,7 @@ RequestProcessor = function(action, query, response) { // /search?query=B&limit=10 // /history?symbol=C&from=DATE&resolution=E -var firstPort = 8888; +var firstPort = process.env.YAHOO_PORT || 8888; function getFreePort(callback) { var port = firstPort; firstPort++; @@ -390,7 +48,7 @@ getFreePort(function(port) { http.createServer(function(request, response) { var uri = url.parse(request.url, true); var action = uri.pathname; - new RequestProcessor(action, uri.query, response); + return requestProcessor.processRequest(action, uri.query, response); }).listen(port);