package org.cryptocoinpartners.esper;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import org.cryptocoinpartners.schema.Bar;
import org.cryptocoinpartners.schema.Market;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.ExtensionServicesContext;
import com.espertech.esper.epl.expression.ExprNode;
import com.espertech.esper.event.EventAdapterService;
import com.espertech.esper.schedule.ScheduleHandleCallback;
import com.espertech.esper.schedule.ScheduleSlot;
import com.espertech.esper.view.CloneableView;
import com.espertech.esper.view.View;
import com.espertech.esper.view.ViewSupport;
/**
* Custom view to compute minute OHLC bars for double values and based on the event's timestamps.
* <p>
* Assumes events arrive in the order of timestamps, i.e. event 1 timestamp is always less or equal event 2 timestamp.
* <p>
* Implemented as a custom plug-in view rather then a series of EPL statements for the following reasons:
* - Custom output result mixing aggregation (min/max) and first/last values
* - No need for a data window retaining events if using a custom view
* - Unlimited number of groups (minute timestamps) makes the group-by clause hard to use
*/
public class OHLCBarPlugInView extends ViewSupport implements CloneableView {
private final static int LATE_EVENT_SLACK_SECONDS = 5;
protected static Logger log = LoggerFactory.getLogger("org.cryptocoinpartners.OHLCBarPlugInView");
private final AgentInstanceViewFactoryChainContext agentInstanceViewFactoryContext;
private final ScheduleSlot scheduleSlot;
private final ExprNode timestampExpression;
private final ExprNode valueExpression;
private ExprNode marketExpression;
private ExprNode intervalExpression;
private final EventBean[] eventsPerStream = new EventBean[1];
private EPStatementHandleCallback handle;
private Long cutoffTimestampMinute;
private Long currentTimestampMinute;
private Double first;
private Double last;
private Double max;
private Double min;
private Market market;
private static Double interval;
private EventBean lastEvent;
public OHLCBarPlugInView(AgentInstanceViewFactoryChainContext agentInstanceViewFactoryContext, ExprNode timestampExpression, ExprNode valueExpression) {
this.agentInstanceViewFactoryContext = agentInstanceViewFactoryContext;
this.timestampExpression = timestampExpression;
this.valueExpression = valueExpression;
this.scheduleSlot = agentInstanceViewFactoryContext.getStatementContext().getScheduleBucket().allocateSlot();
}
public OHLCBarPlugInView(AgentInstanceViewFactoryChainContext agentInstanceViewFactoryContext, ExprNode timestampExpression, ExprNode valueExpression,
ExprNode marketExpression, ExprNode intervalExpression) {
this.agentInstanceViewFactoryContext = agentInstanceViewFactoryContext;
this.timestampExpression = timestampExpression;
this.valueExpression = valueExpression;
this.marketExpression = marketExpression;
this.intervalExpression = intervalExpression;
this.scheduleSlot = agentInstanceViewFactoryContext.getStatementContext().getScheduleBucket().allocateSlot();
}
@Override
public void update(EventBean[] newData, EventBean[] oldData) {
if (newData == null) {
return;
}
for (EventBean theEvent : newData) {
eventsPerStream[0] = theEvent;
log.trace(this.getClass().getSimpleName() + ":updaate recieved new event" + eventsPerStream[0].toString());
interval = (Double) intervalExpression.getExprEvaluator().evaluate(eventsPerStream, true, agentInstanceViewFactoryContext);
Long timestamp = (Long) timestampExpression.getExprEvaluator().evaluate(eventsPerStream, true, agentInstanceViewFactoryContext);
Long timestampMinute = removeSeconds(timestamp);
double value = (Double) valueExpression.getExprEvaluator().evaluate(eventsPerStream, true, agentInstanceViewFactoryContext);
if (marketExpression != null)
market = (Market) marketExpression.getExprEvaluator().evaluate(eventsPerStream, true, agentInstanceViewFactoryContext);
// test if this minute has already been published, the event is too late
if (interval == null || interval == 0 || timestamp == null || timestamp == 0 || timestampMinute == null || timestampMinute == 0 || value == 0
|| (marketExpression != null && market == null)) {
log.error(this.getClass().getSimpleName() + ":unable to create bar with interval: " + interval + " timestamp: " + timestamp
+ " timestampMinute:" + timestampMinute + " value: " + value + " market: " + market + " cutoffTimestampMinute: "
+ cutoffTimestampMinute);
return;
}
log.trace(this.getClass().getSimpleName() + ":update determing bar for interval: " + interval + " timestamp: " + timestamp + " timestampMinute:"
+ timestampMinute + " value: " + value + " market: " + market + " cutoffTimestampMinute: " + cutoffTimestampMinute);
if ((cutoffTimestampMinute != null) && (timestampMinute <= cutoffTimestampMinute)) {
continue;
}
// if the same minute, aggregate
if (timestampMinute.equals(getCurrentTimestampMinute())) {
log.trace(this.getClass().getSimpleName() + ":apply value for " + value);
applyValue(value);
}
// first time we see an event for this minute
else {
// there is data to post
if (getCurrentTimestampMinute() != null) {
log.trace(this.getClass().getSimpleName() + " update: posing data for bar for market: " + market + " with timestamp:"
+ currentTimestampMinute + " first:" + first + " high: " + max + " low: " + min + " close: " + last);
postData();
}
setCurrentTimestampMinute(timestampMinute);
// currentTimestampMinute = timestampMinute;
log.trace(this.getClass().getSimpleName() + ":apply value for " + value);
applyValue(value);
// schedule a callback to fire in case no more events arrive
log.trace(this.getClass().getSimpleName() + ":scheduleCallback for interval: " + interval.longValue() + " slack " + LATE_EVENT_SLACK_SECONDS);
scheduleCallback();
}
}
}
public Long getCurrentTimestampMinute() {
return currentTimestampMinute;
}
protected void setCurrentTimestampMinute(Long timestamp) {
if ((timestamp == null || currentTimestampMinute == null) || (timestamp > 0 && timestamp > currentTimestampMinute)) {
this.currentTimestampMinute = timestamp;
} else {
log.error("setCurrentTimestampMinute: unable to set curren time stamp minute as currnet currentTimestampMinute " + currentTimestampMinute
+ " is greater than new time stamp " + timestamp);
}
}
public Long getCutoffTimestampMinute() {
return cutoffTimestampMinute;
}
protected void setCutoffTimestampMinute(Long cutoff) {
if (cutoffTimestampMinute == null || (cutoff != null && cutoff > cutoffTimestampMinute)) {
this.cutoffTimestampMinute = cutoff;
} else {
log.error("setCutoffTimestampMinute: unable to set cut off timestamp as currnet cutoffTimestampMinute " + cutoffTimestampMinute
+ " is greater than new cut off time stamp " + cutoff);
}
}
@Override
public EventType getEventType() {
return getEventType(agentInstanceViewFactoryContext.getStatementContext().getEventAdapterService());
}
@Override
public Iterator<EventBean> iterator() {
throw new UnsupportedOperationException("Not supported");
}
@Override
public View cloneView() {
return new OHLCBarPlugInView(agentInstanceViewFactoryContext, timestampExpression, valueExpression, marketExpression, intervalExpression);
}
private void applyValue(double value) {
if (first == null) {
first = value;
}
last = value;
if (min == null) {
min = value;
} else if (min.compareTo(value) > 0) {
min = value;
}
if (max == null) {
max = value;
} else if (max.compareTo(value) < 0) {
max = value;
}
}
protected static EventType getEventType(EventAdapterService eventAdapterService) {
return eventAdapterService.addBeanType(Bar.class.getName(), Bar.class, false, false, false);
}
private static long removeSeconds(long timestamp) {
Calendar cal = GregorianCalendar.getInstance();
cal.setTimeInMillis(timestamp);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
//TODO: need to support bars for mulitiple days
if ((interval / 86400) > 1) {
int days = (int) Math.round(interval / 86400);
cal.set(Calendar.HOUR_OF_DAY, 0);
int modulo = cal.get(Calendar.DAY_OF_YEAR) % days;
if (modulo > 0) {
cal.add(Calendar.DAY_OF_YEAR, -modulo);
}
//cal.set(Calendar.DAY_OF_YEAR, 0);
// round interval to nearest day
interval = ((double) Math.round(interval / 86400)) * 86400;
}
if ((interval / 3600) > 1) {
int hours = (int) Math.round(interval / 3600);
cal.set(Calendar.MINUTE, 0);
int modulo = cal.get(Calendar.HOUR_OF_DAY) % hours;
if (modulo > 0) {
cal.add(Calendar.HOUR_OF_DAY, -modulo);
}
// cal.set(Calendar.HOUR_OF_DAY, 0);
// round interval to nearest hour
interval = ((double) Math.round(interval / 3600)) * 3600;
}
if ((interval / 60) > 1) {
int mins = (int) (Math.round(interval / 60));
int modulo = cal.get(Calendar.MINUTE) % mins;
if (modulo > 0) {
cal.add(Calendar.MINUTE, -modulo);
}
interval = ((double) Math.round(interval / 60)) * 60;
}
return cal.getTimeInMillis();
}
private void scheduleCallback() {
if (handle != null) {
// remove old schedule
agentInstanceViewFactoryContext.getStatementContext().getSchedulingService().remove(handle, scheduleSlot);
handle = null;
}
long currentTime = agentInstanceViewFactoryContext.getStatementContext().getSchedulingService().getTime();
long currentRemoveSeconds = removeSeconds(currentTime);
//long targetTime = currentRemoveSeconds + (86400 + LATE_EVENT_SLACK_SECONDS) * 1000; // leave some seconds for late comers
long targetTime = currentRemoveSeconds + ((interval.longValue() + LATE_EVENT_SLACK_SECONDS) * 1000); // leave some seconds for late comers
long scheduleAfterMSec = targetTime - currentTime;
log.trace(this.getClass().getSimpleName() + ":scheduling Callback after : " + scheduleAfterMSec + " for currentTime " + currentTime + " targetTime "
+ targetTime);
ScheduleHandleCallback callback = new ScheduleHandleCallback() {
@Override
public void scheduledTrigger(ExtensionServicesContext extensionServicesContext) {
handle = null; // clear out schedule handle
OHLCBarPlugInView.this.postData();
}
};
handle = new EPStatementHandleCallback(agentInstanceViewFactoryContext.getEpStatementAgentInstanceHandle(), callback);
agentInstanceViewFactoryContext.getStatementContext().getSchedulingService().add(scheduleAfterMSec, handle, scheduleSlot);
log.trace(this.getClass().getSimpleName() + ":scheduledCallback after : " + scheduleAfterMSec + " for handle " + handle + " scheduleSlot "
+ scheduleSlot);
}
private void postData() {
Bar barValue;
if (currentTimestampMinute != null && first != null && last != null && max != null && min != null && market != null) {
try {
barValue = new Bar(currentTimestampMinute, first, last, max, min, market);
} catch (Exception | Error ex) {
log.error("PostData: Unable to generate bar for market: " + market + " with timestamp:" + currentTimestampMinute + " first:" + first
+ " high: " + max + " low: " + min + " close: " + last);
return;
}
EventBean outgoing = agentInstanceViewFactoryContext.getStatementContext().getEventAdapterService().adapterForBean(barValue);
if (lastEvent == null) {
log.trace("PostData: updating child " + outgoing.getUnderlying().toString());
this.updateChildren(new EventBean[] { outgoing }, null);
} else {
log.trace("PostData: updating child outgoing event " + outgoing.getUnderlying().toString() + " last event "
+ lastEvent.getUnderlying().toString());
this.updateChildren(new EventBean[] { outgoing }, new EventBean[] { lastEvent });
}
lastEvent = outgoing;
setCutoffTimestampMinute(currentTimestampMinute);
// cutoffTimestampMinute = currentTimestampMinute;
first = null;
last = null;
max = null;
min = null;
setCurrentTimestampMinute(null);
// currentTimestampMinute = null;
} else
log.error("PostData: Unable to generate bar for market: " + market + " with timestamp:" + currentTimestampMinute + " first:" + first + " high: "
+ max + " low: " + min + " close: " + last);
}
}