-- More information about this indicator can be found at:
-- http://fxcodebase.com/code/viewtopic.php?f=17&t=1972
--+------------------------------------------------------------------+
--| Copyright © 2018, Gehtsoft USA LLC |
--| http://fxcodebase.com |
--+------------------------------------------------------------------+
--| Developed by : Mario Jemic |
--| mario.jemic@gmail.com |
--+------------------------------------------------------------------+
--| Support our efforts by donating |
--| Paypal: https://goo.gl/9Rj74e |
--+------------------------------------------------------------------+
--| Patreon : https://goo.gl/GdXWeN |
--| BitCoin : 15VCJTLaz12Amr7adHSBtL9v8XomURo9RF |
--| BitCoin Cash: 1BEtS465S3Su438Kc58h2sqvVvHK9Mijtg |
--| Ethereum : 0x8C110cD61538fb6d7A2B47858F0c0AaBd663068D |
--| LiteCoin : LLU8PSY2vsq7B9kRELLZQcKf5nJQrdeqwD |
--+------------------------------------------------------------------+
function Init()
indicator:name("DailyFX News");
indicator:description("The indicator shows chosen dailyfx calendar events")
indicator:requiredSource(core.Bar);
indicator:type(core.Indicator);
indicator.parameters:addGroup("Calculation");
indicator.parameters:addDate("NewsFrom", "News from", "", -60);
indicator.parameters:addString("Instruments", "Instruments", "", "All");
indicator.parameters:addStringAlternative("Instruments", "All", "", "All");
indicator.parameters:addStringAlternative("Instruments", "Current", "", "Current");
indicator.parameters:addStringAlternative("Instruments", "List", "", "List");
indicator.parameters:addString("List", "Instruments List", "Used when Instruments is set to List. Specify currencies in a comma separated list, i.e., EUR,USD,AUD etc", "");
indicator.parameters:addString("IMP", "Importance of the new to show", "", "ALL");
indicator.parameters:addStringAlternative("IMP", "All news", "", "ALL");
indicator.parameters:addStringAlternative("IMP", "Medium or above", "", "MED");
indicator.parameters:addStringAlternative("IMP", "High only", "", "HIGH");
indicator.parameters:addString("NM", "Meaning of Negative", "", "LTZ");
indicator.parameters:addStringAlternative("NM", "Less than zero", "", "LTZ");
indicator.parameters:addStringAlternative("NM", "Less than forecast/previous", "", "LTF");
indicator.parameters:addStringAlternative("NM", "Greater than forecast/previous", "", "GTF");
indicator.parameters:addBoolean("REFRESH", "Refresh news automatically", "", false);
indicator.parameters:addInteger("TIMEOUT", "Refresh news in the specified timeout (in minutes)", "", 5, 5, 60);
indicator.parameters:addGroup("Style");
indicator.parameters:addInteger("S", "Font size in points", "", 8, 6, 20);
indicator.parameters:addColor("clrN", "Negative news color", "", core.rgb(255, 0, 0));
indicator.parameters:addColor("clrP", "Positive/Neutral news color", "", core.rgb(0, 255, 0));
end
local source;
local newsN;
local newsP;
local loadingRequest;
local loading;
local loadingWeek;
local loadingYear;
local barSize;
local offset;
local extent;
local instr;
local barSizeInDays;
local IMP;
local DUMMY;
local refresh = false;
local notBefore = 40325; -- May, 26 2010, the oldest availabe news archive
local NM;
local instruments;
local instrumentsList;
local from;
function Prepare(onlyName)
local name = profile:id();
instance:name(name);
if onlyName then
return ;
end
source = instance.source;
barSize = source:barSize();
instr = source:instrument();
offset = core.host:execute("getTradingDayOffset");
IMP = instance.parameters.IMP;
NM = instance.parameters.NM;
instruments = instance.parameters.Instruments;
if (instruments == "List") then
instrumentsList = instance.parameters.List:split(",");
end
from = instance.parameters.NewsFrom;
-- calculate the size of the candle
local s, e;
s, e = core.getcandle(source:barSize(), core.now(), offset);
s = e - s; -- length of candle in days
if s > 1 then
assert(false, "1-day is the largest chart which can be used to get news");
end
barSizeInDays = s;
-- number of candles to extent for 1 week.
-- if source:isAlive() then
if true then
extent = math.floor(7 / s);
if extent > 300 then
extent = 300;
end
else
extent = 0;
end
newsN = instance:createTextOutput("N", "N", "Arial", instance.parameters.S, core.H_Center, core.V_Bottom, instance.parameters.clrN, extent);
newsP = instance:createTextOutput("P", "P", "Arial", instance.parameters.S, core.H_Center, core.V_Top, instance.parameters.clrP, extent);
loading = false;
DUMMY = instance:addInternalStream(0, 0);
http = core.makeHttpLoader();
core.host:execute("setTimer", 1, 1);
core.host:execute("addCommand", 2, "Refresh News", "");
if instance.parameters.REFRESH then
core.host:execute("setTimer", 3, instance.parameters.TIMEOUT * 60);
end
end
local gWeekData = {};
function getweek(date)
-- gets a week for the specified date
local t = core.dateToTable(date);
date = math.floor(date) - (t.wday - 1);
t = core.dateToTable(date);
return string.format("%04i/%02i%02i", t.year, t.month, t.day), t.year;
end
function includeCurrency(curr)
if instruments == "All" then
return true;
end
if instruments == "Current" then
return (string.find(instr, curr, 1, true) ~= nil);
end
for i,ccy in ipairs(instrumentsList) do
if (string.upper(ccy) == curr) then
return true;
end
end
return false;
end
local pattern_date = "(%d%d%d%d)-(%d%d)-(%d%d)";
local pattern_time = "(%d%d?):(%d%d)";
function parseHtml(response, year)
local weekData = {};
local idx = 1;
local pos = 1;
while true do
local tableAtrib, tableBody = response:match("
", pos);
if tableAtrib == nil or tableBody == nil then
break;
end
if tableAtrib:match ('id="daily.cal%d*') ~= nil then
idx = parseTable(tableBody, year, weekData, idx)
end
pos = string.find(response, "", pos) + 1
end
weekData.last = idx - 1;
return weekData;
end
function parseTable(table, year, weekData, idx)
local bodyTag = table:match("(.-)");
for trAtrib, trBody in string.gmatch(bodyTag, "(.-)
") do
local row = {};
row.category = trAtrib:match('data.category="(%w+)"');
row.importance = trAtrib:match('data.importance="(%w+)"');
if row.category ~= nil and row.importance ~= nil then
parseRow(trAtrib, trBody, row)
if IMP == "ALL" or (IMP == "MED" and row.importance == "medium" or row.importance == "high" or
IMP == "HIGH" and row.importance == "high") then
if includeCurrency(string.upper(row.category)) then
local news = {};
news.instrument = row.category;
news.orgtime = row.date .. " " .. row.time ;
local ttime = {};
ttime.year, ttime.month, ttime.day = row.date:match(pattern_date);
ttime.hour, ttime.min = row.time:match(pattern_time);
ttime.sec = 0;
news.gmt_time = core.tableToDate(ttime);
news.est_time = core.host:execute("convertTime", 2, 1, news.gmt_time);
ttime = core.dateToTable(core.host:execute("convertTime", 2, 4, news.gmt_time));
news.sdate = string.format("%02i/%02i %02i:%02i", ttime.month, ttime.day, ttime.hour, ttime.min);
-- get new's candle
local s, e;
s, e = core.getcandle(barSize, news.est_time, offset);
local n, e1;
news.orgcandles = s;
news.orgcandlee = e;
-- check whether candle is a nontraing candle
n, e1 = core.isnontrading(s, offset);
if n then
-- put the news to the first after-nontrading candle (2 day after the begin of the
-- non-trading period)
s, e = core.getcandle(barSize, e1 + 2, offset);
end
news.candles = s;
news.candlee = e;
news.subject = row.description;
if (NM == "LTZ") then
-- negative means less than zero.
local posneg = "";
if row.actual ~= "" then
posneg = row.actual;
elseif row.forecast ~= "" then
posneg = row.forecast;
end
news.neg = (string.find(posneg, "-", 1, true) ~= nil);
else
if row.actual ~= "" then
local actual = row.actual;
local pp = 0;
if row.forecast ~= "" then
pp = row.forecast;
elseif row.previous ~= "" then
pp = row.previous;
end
-- format numbers.
local actualValue = formatValue(row.actual);
local prevValue = formatValue(pp);
if (actualValue == nil or prevValue == nil) then
core.host:trace("Failed to parse " .. news.sdate .. " values: Act: " .. actual .. " Parsed: " .. tostring(actualValue) .. " Prev: " .. pp .. " Parsed: " .. tostring(prevValue));
news.neg = false;
else
if (NM == "LTF") then
-- negative means less than forecast
news.neg = actualValue < prevValue;
elseif (NM == "GTF") then
-- negative means greater than forecast
news.neg = actualValue > prevValue;
end
end
end
end
news.act = row.actual;
news.fore = row.forecast;
news.prev = row.previous;
news.imp = row.importance;
weekData[idx] = news;
idx = idx + 1;
end
end
end
end
return idx;
end
function parseRow(trAtrib, trBody, row)
local index = 1;
row.actual = "";
row.forecast = "";
row.previous = "";
row.description = "";
for tdAtrib, tdBody in string.gmatch(trBody, "(.-) | ") do
if index == 2 then
row.date = tdBody:match(('%d+-%d+-%d+'));
row.time = tdBody:match(('%d+:%d+:%d+'));
elseif index == 4 then
local desc = tdBody:match('
(.*)');
if desc ~= nil then
row.description = desc;
end
elseif index == 6 then
--local value = tdBody:match('(-?%d+.%d+)');
row.actual = tdBody;
elseif index == 7 then
row.forecast = tdBody;
elseif index == 8 then
row.previous = tdBody;
end
index = index + 1;
end
end
function formatValue(str)
if (str == nil or str == "") then
return 0;
end
local suffix = string.sub (str, -1);
if (suffix == " ") then
-- trim the string.
str = string.sub (str, 1, string.len(str) - 1);
return formatValue(str);
end
if (string.len(str) > 1) then
local prefix = string.sub (str, 1, 1);
local neg = false;
local count = 1;
while (count < 5 and tonumber(prefix) == nil) do
-- sometimes the first character is not a number, like a $ sign or something.
-- Actually, sometimes it is more than the first character like for Yen values.. example "¥80T"
-- so we need to loop around until we get a proper value.
if (prefix == "-") then
-- need to handle negative.
neg = true;
end
-- remove the prefix
str = string.sub (str, -(string.len(str) - 1));
-- try again.
prefix = string.sub (str, 1, 1);
count = count + 1;
end
if (neg) then
str = "-" .. str;
end
end
local number = tonumber(string.sub (str, 1, string.len(str) - 1));
if (number == nil) then
return tonumber(str);
elseif (suffix == "K" or suffix =="k") then
return number * 1000;
elseif (suffix == "M" or suffix =="m") then
return number * 1000000;
elseif (suffix == "B" or suffix =="b") then
return number * 1000000000;
elseif (suffix == "T" or suffix =="t") then
return number * 1000000000000;
elseif (suffix == "%") then
return number;
end
return tonumber(str);
end
function loadweek(week, year)
local url;
loadingRequest = http_lua.createRequest();
loadingRequest:setRequestHeader("Accept-Encoding:", "deflate");
loadingRequest:start("http://dailyfx.com/calendar?previous=true&week=" .. week);
loading = true;
core.host:execute("setStatus", "loading " .. week .. "...");
loadingWeek = week;
loadingYear = year;
return ;
end
function ProcessCandle(period, date)
local week, weekData, year;
week, year = getweek(date);
if gWeekData[week] == nil then
if date >= from and date >= notBefore then
if not(loading) then
loadweek(week, year);
DUMMY:setBookmark(1, period);
end
return ;
else
gWeekData[week] = {};
weekData = gWeekData[week];
weekData.last = 0;
end
else
weekData = gWeekData[week];
end
if weekData ~= nil then
local msgp = "";
local cntp = 0;
local msgn = "";
local cntn = 0;
for i = 1, weekData.last, 1 do
local data = weekData[i];
if (data.candles <= date and
data.candlee > date) or
(data.orgcandles <= date and
data.orgcandlee > date) then
if data.neg then
if cntn > 0 then
msgn = msgn .. "\013\010";
end
msgn = msgn .. data.sdate .. " " .. data.subject .. "(" .. data.imp .. ")";
if data.act ~= "" then
msgn = msgn .. " Act=" .. data.act;
end
if data.fore ~= "" then
msgn = msgn .. " For=" .. data.fore;
elseif data.prev ~= "" then
msgn = msgn .. " Prev=" .. data.prev;
end
cntn = cntn + 1;
else
if cntp > 0 then
msgp = msgp .. "\013\010";
end
msgp = msgp .. data.sdate .. " " .. data.subject .. "(" .. data.imp .. ")";
if data.act ~= "" then
msgp = msgp .. " Act=" .. data.act;
end
if data.fore ~= "" then
msgp = msgp .. " For=" .. data.fore;
elseif data.prev ~= "" then
msgp = msgp .. " Prev=" .. data.prev;
end
cntp = cntp + 1;
end
end
end
local pperiod;
if period >= source:size() then
pperiod = source:size() - 1;
else
pperiod = period;
end
if (cntn > 0) then
newsN:set(period, source.low[pperiod], "(" .. cntn .. ")", msgn);
end
if (cntp > 0) then
newsP:set(period, source.high[pperiod], "(" .. cntp .. ")", msgp);
end
end
end
function Update(period, mode)
if not(source:hasData(period)) then
return ;
end
ProcessCandle(period, source:date(period));
if extent > 0 and period == source:size() - 1 then
local i, ccandle;
ccandle = source:date(period);
for i = 1, extent - 1, 1 do
ccandle = ccandle + barSizeInDays;
ProcessCandle(period + i, ccandle);
end
end
end
function preprocessingHTML(data)
data = data:gsub("", "\n\r\n\r"); --fix the absence of a for the last row of the table.
data = data:gsub("\n\r
\n\r
return data;
end
function AsyncOperationFinished(cookie, success, message)
if cookie == 1 then
if loading then
if not(loadingRequest:loading()) then
if loadingRequest:httpStatus() == 200 then
local body = loadingRequest:response();
gWeekData[loadingWeek] = parseHtml(body, loadingYear);
else
gWeekData[loadingWeek] = nil;
end
loading = false;
core.host:execute("setStatus", "");
instance:updateFrom(math.max(0, DUMMY:getBookmark(1)));
return core.ASYNC_REDRAW;
end
end
return 0;
elseif cookie == 2 then
gWeekData = {};
instance:updateFrom(0);
return core.ASYNC_REDRAW;
elseif cookie == 3 then
if not(refresh) then
-- skip the first refresh tick
refresh = true;
return 0;
else
gWeekData = {};
instance:updateFrom(0);
return core.ASYNC_REDRAW;
end
end
return 0;
end
function ReleaseInstance()
if loading then
while (http:loading()) do
end
end
core.host:execute("killTimer", 1);
core.host:execute("killTimer", 3);
end
function string:split(sep)
local sep, fields = sep or ";", {}
local pattern = string.format("([^%s]+)", sep)
self:gsub(pattern, function(c) fields[#fields + 1] = c end)
return fields
end
require("http_lua");