November 10, 2007
High-Precision Delay and Interval Timing
Before continuing with this post you should carefully read the AmiBroker Help topic for the getPerformanceCounter().
Measuring time is an important aspect of all real-time intraday trading systems. Typical tasks requiring high-resolution timing include:
- Limiting the message rate to Interactive Brokers (IB) to 50/sec (API Error 100).
- Inserting small delays before polling IB Status, to allow for Internet delays.
- Staggering (interlacing) portfolio trades to spread out the action.
- Measuring and optimizing AFL execution time.
- Modifying orders after a small delay (to ensure fills).
- Periodic execution of tasks, for example, Watchlist scanning, Display and Status refresh, calculations based on slow changing variables, etc.
- Time-Stamping events, for example order placement.
- Collecting/preprocessing quotes.
- Overlay live tick-charts on faster and easier to manage 1-minute charts.
Most of these tasks can be accomplished using just three custom timing functions:
- GetElapsedTime(): A function that returns elapsed time since reset.
- SetDelay(): A function that returns time left to reach a future time.
- GetDelayTrigger(): A function that returns a trigger when a delay times out.
This post provides example functions in a demo application. To allow the use of many timers each function requires you to provide a TimerName, which will be used to retrieve timer information. Static variables are Global and can be read from anywhere; this means you have to be careful not to cross-reference the timers by using the same TimerName from different panes or windows. When running multiple copies of the same code you will need to key the TimerNames. For more on how to do this, see Keying Static Variables in the Real-Time AFL Programming category.
The timers below are implemented using the getPerformanceCounter(). This function returns the amount of time elapsed since the computer was last started. Tomasz recently explained this as follows: “The underlying high frequency counter runs all the time since computer start. What ‘reset’ flag really does is to store last value so next time you read it, it gets subtracted from last value giving you the difference. If reset is false, the last value is set to zero, and you get the original number of ‘clock ticks’ since computer start“.
The timers in this post do their own sampling of the underlying high frequency counter, and the getPerformanceCounter() Reset argument is always left set to False.
The getPerformanceCounter() returns values with microsecond resolution; however, the practical accuracy is severely limited by interruptions from the computer’s operating system. Do not expect much better than about 50-millisecond absolute accuracy. Aside from designing your own dedicated trading hardware (to replace the PC) there isn’t much that can be done about this. If you are brave, you can experiment with increasing program priority in your Task Manager window.
Chart refreshes are most often initiated by an arriving quote, but they can also be initiated by mouse clicks, tooltip, and various chart operations. This means that, when market activity is low and things are not happening as fast as you would like, you can force extra AFL executions by clicking on your chart. You can verify this by running the code below and, while clicking rapidly on the chart, observe that the timer counts displayed will update more rapidly.
You can ensure a one-second chart refresh by adding a RequestTimedRefresh(1) to your code. If the frequency of your arriving data is slow, your AFL code may execute only sporadically. Since your code must execute to read your timers, the resolution of your timers will be limited by the chart refresh rate. If your chart refreshes once a second your timing resolution will be one second!
Normally most of the AFL code in an Indicator window executes when your chart refreshes; however, to obtain speed advantages, you may execute non-critical sections (like account information and System Status) of your code less frequently using a timer. You can also execute small sections of code more frequently by placing them inside a well-controlled loop. If you do this, be sure to limit the maximum time your code can spend inside the loop to one second or less.
For fast trading systems, the frequency of AFL executions (chart refreshes) may be slow and this may make it difficult for you to get LMT fills. There is no way to have a program that requires 50 milliseconds per pass to execute 20 times per second.
Considering the interval between AFL executions, it is important to plan the layout of your code so that all events are handled in the most efficient order. If you don’t, transmittance of your order could well be delayed by up to a full second. There are situations where you want to invoke an immediate re-execution of your code. In some cases you might want to do this after placing an order to check order status before the next quote or refresh. Although it should be used sparingly this is possible by calling the RefreshAll():
<p>This function can only be called once a second; calling it faster will not result in more frequent chart refreshes. This means you should only call it when really needed. <p>The code presented below is for demonstration only. The getElapsedTime() lets you measure elapsed time from the moment of Reset. The first argument passes the name you assign to the static timer; this allows you to use the same function to time different events. The second argument is a Reset flag. When this Reset is True, the function samples the underlying high frequency counter and uses it for later reference. When you call the function with the Reset argument set to False, it calculates the elapsed time by subtracting the earlier sampled value from the current value of the Performance Counter. <p>The setDelay() function lets you Start, Read, and Cancel a time delay. The TimerName argument functions as in the getElapsedTime(). Calling the setDelay() with the mSecDelay argument set to a non-zero value will start the Delay timer. Calling it with the mSecDelay argument set to zero will make it return the current count-down time in millseconds. Calling the function with the cancel argument set to True will terminate the delay. <p>The getDelayTrigger() function returns a trigger. This is a signal that is true for only one pass through the code. Triggers are frequently used in real-time trading systems. They are needed to prevent multiple actions when a signal becomes True. <p>To run the code, copy the formula to an Indicator and click Insert. You'll see a chart window like Figure 1 below: <p align="center">Figure 1. Result from running the example code. <a href='http://www.amibroker.org/userkb/2007/11/10/high-precision-delay-and-interval-timing/timerdisplayjpg/' rel='attachment wp-att-1422' title='timerdisplay.jpg'><img src='http://www.amibroker.org/userkb/wp-content/uploads/2007/11/timerdisplay.jpg' alt='timerdisplay.jpg' /></a> <p>The example code maintains three timers, T1, T2 and T3. All timing values are expressed in milliseconds. In Figure-1 the Elapsed Time shown is measured from timer Reset. The Delay shown is the time remaining after Start, until the delay times out. The line for Timer T2 shows that its Delay just timed-out and produced a trigger. Timer T3 still has a Delay in progress. Right-click on the chart to open the Param window: <p align="center">Figure 2. Param window. <a href='http://www.amibroker.org/userkb/2007/11/10/high-precision-delay-and-interval-timing/timerparampng/' rel='attachment wp-att-1423' title='timerparam.png'><img src='http://www.amibroker.org/userkb/wp-content/uploads/2007/11/timerparam.png' alt='timerparam.png' /></a> <p>If you click one of the timer Resets in the Param window you'll see the ElapsedTime in the corresponding row go to zero, and then start to increment sporadically when your chart refreshes. Without live data this would be at approximately 1-second intervals, as determined by the RequestTimedRefresh(1); <p>If you click Start for one of the timers this will start a delay. You can see how it counts down in the Delay column. Click the timer's Cancel to terminate the Delay. Note that whenever a Delay times out, the word "Trigger" briefly appears in the third column. function RefreshAll() { oAB = CreateObject("Broker.Application"); oAB.RefreshAll(); } function getElapsedTime( TimerName, Reset ) { if( Reset ) { TimeRef= GetPerformanceCounter(False); StaticVarSet(TimerName,TimeRef); } TimeRef = Nz(StaticVarGet(TimerName)); ElapsedTime = GetPerformanceCounter(False) - TimeRef; return ElapsedTime; } function setDelay( TimerName, MsecDelay, Cancel ) { HRCounter= GetPerformanceCounter(False); if( Cancel ) { StaticVarSet("TO"+TimerName,-1); StaticVarSet("DS"+TimerName, 0); } else if( MsecDelay ) { StaticVarSet("TO"+TimerName,HRCounter+MsecDelay ); } TimeOutTime = Nz(StaticVarGet("TO"+TimerName)); DelayCount = Max(0, TimeOutTime - HRCounter ); StaticVarSet("DC"+Timername, DelayCount); return DelayCount; } function getDelayTrigger( TimerName ) { DelayCount = Nz(StaticVarGet("DC"+TimerName)); DelayState = DelayCount > 0; PrevDelayState = Nz(StaticVarGet("DS"+TimerName)); StaticVarSet("DS"+TimerName, DelayState); return DelayState < PrevDelayState; } RequestTimedRefresh( 1); _SECTION_BEGIN("TIMER 1"); Reset1 = ParamTrigger("1 - Reset","RESET"); MSecDelay1 = Param("1 - Delay (mS)",1000,0,10000,10); if( ParamTrigger("1 - Start", "START") ) setDelay( "Timer1", MSecDelay1, 0 ); if( ParamTrigger("1 - Cancel", "CANCEL") ) setDelay( "Timer1", MSecDelay1, 1 ); _SECTION_END(); _SECTION_BEGIN("TIMER 2"); Reset2 = ParamTrigger("2 - Reset","RESET"); MSecDelay2 = Param("2 - Delay (mS)",4000,0,10000,10); if( ParamTrigger("2 - Start", "START") ) setDelay( "Timer2", MSecDelay2, 0 ); if( ParamTrigger("2 - Cancel", "CANCEL") ) setDelay( "Timer2", MSecDelay2, 1 ); _SECTION_END(); _SECTION_BEGIN("TIMER 3"); Reset3 = ParamTrigger("3 - Reset","RESET"); MSecDelay3 = Param("3 - Delay (mS)",8000,0,10000,10); if( ParamTrigger("3 - Start", "START") ) setDelay( "Timer3", MSecDelay3, 0 ); if( ParamTrigger("3 - Cancel", "CANCEL") ) setDelay( "Timer3", MSecDelay3, 1 ); _SECTION_END(); ET1 = getElapsedTime( "Timer1", Reset1 ); ETA1 = setDelay( "Timer1", 0, 0 ); TT1 = getDelayTrigger( "Timer1"); ET2 = getElapsedTime( "Timer2", Reset2 ); ETA2 = setDelay( "Timer2", 0, 0 ); TT2 = getDelayTrigger( "Timer2"); ET3 = getElapsedTime( "Timer3", Reset3 ); ETA3 = setDelay( "Timer3", 0, 0 ); TT3 = getDelayTrigger( "Timer3"); Title = "\nFilename: "+Filename+"\n\n"+ "MilliSeconds Since Computer Startup: "+NumToStr(GetPerformanceCounter(False),1.3)+"\n\n"+ "Timer ElapsedTime Delay Trigger\n\n"+ "T1 "+NumToStr(ET1,10.0)+" "+NumToStr(ETA1,7.0)+" "+WriteIf(TT1,"TRIGGER1","")+"\n\n"+ "T2 "+NumToStr(ET2,10.0)+" "+NumToStr(ETA2,7.0)+" "+WriteIf(TT2,"TRIGGER2","")+"\n\n"+
Edited by Al Venosa.
Filed by Herman at 9:09 pm under Real-Time AFL Programming
Comments Off on High-Precision Delay and Interval Timing