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:

  1. Limiting the message rate to Interactive Brokers (IB) to 50/sec (API Error 100).
  2. Inserting small delays before polling IB Status, to allow for Internet delays.
  3. Staggering (interlacing) portfolio trades to spread out the action.
  4. Measuring and optimizing AFL execution time.
  5. Modifying orders after a small delay (to ensure fills).
  6. Periodic execution of tasks, for example, Watchlist scanning, Display and Status refresh, calculations based on slow changing variables, etc.
  7. Time-Stamping events, for example order placement.
  8. Collecting/preprocessing quotes.
  9. 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:

  1. GetElapsedTime(): A function that returns elapsed time since reset.
  2. SetDelay(): A function that returns time left to reach a future time.
  3. 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 secondcalling it faster will not result in more frequent chart refreshesThis means you should only call it when really needed. <p>The code presented below is for demonstration onlyThe getElapsedTime() lets you measure elapsed time from the moment of ResetThe first argument passes the name you assign to the static timerthis allows you to use the same function to time different eventsThe second argument is a Reset flagWhen this Reset is Truethe function samples the underlying high frequency counter and uses it for later referenceWhen you call the function with the Reset argument set to Falseit calculates the elapsed time by subtracting the earlier sampled value from the current value of the Performance Counter. <p>The setDelay() function lets you StartRead, and Cancel a time delayThe TimerName argument functions as in the getElapsedTime(). Calling the setDelay() with the mSecDelay argument set to a non-zero value will start the Delay timerCalling it with the mSecDelay argument set to zero will make it return the current count-down time in millsecondsCalling the function with the cancel argument set to True will terminate the delay. <p>The getDelayTrigger() function returns a triggerThis is a signal that is true for only one pass through the codeTriggers are frequently used in real-time trading systemsThey are needed to prevent multiple actions when a signal becomes True. <p>To run the codecopy the formula to an Indicator and click InsertYou'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 timersT1T2 and T3All timing values are expressed in millisecondsIn Figure-1 the Elapsed Time shown is measured from timer ResetThe Delay shown is the time remaining after Startuntil the delay times outThe line for Timer T2 shows that its Delay just timed-out and produced a triggerTimer T3 still has a Delay in progressRight-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 DelayNote that whenever a Delay times outthe word "Trigger" briefly appears in the third column. 

function RefreshAll()
    oAB CreateObject("Broker.Application");

function getElapsedTimeTimerNameReset )
    if( Reset ) 
    TimeRef Nz(StaticVarGet(TimerName));
    ElapsedTime GetPerformanceCounter(False) - TimeRef;
    return ElapsedTime;

function setDelayTimerNameMsecDelayCancel )
    if( Cancel ) 
    else if( MsecDelay ) 
        StaticVarSet("TO"+TimerName,HRCounter+MsecDelay );
    TimeOutTime Nz(StaticVarGet("TO"+TimerName));
    DelayCount Max(0TimeOutTime HRCounter );
    return DelayCount;

function getDelayTriggerTimerName )
    DelayCount Nz(StaticVarGet("DC"+TimerName));
    DelayState DelayCount 0;
    PrevDelayState Nz(StaticVarGet("DS"+TimerName));
    return DelayState PrevDelayState;

Reset1 ParamTrigger("1 - Reset","RESET");
MSecDelay1 Param("1 - Delay (mS)",1000,0,10000,10);
if( ParamTrigger("1 - Start""START") ) setDelay"Timer1"MSecDelay1);
if( ParamTrigger("1 - Cancel""CANCEL") ) setDelay"Timer1"MSecDelay1);

Reset2 ParamTrigger("2 - Reset","RESET");
MSecDelay2 Param("2 - Delay (mS)",4000,0,10000,10);
if( ParamTrigger("2 - Start""START") ) setDelay"Timer2"MSecDelay2);
if( ParamTrigger("2 - Cancel""CANCEL") ) setDelay"Timer2"MSecDelay2);

Reset3 ParamTrigger("3 - Reset","RESET");
MSecDelay3 Param("3 - Delay (mS)",8000,0,10000,10);
if( ParamTrigger("3 - Start""START") ) setDelay"Timer3"MSecDelay3);
if( ParamTrigger("3 - Cancel""CANCEL") ) setDelay"Timer3"MSecDelay3);

ET1     getElapsedTime"Timer1"Reset1 );
ETA1     setDelay"Timer1"0);
TT1     getDelayTrigger"Timer1");

ET2     getElapsedTime"Timer2"Reset2 );
ETA2     setDelay"Timer2"0);
TT2     getDelayTrigger"Timer2");

ET3     getElapsedTime"Timer3"Reset3 );
ETA3     setDelay"Timer3"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.

Comments are closed.