Merge branch 'candlestick_calcs'

This commit is contained in:
2018-11-14 21:16:40 -08:00
14 changed files with 865 additions and 322 deletions

View File

@ -9,19 +9,25 @@
-- The time the trading logic will begin to enter trades.
-- Exiting trades is 24/7.
create constant variable int StartTimeHour = 9
create constant variable DateTime TradingStartTime = EPLHelpers.parseTime("00:00")
-- The time the trading logic will begin to enter trades.
-- The time the trading logic will no longer enter trades.
-- Exiting trades is 24/7.
create constant variable int EndTimeHour = 17
create constant variable DateTime TradingEndTime = EPLHelpers.parseTime("23:59")
-- The time frame for OHLC calculation.
-- Example values: '5s', '1m', '1h 30m', '2h', '12h', '1d'.
create constant variable string OHLCInterval = '10s'
-- The number of ticks for OHLC TB calculation.
create constant variable int OHLCTicks = 100
-- Amount to be traded, measured in units.
create constant variable int TradeSize = 10
-- How large is a pip?
create constant variable BigDecimal PipSize = new BigDecimal(0.0001)
-- How many events to use for simple moving average calculation
create constant variable int SMASize = 5
@ -35,11 +41,11 @@ create constant variable int RefSize = 5
-- Define the window as length 1, using the structure of the TickEvent
-- java class to describe what the window contains.
create window CurrentTickWindow#length(1) as TickEvent
create window CurrentTick#length(1) as TickEvent
-- Describe how events get added to the window. This runs every time
-- a new TickEvent is posted.
insert into CurrentTickWindow select * from TickEvent
insert into CurrentTick select * from TickEvent
--
@ -47,47 +53,85 @@ insert into CurrentTickWindow select * from TickEvent
--
-- InTradingHours will be set to true when the current time is between
-- StartTime and EndTime.
-- TradingStartTime and TradingEndTime.
create variable bool InTradingHours = false
-- Update on each tick
-- NOTE: see "timer:within" pattern for possible alternate formulation
on TickEvent as t set InTradingHours =
(EPLHelpers.getHour(t.time) >= StartTimeHour and
EPLHelpers.getHour(t.time) < EndTimeHour)
on TickEvent as t
set InTradingHours = EPLHelpers.inTimeRange(t.time, TradingStartTime, TradingEndTime)
--
-- In long position. Set later.
--
create variable bool InLongEntry = false
--
-- A stream of OHLC values calculated from TickEvents
--
-- Create the stream to contain OHLCEvents
create variant schema OHLCStream as OHLCEvent
-- Send every TickEvent to the OHLC plugin. The plugin will post an
-- OHLCEvent to OHLCStream every OHLCInterval amount of time. It uses
-- TickEvent.time ("time") as the source of the timestamp, and uses
-- TickEvent.midDouble() as the value to use in the OHLC calculation.
create variant schema OHLCStream as OHLCEvent
insert into OHLCStream
select * from TickEvent#OHLC(OHLCInterval, time, midDouble)
select * from TickEvent#OHLC("OHLC", "time", OHLCInterval, time, midDouble)
-- Send every TickEvent to the OHLC plugin. The plugin will post an
-- OHLCEvent using Heikin-Ashi calculations to HKStream every
-- OHLCInterval amount of time. It uses TickEvent.midDouble() as the
-- value to use in the OHLC calculation.
create variant schema HKStream as OHLCEvent
insert into HKStream
select * from TickEvent#OHLC("HeikinAshi", "time", OHLCInterval, time, midDouble)
-- Send every TickEvent to the OHLC plugin. The plugin will post an
-- OHLCEvent to TBStream every OHLCTicks ticks. It uses
-- TickEvent.time ("time") as the source of the timestamp, and uses
-- TickEvent.midDouble() as the value to use in the OHLC calculation.
create variant schema TBStream as OHLCEvent
insert into TBStream
select * from TickEvent#OHLC("HeikinAshi", "ticks", OHLCTicks, time, midDouble)
-- Send every TickEvent to the OHLC plugin. The plugin will post an
-- OHLCEvent using Heikin-Ashi calculations to TBHKStream every
-- OHLCTicks ticks. It uses TickEvent.midDouble() as the value to use
-- in the OHLC calculation.
create variant schema TBHKStream as OHLCEvent
insert into TBHKStream
select * from TickEvent#OHLC("HeikinAshi", "ticks", OHLCTicks, time, midDouble)
--
-- Simple moving average streams
--
-- SMACloseStream contains OHLCValueEvents. These are like
-- OHLCEvents, but add an extra field for an arbitrary value. In this
-- stream, that extra value will contain the average of OHLC close
-- values.
create schema SMACloseStream as ats.plugin.OHLCValueEvent
-- SMACloseStream bundles a simple moving average of the close values
-- with the values of the OHLCStream.
create schema SMACloseStream as (time DateTime,
open double,
high double,
low double,
close double,
averageClose double)
-- Average the most recent OHLC close values from OHLCStream and
-- post an event that contains open, high, low, close, and
-- SMA(close). The number of OHLC events used in the SMA calc is set
-- by the SMASize variable.
insert into SMACloseStream
select new ats.plugin.OHLCValueEvent(time, open, high, low, close, Avg(close))
select time, open, high, low, close, Avg(close) as averageClose
from OHLCStream#length(SMASize)
@ -98,21 +142,19 @@ insert into SMACloseStream
-- A stream that feeds B1 and B2. Each event contains a double
-- precision floating point value "low", and a timestamp called
-- "time".
create schema BStream as (low double, time org.joda.time.DateTime)
create schema BStream as (low double, time DateTime)
-- Listen to the last "RefSize" number of SMACloseStream events. Look
-- for an OHLC bar with a lower average close than its neighbors. Add
-- that bar's low value and its timestamp to the stream. As described
-- in SMACloseStream, "value" in the query below represents
-- SMA(close).
-- that bar's low value and its timestamp to the stream.
insert into BStream
select prev(1, low) as low, prev(1, time) as time from SMACloseStream#length(RefSize)
where prev(0, value) > prev(1, value)
and prev(1, value) < prev(2, value)
where prev(0, averageClose) > prev(1, averageClose)
and prev(1, averageClose) < prev(2, averageClose)
-- Define B1 to contain the same fields as BStream
create schema B1 (low double, time org.joda.time.DateTime)
create schema B1 (low double, time DateTime)
-- B1 contains the most recent low value and time from BStream.
-- This is the last time an average close was lower
@ -123,33 +165,33 @@ insert into B1 select prev(0, low) as low, prev(0, time) as time
-- B2 contains the *second* most recent occurrence in BStream, but is
-- otherwise the same as B1.
create schema B2 (low double, time org.joda.time.DateTime)
create schema B2 (low double, time DateTime)
insert into B2 select prev(1, low) as low, prev(1, time) as time
from BStream#length(RefSize)
-- A stream that feeds P1 and P2.
create schema PStream as (low double, time org.joda.time.DateTime)
create schema PStream as (low double, time DateTime)
-- Find an OHLC bar with a higher average close than its neighbors.
-- Add that low value and its timestamp to the stream.
insert into PStream
select prev(1, low) as low, prev(1, time) as time from SMACloseStream#length(RefSize)
where prev(0, value) < prev(1, value)
and prev(1, value) > prev(2, value)
where prev(0, averageClose) < prev(1, averageClose)
and prev(1, averageClose) > prev(2, averageClose)
-- P1 contains the most recent low value and time from PStream.
-- This is the last time an average close was higher
-- than the ones before and after.
-- Since the time is included in the event, no separate PT1 is needed.
create schema P1 (low double, time org.joda.time.DateTime)
create schema P1 (low double, time DateTime)
insert into P1 select prev(0, low) as low, prev(0, time) as time
from PStream#length(RefSize)
-- P2 contains the second most recent occurrence in PStream.
create schema P2 (low double, time org.joda.time.DateTime)
create schema P2 (low double, time DateTime)
insert into P2 select prev(1, low) as low, prev(1, time) as time
from PStream#length(RefSize)
@ -168,9 +210,9 @@ insert into MaxHigh3Window
select max(high) as high from OHLCStream#length(3)
-- Long entry events contain the current tick's midpoint value and
-- timestamp.
create schema LongEntryStream as (current BigDecimal, time org.joda.time.DateTime, instrument String)
-- Long entry events contain the current tick's midpoint value,
-- timestamp, and instrument name.
create schema LongEntryStream as (current BigDecimal, time DateTime, instrument String)
-- The long entry calc below is translated from this entry in the
-- spreadsheet:
@ -184,7 +226,7 @@ create schema LongEntryStream as (current BigDecimal, time org.joda.time.DateTim
insert into LongEntryStream
select C.mid as current, C.time as time, C.instrument as instrument
from CurrentTickWindow as C,
from CurrentTick as C,
MaxHigh3Window as T,
B1#lastevent, B2#lastevent,
P1#lastevent, P2#lastevent
@ -194,8 +236,10 @@ insert into LongEntryStream
and EPLHelpers.laterThan(B1.time, P1.time)
and EPLHelpers.laterThan(B2.time, P2.time)
and EPLHelpers.laterThan(P1.time, B2.time)
and InTradingHours
and not InLongEntry
-- Because multiple streams feed LongEntryStream (CurrentTickWindow,
-- Because multiple streams feed LongEntryStream (CurrentTick,
-- MaxHigh3Window, B1, B2...), an event on any of those streams causes
-- the LongEntryStream logic above to be triggered. This often causes
-- multiple LongEntryStream events to be generated for a single tick
@ -203,14 +247,101 @@ insert into LongEntryStream
--
-- LongEntryDistinct filters out duplicate LongEntryStream events,
-- leaving a maximum of one event per tick.
create schema LongEntryDistinct as (current BigDecimal, time org.joda.time.DateTime,
instrument String, units int)
create schema LongEntryDistinct
as (current BigDecimal,
time DateTime,
instrument String,
units int)
insert into LongEntryDistinct
select le.current as current, le.time as time,
le.instrument as instrument, TradeSize as units
from pattern [every-distinct(le.time) le=LongEntryStream]
--
-- Long entry derived values
--
-- Register if We're in a long entry when we get the LongEntryDistinct
-- event. (InLongEntry is declared near top of file)
on LongEntryDistinct as le set InLongEntry = true
-- LongEntryTime contains the timestamp of the last long entry,
-- or null if none has happened yet.
create variable DateTime LongEntryTime = null
on LongEntryDistinct as le set LongEntryTime = le.time
-- LongEntryStopBarCount contains the number of OHLCStream bars that
-- have occurred since the most recent long entry.
create variable int LongEntryStopBarCount = 0
-- Reset LongEntryStopBarCount when we get a new LE.
on LongEntryDistinct as le set LongEntryStopBarCount = 0
-- Increment LongEntryStopBarCount on every bar.
on OHLCStream as ohlc set LongEntryStopBarCount = LongEntryStopBarCount + 1
-- LongEntryPrice contains the price of the last long entry,
-- or null if none has happened yet.
create variable BigDecimal LongEntryPrice = null
on LongEntryDistinct as le set LongEntryPrice = le.current
-- LongEntryPreEntryBar is a stream that keeps bars just prior to the
-- last long entry
create schema LongEntryPreEntryBar
as (time DateTime,
open double,
high double,
low double,
close double)
-- Add bars so long as we're not in a long position
insert into LongEntryPreEntryBar
select time, open, high, low, close from OHLCStream
where InLongEntry is false
-- Contains the second-most-recent LongEntryPreEntryBar's "low" value
create schema LongEntryPreEntryBarPrevLow (low double)
insert into LongEntryPreEntryBarPrevLow select prev(1, low) as low
from LongEntryPreEntryBar#length(2)
--
-- Long exit
--
-- Long exit event definition
create schema LongExitStream
as (current BigDecimal,
time DateTime,
instrument String,
units int)
-- The long exit calc below is translated from this entry in the
-- spreadsheet:
--
-- LX = (LESBC <= 60 and C < MIN(PreEntrybar(Low,1), (LongEntryPrice - 10 pips)))
-- or
-- (LESBC>60 and C<(EntryPrice + 2 pips)
insert into LongExitStream
select C.mid as current,
C.time as time,
C.instrument as instrument,
0 - TradeSize as units -- negative for "sell"
from CurrentTick as C,
LongEntryPreEntryBarPrevLow#lastevent as L
where ((LongEntryStopBarCount <= 60 and C.mid < min(L.low, LongEntryPrice - (10 * PipSize))) or
(LongEntryStopBarCount > 60 and C.mid < LongEntryPrice + (2 * PipSize)))
and InLongEntry
-- Register the long position as closed
on LongExitStream as lx set InLongEntry = false
--
-- Event logging
@ -224,27 +355,51 @@ create schema LogStream as (stream string, event string)
-- of these can be either helpful or too noisy. Comment/uncomment as
-- you see fit.
insert into LogStream select 'TickEvent' as stream, EPLHelpers.str(*) as event from TickEvent
-- insert into LogStream select 'TickEvent' as stream, EPLHelpers.str(*) as event from TickEvent
insert into LogStream select 'OHLCStream' as stream, EPLHelpers.str(*) as event from OHLCStream
-- insert into LogStream select 'OHLCStream' as stream, EPLHelpers.str(*) as event from OHLCStream
-- insert into LogStream select 'HKStream' as stream, EPLHelpers.str(*) as event from HKStream
-- insert into LogStream select 'TBStream' as stream, EPLHelpers.str(*) as event from TBStream
-- insert into LogStream select 'TBHKStream' as stream, EPLHelpers.str(*) as event from TBHKStream
-- insert into LogStream select 'InTradingHours' as stream, EPLHelpers.str(time, InTradingHours) as event from TickEvent
-- insert into LogStream select 'BStream' as stream, EPLHelpers.str(*) as event from BStream
-- insert into LogStream select 'PStream' as stream, EPLHelpers.str(*) as event from PStream
insert into LogStream select 'B1' as stream, EPLHelpers.str(*) as event from B1
-- insert into LogStream select 'B1' as stream, EPLHelpers.str(*) as event from B1
insert into LogStream select 'B2' as stream, EPLHelpers.str(*) as event from B2
-- insert into LogStream select 'B2' as stream, EPLHelpers.str(*) as event from B2
insert into LogStream select 'P1' as stream, EPLHelpers.str(*) as event from P1
-- insert into LogStream select 'P1' as stream, EPLHelpers.str(*) as event from P1
insert into LogStream select 'P2' as stream, EPLHelpers.str(*) as event from P2
-- insert into LogStream select 'P2' as stream, EPLHelpers.str(*) as event from P2
-- insert into LogStream select 'MaxHigh3Window' as stream, EPLHelpers.str(*) as event from MaxHigh3Window
-- insert into LogStream select 'LongEntryStream' as stream, EPLHelpers.str(*) as event from LongEntryStream
-- insert into LogStream select 'LongEntryDistinct' as stream, EPLHelpers.str(*) as event from LongEntryDistinct
insert into LogStream select 'LongEntryDistinct' as stream, EPLHelpers.str(*) as event from LongEntryDistinct
-- insert into LogStream select 'InLongEntry' as stream, EPLHelpers.str(InLongEntry) as event from OHLCStream
-- insert into LogStream select 'LongEntryTime' as stream, EPLHelpers.str(LongEntryTime) as event from TickEvent
-- insert into LogStream select 'LongEntryPrice' as stream, EPLHelpers.str(LongEntryPrice) as event from OHLCStream
-- insert into LogStream select 'LongEntryStopBarCount' as stream, EPLHelpers.str(LongEntryStopBarCount) as event from OHLCStream where InLongEntry
-- insert into LogStream select 'LongEntryPreEntryBar' as stream, EPLHelpers.str(*) as event from LongEntryPreEntryBar
-- insert into LogStream select 'LongEntryPreEntryBarPrevLow' as stream, EPLHelpers.str(*) as event from LongEntryPreEntryBarPrevLow
-- insert into LogStream select 'LXPrice' as stream, EPLHelpers.str(mid, LongEntryPrice, LongEntryPrice - (10 * PipSize)) as event from TickEvent
-- where InLongEntry and mid < LongEntryPrice
insert into LogStream select 'LongExitStream' as stream, EPLHelpers.str(*) as event from LongExitStream
-- TODO (for Seth): look into LogSink http://esper.espertech.com/release-7.1.0/esper-reference/html/dataflow.html#dataflow-reference-logsink

View File

@ -1,27 +1,54 @@
import java.util.StringJoiner;
import org.joda.time.DateTime;
import org.joda.time.DateTimeComparator;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.joda.time.DateTimeComparator;
/**
* EPLHelpers contains small helper routines used within epl files.
*/
public class EPLHelpers {
final static Logger log = LoggerFactory.getLogger(EPLHelpers.class);
private static DateTimeFormatter timeFormatter = DateTimeFormat.forPattern("HH:mm");
private static DateTimeComparator timeComparator = DateTimeComparator.getTimeOnlyInstance();
/** Return the hour of the day for the given date. */
public static int getHour(DateTime date) {
return date.getHourOfDay();
/**
* A simple toString() wrapper.
*/
public static String str(Object... objects) {
StringJoiner sj = new StringJoiner(", ");
for (Object o : objects) {
sj.add(o != null ? o.toString() : "null");
}
return sj.toString();
}
/** A simple toString() wrapper for use in epl. */
public static String str(Object o) { return o.toString(); }
/**
* Return a DateTime object for the time. Should be specified in
* 24-hour "hh:mm" format.
*/
public static DateTime parseTime(String time) {
return timeFormatter.parseDateTime(time);
}
/**
* Return true if the time portion of 'now' is between the time
* portion of 'start' and 'end'.
*/
public static boolean inTimeRange(DateTime now, DateTime start, DateTime end) {
return laterThan(now, start) && earlierThan(now, end);
}
/**
* Compare two times and return true if the first is earlier than
* the second.
*/
public static boolean earlierThan(DateTime a, DateTime b) {
return DateTimeComparator.getInstance().compare(a, b) < 0;
return timeComparator.compare(a, b) < 0;
}
/**
@ -29,6 +56,6 @@ public class EPLHelpers {
* the second.
*/
public static boolean laterThan(DateTime a, DateTime b) {
return DateTimeComparator.getInstance().compare(a, b) > 0;
return timeComparator.compare(a, b) > 0;
}
}

View File

@ -9,13 +9,13 @@ import com.espertech.esper.client.StatementAwareUpdateListener;
import com.espertech.esper.client.UpdateListener;
import com.espertech.esper.client.time.CurrentTimeEvent;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ats.orders.MarketOrderRequest;
import ats.plugin.OHLCEvent;
import ats.plugin.OHLCPlugInViewFactory;
import ats.plugin.OHLCValueEvent;
public class EsperProcessor implements TickProcessor {
@ -35,7 +35,7 @@ public class EsperProcessor implements TickProcessor {
// register event types defined in java classes
config.addEventType(TickEvent.class);
config.addEventType(OHLCEvent.class);
config.addEventType(OHLCValueEvent.class);
config.addEventType(DateTime.class);
// add OHLC plugin
config.addPlugInView("ATS", "OHLC", OHLCPlugInViewFactory.class.getName());
@ -61,6 +61,20 @@ public class EsperProcessor implements TickProcessor {
newData[0].get("current"),
newData[0].get("time"));
});
// respond to long exit events
addStatement("select * from LongExitStream",
(newData, oldData) -> {
String instrument = (String)newData[0].get("instrument");
Integer units = (Integer)newData[0].get("units");
trader.placeOrder(new MarketOrderRequest(instrument, units));
log.debug("Long exit triggered: {} of {} at price {} at time {}",
units,
instrument,
newData[0].get("current"),
newData[0].get("time"));
});
}
/**

View File

@ -0,0 +1,25 @@
package ats.plugin;
/**
* CandlestickCalc embodies a method of calculating candlestick
* values. For example, OHLC or Heikin-Ashi.
*/
interface CandlestickCalc {
public enum Type { OHLC, HeikinAshi };
/**
* Reset the calculation for the beginning of an interval.
*/
void reset();
/**
* Accept a current tick value to apply to the current
* calculations.
*/
void applyValue(double value);
/**
* Return the values calculated since the last reset point.
*/
OHLCValues getValues();
}

View File

@ -0,0 +1,17 @@
package ats.plugin;
import org.joda.time.DateTime;
/**
* A CandlestickWindow runs a CandlestickCalc across a number of
* ticks and sends
*/
interface CandlestickWindow {
public enum Type { time, ticks };
/**
* Process a tick with the given time and value.
*/
public void update(DateTime timestamp, double value);
}

View File

@ -0,0 +1,102 @@
package ats.plugin;
/**
* HeikinAshiCandlestickCalc calculates values using the Heikin-Ashi
* technique.
*
* @see <a href="https://stockcharts.com/school/doku.php?id=chart_school:chart_analysis:heikin_ashi">Heikin-Ashi description</a>
*/
class HeikinAshiCandlestickCalc implements CandlestickCalc {
private Double lastOpen;
private Double lastClose;
private Double currentOpen;
private Double currentClose;
private Double currentHigh;
private Double currentLow;
/**
* Reset the calculation for the beginning of an interval.
*/
@Override
public void reset() {
lastOpen = currentOpen;
lastClose = currentClose;
currentOpen = null;
currentClose = null;
currentHigh = null;
currentLow = null;
}
/**
* Accept a current tick value to apply to the current
* calculations.
*/
@Override
public void applyValue(double value) {
if (currentOpen == null) {
currentOpen = value;
}
currentClose = value;
if (currentLow == null) {
currentLow = value;
} else if (currentLow.compareTo(value) > 0) {
currentLow = value;
}
if (currentHigh == null) {
currentHigh = value;
} else if (currentHigh.compareTo(value) < 0) {
currentHigh = value;
}
}
/**
* HA-Open = (HA-Open(-1) + HA-Close(-1)) / 2
*/
private double calcOpen() {
double sum = 0;
sum += lastOpen != null ? lastOpen : currentOpen;
sum += lastClose != null ? lastClose : currentClose;
return sum / 2d;
}
/**
* HA-Close = (Open(0) + High(0) + Low(0) + Close(0)) / 4
*/
private double calcClose() {
return (currentOpen + currentHigh + currentLow + currentClose) / 4d;
}
/**
* HA-High = Maximum of the High(0), HA-Open(0) or HA-Close(0)
*/
private double calcHigh(double haOpen, double haClose) {
return Math.max(currentHigh, Math.max(haOpen, haClose));
}
/**
* HA-Low = Minimum of the Low(0), HA-Open(0) or HA-Close(0)
*/
private double calcLow(double haOpen, double haClose) {
return Math.min(currentLow, Math.min(haOpen, haClose));
}
/**
* Return our calculated values up to now.
*/
@Override
public OHLCValues getValues() {
double o = calcOpen();
double c = calcClose();
double h = calcHigh(o, c);
double l = calcLow(o, c);
return new OHLCValues(o, h, l, c);
}
}

View File

@ -0,0 +1,56 @@
package ats.plugin;
/**
* OHLCCandlestickCalc calculates values for OHLC.
*/
class OHLCCandlestickCalc implements CandlestickCalc {
private Double open;
private Double close;
private Double high;
private Double low;
/**
* Reset the calculation for the beginning of an interval.
*/
@Override
public void reset() {
open = null;
close = null;
high = null;
low = null;
}
/**
* Accept a current tick value to apply to the current
* calculations.
*/
@Override
public void applyValue(double value) {
if (open == null) {
open = value;
}
close = value;
if (low == null) {
low = value;
} else if (low.compareTo(value) > 0) {
low = value;
}
if (high == null) {
high = value;
} else if (high.compareTo(value) < 0) {
high = value;
}
}
/**
* Return our calculated values up to now.
*/
@Override
public OHLCValues getValues() {
return new OHLCValues(open, high, low, close);
}
}

View File

@ -13,6 +13,10 @@ public class OHLCEvent {
private double close;
public OHLCEvent(DateTime time, OHLCValues values) {
this(time, values.open, values.high, values.low, values.close);
}
public OHLCEvent(DateTime time,
double open, double high,
double low, double close)

View File

@ -1,92 +1,94 @@
package ats.plugin;
import java.util.Iterator;
import com.espertech.esper.client.EventBean;
import com.espertech.esper.client.EventType;
import com.espertech.esper.core.context.util.AgentInstanceViewFactoryChainContext;
import com.espertech.esper.core.service.EPStatementHandleCallback;
import com.espertech.esper.core.service.EngineLevelExtensionServicesContext;
import com.espertech.esper.epl.expression.core.ExprEvaluator;
import com.espertech.esper.epl.expression.core.ExprNode;
import com.espertech.esper.event.EventAdapterService;
import com.espertech.esper.schedule.ScheduleHandleCallback;
import com.espertech.esper.view.ViewSupport;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.joda.time.Period;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;
import com.espertech.esper.epl.expression.core.ExprEvaluator;
import com.espertech.esper.schedule.SchedulingService;
import org.joda.time.Duration;
import org.joda.time.format.ISODateTimeFormat;
import java.util.TimeZone;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Instant;
import org.joda.time.PeriodType;
/**
* OHLCPlugInView computes OHLC bars for a given time interval.
*/
public class OHLCPlugInView extends ViewSupport {
private static final Logger log = LoggerFactory.getLogger(OHLCPlugInView.class);
private static final int LATE_EVENT_SLACK_SECONDS = 5;
private static final PeriodFormatter periodFormatter = new PeriodFormatterBuilder()
.appendDays().appendSuffix("d")
.appendHours().appendSuffix("h")
.appendMinutes().appendSuffix("m")
.appendSeconds().appendSuffix("s")
.toFormatter();
private final AgentInstanceViewFactoryChainContext agentContext;
private final long scheduleSlot;
private EPStatementHandleCallback handle;
private final Duration interval;
private final AgentInstanceViewFactoryChainContext context;
private final ExprNode timestampExpression;
private final ExprNode valueExpression;
private DateTime windowStartTime;
private DateTime windowEndTime;
private Double open;
private Double close;
private Double high;
private Double low;
private CandlestickWindow window;
private EventAdapterService service;
private EventBean[] lastData;
/**
* Create a new plugin.
*/
public OHLCPlugInView(AgentInstanceViewFactoryChainContext context,
ExprNode calcTypeExpression,
ExprNode timeTypeExpression,
ExprNode intervalExpression,
ExprNode timestampExpression,
ExprNode valueExpression)
{
agentContext = context;
scheduleSlot = context.getStatementContext().getScheduleBucket().allocateSlot();
interval = parseInterval(intervalExpression);
// log.info("Interval is {}", interval);
this.context = context;
this.timestampExpression = timestampExpression;
this.valueExpression = valueExpression;
service = context.getStatementContext().getEventAdapterService();
CandlestickCalc calc = chooseCalc(calcTypeExpression);
window = chooseWindow(context, timeTypeExpression, intervalExpression, calc);
}
/**
* Return the time period specified by the given expression value.
* Return the proper CandlestickCalc according to the value of the
* calc type parameter.
*/
private Duration parseInterval(ExprNode interval) {
ExprEvaluator evaluator = interval.getForge().getExprEvaluator();
String intervalStr = (String)evaluator.evaluate(null, true, agentContext);
return parseInterval(intervalStr);
private CandlestickCalc chooseCalc(ExprNode calcTypeExpression) {
String calcType = exprNodeValue(calcTypeExpression);
if (CandlestickCalc.Type.valueOf(calcType) == CandlestickCalc.Type.HeikinAshi) {
log.info("Using Heikin-Ashi calc type");
return new HeikinAshiCandlestickCalc();
}
log.info("Using OHLC calc type");
return new OHLCCandlestickCalc();
}
/**
* Return the time period specified by the given string.
* Return the proper window according to the value of the
* calc type parameter.
* @param intervalExpression
*/
public static Duration parseInterval(String interval) {
interval = interval.replaceAll("\\s+","");
return periodFormatter.parsePeriod(interval).toStandardDuration();
private CandlestickWindow chooseWindow(AgentInstanceViewFactoryChainContext context,
ExprNode timeTypeExpression,
ExprNode intervalExpression,
CandlestickCalc calc)
{
String timeType = exprNodeValue(timeTypeExpression);
if (CandlestickWindow.Type.valueOf(timeType) == CandlestickWindow.Type.time) {
log.info("Using time based window type");
return new TimeCandlestickWindow(this, calc, context, intervalExpression);
}
log.info("Using ticks based window type");
return new TicksCandlestickWindow(this, calc, context, intervalExpression);
}
/**
* Return the string value of a node.
*/
private String exprNodeValue(ExprNode node) {
ExprEvaluator evaluator = node.getForge().getExprEvaluator();
return (String)evaluator.evaluate(null, true, context);
}
/**
@ -94,15 +96,7 @@ public class OHLCPlugInView extends ViewSupport {
*/
private DateTime getTimestamp(EventBean event) {
ExprEvaluator evaluator = timestampExpression.getForge().getExprEvaluator();
return (DateTime)evaluator.evaluate(new EventBean[] {event}, true, agentContext);
}
/**
* Convert a bare long value to a proper DateTime entity. Assumes
* UTC time zone.
*/
public static DateTime toDateTime(long l) {
return new DateTime(l, DateTimeZone.UTC);
return (DateTime)evaluator.evaluate(new EventBean[] {event}, true, context);
}
/**
@ -110,7 +104,7 @@ public class OHLCPlugInView extends ViewSupport {
*/
private double getValue(EventBean event) {
ExprEvaluator evaluator = valueExpression.getForge().getExprEvaluator();
return (double)evaluator.evaluate(new EventBean[] {event}, true, agentContext);
return (double)evaluator.evaluate(new EventBean[] {event}, true, context);
}
/**
@ -123,150 +117,17 @@ public class OHLCPlugInView extends ViewSupport {
for (EventBean event : newData) {
DateTime timestamp = getTimestamp(event);
double value = getValue(event);
ensureWindow(timestamp);
applyValue(value);
window.update(timestamp, value);
}
}
/**
* Make sure our window times are set up and current.
* Send an event to all plugin listeners.
*/
private void ensureWindow(DateTime timestamp) {
if (timestamp == null) return;
if (windowStartTime == null) {
// create open window
windowStartTime = makeWindowStartTime(timestamp, interval);
windowEndTime = makeWindowEndTime(windowStartTime, interval);
scheduleCallback(windowEndTime);
}
if (!inWindow(timestamp)) {
// past current window.
// post and create a new one.
postData();
windowStartTime = makeWindowStartTime(timestamp, interval);
windowEndTime = makeWindowEndTime(windowStartTime, interval);
scheduleCallback(windowEndTime);
}
}
public static DateTime makeWindowStartTime(DateTime timestamp,
Duration interval)
{
DateTime today = timestamp.withTimeAtStartOfDay();
// log.info("Timestamp is {}", timestamp);
// log.info("Day start is {}", today);
Duration intoToday = new Duration(today, timestamp);
// calc how far into the current window we are
long intoPeriod = intoToday.getMillis() % interval.getMillis();
return timestamp.minus(intoPeriod);
}
private static DateTime makeWindowEndTime(DateTime startTime,
Duration interval)
{
if (startTime == null || interval == null) return null;
return startTime.plus(interval.getMillis());
}
/**
* Return true if the timestamp is within the current time window.
*/
private boolean inWindow(DateTime timestamp) {
if (timestamp == null) return false;
return timestamp.compareTo(windowStartTime) >= 0 &&
timestamp.compareTo(windowEndTime) < 0;
}
private void applyValue(double value) {
if (open == null) {
open = value;
}
close = value;
if (low == null) {
low = value;
} else if (low.compareTo(value) > 0) {
low = value;
}
if (high == null) {
high = value;
} else if (high.compareTo(value) < 0) {
high = value;
}
}
/**
* Set up a callback to post an event when our time window expires.
*/
private void scheduleCallback(DateTime endTime) {
SchedulingService sched = agentContext.getStatementContext().getSchedulingService();
if (handle != null) {
// remove old schedule
// log.info("Removing old callback");
sched.remove(handle, scheduleSlot);
handle = null;
}
DateTime currentTime = toDateTime(sched.getTime());
DateTime targetTime = endTime.plusSeconds(LATE_EVENT_SLACK_SECONDS);
long callbackTime = targetTime.getMillis() - currentTime.getMillis();
ScheduleHandleCallback callback = new ScheduleHandleCallback() {
public void scheduledTrigger(EngineLevelExtensionServicesContext esc) {
handle = null; // clear out schedule handle
// log.info("Callback running");
OHLCPlugInView.this.postData();
}
};
handle = new EPStatementHandleCallback(agentContext.getEpStatementAgentInstanceHandle(), callback);
sched.add(callbackTime, handle, scheduleSlot);
// log.info("Scheduled callback for {}", callbackTime);
}
DateTime lastStartTime;
/**
* Update listeners with our new value.
*/
private void postData() {
if (open == null) {
// log.info("No data to post");
return;
}
if (lastStartTime != null && lastStartTime.compareTo(windowStartTime) == 0) {
log.warn("DUP START TIME");
}
// log.info("posting {} with {} events in {}", windowStartTime, eventCount, Thread.currentThread());
OHLCEvent value = new OHLCEvent(windowStartTime, open, high, low, close);
// send
EventAdapterService service = agentContext.getStatementContext().getEventAdapterService();
EventBean[] newBeans = new EventBean[] {service.adapterForBean(value)};
updateChildren(newBeans, lastData);
// reset for next post
lastData = newBeans;
open = null;
close = null;
high = null;
low = null;
lastStartTime = windowStartTime;
public void postEvent(OHLCEvent event) {
EventBean[] newData = new EventBean[] {service.adapterForBean(event)};
updateChildren(newData, lastData);
lastData = newData;
}
//
@ -279,11 +140,10 @@ public class OHLCPlugInView extends ViewSupport {
*/
@Override
public EventType getEventType() {
return getEventType(agentContext.getStatementContext().getEventAdapterService());
return getEventType(service);
}
protected static EventType getEventType(EventAdapterService service)
{
protected static EventType getEventType(EventAdapterService service) {
return service.addBeanType(OHLCEvent.class.getName(),
OHLCEvent.class,
false, false, false);

View File

@ -1,5 +1,8 @@
package ats.plugin;
import java.math.BigDecimal;
import java.util.List;
import com.espertech.esper.client.EventType;
import com.espertech.esper.core.context.util.AgentInstanceViewFactoryChainContext;
import com.espertech.esper.core.service.StatementContext;
@ -10,9 +13,7 @@ import com.espertech.esper.view.ViewFactory;
import com.espertech.esper.view.ViewFactoryContext;
import com.espertech.esper.view.ViewFactorySupport;
import com.espertech.esper.view.ViewParameterException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import org.joda.time.DateTime;
/**
@ -21,6 +22,8 @@ import org.joda.time.DateTime;
public class OHLCPlugInViewFactory extends ViewFactorySupport {
private EventAdapterService eventAdapterService;
private List<ExprNode> params;
private ExprNode calcType;
private ExprNode timeType;
private ExprNode interval;
private ExprNode timestamp;
private ExprNode value;
@ -35,8 +38,8 @@ public class OHLCPlugInViewFactory extends ViewFactorySupport {
{
eventAdapterService = context.getEventAdapterService();
if (params.size() != 3) {
throw new ViewParameterException("OHLC view takes three parameters: time interval, timestamp expression, and mid price expression.");
if (params.size() != 5) {
throw new ViewParameterException("OHLC view takes five parameters: calulation type (\"OHLC\" or \"Heikin-Ashi\"), batching type (\"time\" or \"ticks\"), time interval or tick count, timestamp expression, and mid price expression.");
}
this.params = params;
}
@ -57,20 +60,36 @@ public class OHLCPlugInViewFactory extends ViewFactorySupport {
statementContext,
params, true);
interval = validatedNodes[0];
timestamp = validatedNodes[1];
value = validatedNodes[2];
calcType = validatedNodes[0];
timeType = validatedNodes[1];
interval = validatedNodes[2];
timestamp = validatedNodes[3];
value = validatedNodes[4];
Class calcTypeClass = calcType.getForge().getEvaluationType();
if (calcTypeClass != String.class)
{
throw new ViewParameterException("OHLC view needs a String-typed calc type value for parameter 1");
}
Class timeTypeClass = timeType.getForge().getEvaluationType();
if (timeTypeClass != String.class)
{
throw new ViewParameterException("OHLC view needs a String-typed time type value for parameter 2");
}
Class intervalClass = interval.getForge().getEvaluationType();
if ((intervalClass != String.class))
if ((intervalClass != String.class) &&
(intervalClass != Integer.class) &&
(intervalClass != int.class))
{
throw new ViewParameterException("OHLC view needs String-typed interval value for parameter 1.");
throw new ViewParameterException("OHLC view needs String- or integer-typed interval value for parameter 3 - got " + intervalClass);
}
Class timestampClass = timestamp.getForge().getEvaluationType();
if ((timestampClass != DateTime.class))
{
throw new ViewParameterException("OHLC view needs DateTime typed timestamp values for parameter 2");
throw new ViewParameterException("OHLC view needs DateTime typed timestamp values for parameter 4");
}
Class valueClass = value.getForge().getEvaluationType();
@ -78,7 +97,7 @@ public class OHLCPlugInViewFactory extends ViewFactorySupport {
(valueClass != Double.class) &&
(valueClass != BigDecimal.class))
{
throw new ViewParameterException("OHLC view needs double or BigDecimal values for parameter 3");
throw new ViewParameterException("OHLC view needs double or BigDecimal values for parameter 5");
}
}
@ -86,7 +105,8 @@ public class OHLCPlugInViewFactory extends ViewFactorySupport {
* Create a new view using already-passed context and params.
*/
public View makeView(AgentInstanceViewFactoryChainContext agentContext) {
return new OHLCPlugInView(agentContext, interval, timestamp, value);
return new OHLCPlugInView(agentContext, calcType, timeType,
interval, timestamp, value);
}
/**

View File

@ -1,42 +0,0 @@
package ats.plugin;
import org.joda.time.DateTime;
/**
* OHLCValueEvent stores one bar of OHLC info.
*/
public class OHLCValueEvent extends OHLCEvent {
private double value;
public OHLCValueEvent() {
this(null, 0, 0, 0, 0, 0);
}
public OHLCValueEvent(DateTime time,
double open, double high,
double low, double close,
double value)
{
super(time, open, high, low, close);
this.value = value;
}
public static OHLCValueEvent make(DateTime time,
double open, double high,
double low, double close,
double value)
{
return new OHLCValueEvent(time, open, high, low, close, value);
}
public Double getValue() { return value; }
/**
* Return a human readable representation of this event.
*/
public String toString() {
return String.format("OHLCValueEvent[%s, open=%.3f, high=%.3f, low=%.3f, close=%.3f, value=%.3f]",
getTime(), getOpen(), getHigh(), getLow(), getClose(), value);
}
}

View File

@ -0,0 +1,22 @@
package ats.plugin;
/**
* OHLCValues is a struct for storing open, high, low, and close
* values.
*/
public class OHLCValues {
public double open;
public double high;
public double low;
public double close;
public OHLCValues(double open, double high,
double low, double close)
{
this.open = open;
this.high = high;
this.low = low;
this.close = close;
}
}

View File

@ -0,0 +1,85 @@
package ats.plugin;
import com.espertech.esper.core.context.util.AgentInstanceViewFactoryChainContext;
import com.espertech.esper.epl.expression.core.ExprEvaluator;
import com.espertech.esper.epl.expression.core.ExprNode;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TicksCandlestickWindow runs a CandlestickCalc across a set number
* of ticks for each window.
*/
class TicksCandlestickWindow implements CandlestickWindow {
private static final Logger log = LoggerFactory.getLogger(TicksCandlestickWindow.class);
private final AgentInstanceViewFactoryChainContext context;
/** How many ticks in each window? */
private final long windowTicks;
private OHLCPlugInView plugin;
private CandlestickCalc calc;
/** How many ticks into the window we are. */
private long currentTick = 0;
/** The time the window started. */
private DateTime windowStartTime;
/**
* Create a new window that calculates across a set number of
* ticks.
*/
public TicksCandlestickWindow(OHLCPlugInView plugin,
CandlestickCalc calc,
AgentInstanceViewFactoryChainContext context,
ExprNode intervalExpression)
{
this.plugin = plugin;
this.calc = calc;
this.context = context;
windowTicks = parseTickCount(intervalExpression);
}
/**
* Return the time period specified by the given expression value.
*/
private long parseTickCount(ExprNode intervalExpression) {
ExprEvaluator evaluator = intervalExpression.getForge().getExprEvaluator();
Object o = evaluator.evaluate(null, true, context);
return new Long((Integer)o).longValue();
}
/**
* Process a tick with the given time and value.
*/
public void update(DateTime timestamp, double value) {
ensureWindow(timestamp);
calc.applyValue(value);
}
/**
* Make sure our window times are set up and current.
* @param timestamp
*/
public void ensureWindow(DateTime timestamp) {
if (windowStartTime == null)
windowStartTime = timestamp;
currentTick++;
if (currentTick < windowTicks) return;
OHLCValues values = calc.getValues();
if (values == null) {
log.info("No data to post");
return;
}
plugin.postEvent(new OHLCEvent(windowStartTime, values));
// reset for next window
calc.reset();
currentTick = 0;
windowStartTime = null;
}
}

View File

@ -0,0 +1,198 @@
package ats.plugin;
import com.espertech.esper.core.context.util.AgentInstanceViewFactoryChainContext;
import com.espertech.esper.core.service.EPStatementHandleCallback;
import com.espertech.esper.core.service.EngineLevelExtensionServicesContext;
import com.espertech.esper.epl.expression.core.ExprEvaluator;
import com.espertech.esper.epl.expression.core.ExprNode;
import com.espertech.esper.schedule.ScheduleHandleCallback;
import com.espertech.esper.schedule.SchedulingService;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Duration;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TimeCandlestickWindow runs a CandlestickCalc across ticks for a set
* duration each window.
*/
class TimeCandlestickWindow implements CandlestickWindow {
private static final Logger log = LoggerFactory.getLogger(TimeCandlestickWindow.class);
private static final int LATE_EVENT_SLACK_SECONDS = 5;
private static final PeriodFormatter periodFormatter = new PeriodFormatterBuilder()
.appendDays().appendSuffix("d")
.appendHours().appendSuffix("h")
.appendMinutes().appendSuffix("m")
.appendSeconds().appendSuffix("s")
.toFormatter();
private final AgentInstanceViewFactoryChainContext context;
private final long scheduleSlot;
private EPStatementHandleCallback handle;
private final Duration interval;
private OHLCPlugInView plugin;
private CandlestickCalc calc;
private DateTime windowStartTime;
private DateTime windowEndTime;
private DateTime lastStartTime;
/**
* Create a window over the given duration.
*/
public TimeCandlestickWindow(OHLCPlugInView plugin,
CandlestickCalc calc,
AgentInstanceViewFactoryChainContext context,
ExprNode intervalExpression)
{
this.plugin = plugin;
this.calc = calc;
this.context = context;
scheduleSlot = context.getStatementContext().getScheduleBucket().allocateSlot();
interval = parseInterval(intervalExpression);
}
/**
* Process a tick with the given time and value.
*/
public void update(DateTime timestamp, double value) {
ensureWindow(timestamp);
calc.applyValue(value);
}
/**
* Return the time period specified by the given expression value.
*/
private Duration parseInterval(ExprNode interval) {
ExprEvaluator evaluator = interval.getForge().getExprEvaluator();
String intervalStr = (String)evaluator.evaluate(null, true, context);
return parseInterval(intervalStr);
}
/**
* Return the time period specified by the given string.
*/
public static Duration parseInterval(String interval) {
interval = interval.replaceAll("\\s+","");
return periodFormatter.parsePeriod(interval).toStandardDuration();
}
/**
* Make sure our window times are set up and current.
*/
public void ensureWindow(DateTime timestamp) {
if (timestamp == null) return;
if (windowStartTime == null) {
// create open window
windowStartTime = makeWindowStartTime(timestamp, interval);
windowEndTime = makeWindowEndTime(windowStartTime, interval);
scheduleCallback(windowEndTime);
}
if (!inWindow(timestamp)) {
// past current window.
// post and create a new one.
postData();
windowStartTime = makeWindowStartTime(timestamp, interval);
windowEndTime = makeWindowEndTime(windowStartTime, interval);
scheduleCallback(windowEndTime);
}
}
public static DateTime makeWindowStartTime(DateTime timestamp,
Duration interval)
{
DateTime today = timestamp.withTimeAtStartOfDay();
// log.info("Timestamp is {}", timestamp);
// log.info("Day start is {}", today);
Duration intoToday = new Duration(today, timestamp);
// calc how far into the current window we are
long intoPeriod = intoToday.getMillis() % interval.getMillis();
return timestamp.minus(intoPeriod);
}
private static DateTime makeWindowEndTime(DateTime startTime,
Duration interval)
{
if (startTime == null || interval == null) return null;
return startTime.plus(interval.getMillis());
}
/**
* Return true if the timestamp is within the current time window.
*/
private boolean inWindow(DateTime timestamp) {
if (timestamp == null) return false;
return timestamp.compareTo(windowStartTime) >= 0 &&
timestamp.compareTo(windowEndTime) < 0;
}
/**
* Convert a bare long value to a proper DateTime entity. Assumes
* UTC time zone.
*/
public static DateTime toDateTime(long l) {
return new DateTime(l, DateTimeZone.UTC);
}
/**
* Set up a callback to post an event when our time window expires.
*/
private void scheduleCallback(DateTime endTime) {
SchedulingService sched = context.getStatementContext().getSchedulingService();
if (handle != null) {
// remove old schedule
// log.info("Removing old callback");
sched.remove(handle, scheduleSlot);
handle = null;
}
DateTime currentTime = toDateTime(sched.getTime());
DateTime targetTime = endTime.plusSeconds(LATE_EVENT_SLACK_SECONDS);
long callbackTime = targetTime.getMillis() - currentTime.getMillis();
ScheduleHandleCallback callback = new ScheduleHandleCallback() {
public void scheduledTrigger(EngineLevelExtensionServicesContext esc) {
handle = null; // clear out schedule handle
// log.info("Callback running");
TimeCandlestickWindow.this.postData();
}
};
handle = new EPStatementHandleCallback(context.getEpStatementAgentInstanceHandle(), callback);
sched.add(callbackTime, handle, scheduleSlot);
// log.info("Scheduled callback for {}", callbackTime);
}
/**
* Update listeners with our new value.
*/
private void postData() {
if (lastStartTime != null && lastStartTime.compareTo(windowStartTime) == 0) {
log.warn("DUP START TIME");
}
// log.info("posting {} with {} events in {}", windowStartTime, eventCount, Thread.currentThread());
OHLCValues values = calc.getValues();
if (values == null) {
// log.info("No data to post");
return;
}
plugin.postEvent(new OHLCEvent(windowStartTime, values));
calc.reset();
lastStartTime = windowStartTime;
}
}