/* *************************************************************************************** * Copyright (C) 2006 EsperTech, Inc. All rights reserved. * * http://www.espertech.com/esper * * http://www.espertech.com * * ---------------------------------------------------------------------------------- * * The software in this package is published under the terms of the GPL license * * a copy of which has been included with this distribution in the license.txt file. * *************************************************************************************** */ package com.espertech.esper.example.ohlc; 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.ExprNode; import com.espertech.esper.event.EventAdapterService; import com.espertech.esper.schedule.ScheduleHandleCallback; import com.espertech.esper.view.CloneableView; import com.espertech.esper.view.View; import com.espertech.esper.view.ViewSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Iterator; /** * 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; private static Logger log = LoggerFactory.getLogger(OHLCBarPlugInView.class); private final AgentInstanceViewFactoryChainContext agentInstanceViewFactoryContext; private final long scheduleSlot; private final ExprNode timestampExpression; private final ExprNode valueExpression; 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 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 void update(EventBean[] newData, EventBean[] oldData) { if (newData == null) { return; } for (EventBean theEvent : newData) { eventsPerStream[0] = theEvent; Long timestamp = (Long) timestampExpression.getExprEvaluator().evaluate(eventsPerStream, true, agentInstanceViewFactoryContext); Long timestampMinute = removeSeconds(timestamp); double value = (Double) valueExpression.getExprEvaluator().evaluate(eventsPerStream, true, agentInstanceViewFactoryContext); // test if this minute has already been published, the event is too late if ((cutoffTimestampMinute != null) && (timestampMinute <= cutoffTimestampMinute)) { continue; } // if the same minute, aggregate if (timestampMinute.equals(currentTimestampMinute)) { applyValue(value); } else { // first time we see an event for this minute // there is data to post if (currentTimestampMinute != null) { postData(); } currentTimestampMinute = timestampMinute; applyValue(value); // schedule a callback to fire in case no more events arrive scheduleCallback(); } } } public EventType getEventType() { return getEventType(agentInstanceViewFactoryContext.getStatementContext().getEventAdapterService()); } public Iterator<EventBean> iterator() { throw new UnsupportedOperationException("Not supported"); } public View cloneView() { return new OHLCBarPlugInView(agentInstanceViewFactoryContext, timestampExpression, valueExpression); } 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(OHLCBarValue.class.getName(), OHLCBarValue.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); 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 + (60 + LATE_EVENT_SLACK_SECONDS) * 1000; // leave some seconds for late comers long scheduleAfterMSec = targetTime - currentTime; ScheduleHandleCallback callback = new ScheduleHandleCallback() { public void scheduledTrigger(EngineLevelExtensionServicesContext extensionServicesContext) { handle = null; // clear out schedule handle OHLCBarPlugInView.this.postData(); } }; handle = new EPStatementHandleCallback(agentInstanceViewFactoryContext.getEpStatementAgentInstanceHandle(), callback); agentInstanceViewFactoryContext.getStatementContext().getSchedulingService().add(scheduleAfterMSec, handle, scheduleSlot); } private void postData() { OHLCBarValue barValue = new OHLCBarValue(currentTimestampMinute, first, last, max, min); EventBean outgoing = agentInstanceViewFactoryContext.getStatementContext().getEventAdapterService().adapterForBean(barValue); if (lastEvent == null) { this.updateChildren(new EventBean[]{outgoing}, null); } else { this.updateChildren(new EventBean[]{outgoing}, new EventBean[]{lastEvent}); } lastEvent = outgoing; cutoffTimestampMinute = currentTimestampMinute; first = null; last = null; max = null; min = null; currentTimestampMinute = null; } }