The TDash GUI, Bar-Replay version


The TDash code can now be used in Bar-Replay, IBc/TWS interfacing will be the next phase.

To be worked on:

  1. In Bar-Replay you can only place one trade/bar. I don’t think it is possible to changes this.
  2. The Backtester doesn’t scale in/out as the TWS would. This may be solved later.
  3. The code has not been optimized for speed, this may be a problem for some of you. I am using TDash with a 60-day 5-Sec Local DB — so far no major problems.
  4. Buttons for functions yet to be implemented are grayed-out.

Coding level:


This is an advanced afl project. While this code can be used as is, it is intended for the experienced programmer who may just borrow some ideas, functions, or snippets of the code, to create their own trading GUI.
Sorry, I have no time to provide detailed help in using this program.

Demo Video:

TDash Features:

  1. Modify Pending orders by dragging their Markers
  2. Increment/Decrement price, and Cancel pending orders from the markers
  3. Place multiple orders to scale in/out (But Backtester code is not ready for scaling yet)
  4. A floating order bar (QBar) to place MKT, LMT, and STP orders
  5. When placing a LMT order at the wrong side of the price the order changes to a MKT. This allows you to place MKT and LMT orders without having to change type each time.
  6. The use of composites to log multiple trading sessions for long term Backtesting
  7. 3D buttons for Trigger, Toggle, and Rotate functions
  8. Flexibility in custom sizing all objects
  9. Auto-sizing of button layout
  10. Ability to dynamically hide/show buttons and button groups
  11. Use button to control indicators on the main chart
  12. Optional Auto-Backtests when an order is filled to create real-time tradelists and statistics

Misc. Notes:

For the arrows to display properly you need to set Preferences -> Intraday -> “Time of first tick inside bar” or “START time of interval”. Be sure to select REPLAY data by setting the DATA selector in the SETUP button group to REPLAY.

The look of buttons and their layout can be changed in the Parameter window. These parameters are for experimenting, they can be hard-coded when you have decided what you want. To prevent very large buttons when you are re-sizing the TDash pane you can set their maximum height and width in the Parameter window. Button functions have arguments to place buttons groups anywhere in the window/pane.

You can change many other features, like button shape and shadow width, group header height, colors etc. You can make similar adjustments for the QBar and markers. In addition to the settings in the Parameter window the Button3D() function has size, offset, and color arguments you can change from afl. Using these functions you can create dynamically configured button panels, i.e., show/hide buttons or groups, change dimensions and color, to meet virtually any requirement.

At this time there are only three button functions: Trigger, Toggle, and Rotate. The Rotate button is used to select items from a short list. Buttons states are persistent. Grayed-out buttons are not functional yet, or are deactivated from afl because they are not required in the current button layout.

To learn more about buttons, toggle TIPS in the SETUP group to ON, and hover your cursor over the buttons. Except for MKT orders the QBar tracks your vertical cursor position. When MKT type is selected (in the ORDERS button group) the QBar position is fixed at the vertical center of the TDash window and orders will be placed at the last price. LMT@LastPrice or a STP order. For the moment the number of shares traded is set in the parameter window.

STP and LMT prices are monitored during Bar-Replay and fill when the price crosses the threshold. MKT orders will fill immediately. Bracket orders and Pattern orders haven’t been implemented yet. Pattern orders you probably haven’t heard of yet: a pattern order can contain a complex order pattern and are very handy when trading fast, it allows you to place any number of orders with a single click.

Markers for pending orders have a point to the left, a Position Marker is pointed at both ends. Order markers can be dragged; incremented, decremented, and canceled from pop-up controls that appear when you hover your cursor to the right of the marker.

Position Markers have a colored line extending onto the main chart that ends at the entry arrow. Inside the TDash window there is a small profit histogram that shows you current trade profit. If this histogram doesn’t show adjust the X-Offset for Markers to make space.

To run this code you need to apply the TDashMain104.afl to the left top window, the TDash104.afl to the right top window, and TDashInclude104 to the indicator pane below the TDash window (the include is only shown for easy access). The left bottom window can be used for other indicators. To run Auto/Manual Backtests you need to copy TDashBacktest.js to the \TDash\JS folder in your AmiBroker folder

Eventually the TDash window will be extended to the right (perhaps place or extend it on another monitor) to make room for a collection of custom gfx trading indicators.

Logging and displaying trades

In real trading it is desirable to use a persistent trade record so that trades for multiple sessions be analyzed using the Backtester. The easiest way to do this is to save the trades in a composite. The code shown below is a simplified version of what will be used in the TDash system, however, it can be used with any trading system. This code requires you set Preferences -> Intraday -> “Time of first tick inside bar” or “START time of interval”. Apply the code to an empty pane, start Bar-Replay, and open the Parameter window. After placing some trades you can send the code to the Backtester for analysis. You can perform the following actions:

I use small solid triangles for Buy and Sell signals, and hollow triangles for Short and Cover signals. This allows me to overlay the Buy and Cover signals, and the Sell and Short signals, to display trade reversals. The triangles are placed at the actual trading price (the Close in this example). This is what the arrows look like:

_SECTION_BEGIN"USING A TRADE COMPOSITE" );
function DeleteCompositeCompositeName )
{
    global Ticker;
    oAB CreateObject"Broker.Application" );
    oStocks oAB.Stocks();
    oStocks.RemoveCompositeName );
    oAB.RefreshAll();
}

function  AddTradeToCompositeTickerActionLMTPrice )
{
    global Ticker;
    BI  BarIndex();
    LBI LastValueBI );

    if ( Action != "" )
    {
        SignalArray NzForeignTicker "~SignalArrays""V"False ) );
        BuyPriceArray NzForeignTicker "~SignalArrays""O"False ) );
        SellPriceArray NzForeignTicker "~SignalArrays""H"False ) );
        ShortPriceArray NzForeignTicker "~SignalArrays""L"False ) );
        CoverPriceArray NzForeignTicker "~SignalArrays""C"False ) );

        switch ( Action )
        {

            case "BUY":
                SignalArray[LBI] = SignalArray[LBI] | 1;
                BuyPriceArray[LBI] = LMTPrice;
                break;

            case "SELL":
                SignalArray[LBI] = SignalArray[LBI] | 2;
                SellPriceArray[LBI] = LMTPrice;
                break;

            case "SHORT":
                SignalArray[LBI] = SignalArray[LBI] | 4;
                ShortPriceArray[LBI] = LMTPrice;
                break;

            case "COVER":
                SignalArray[LBI] = SignalArray[LBI] | 8;
                CoverPriceArray[LBI] = LMTPrice;
                break;

            case "REVLONG":
                SignalArray[LBI] = SignalArray[LBI] | 1;
                CoverPriceArray[LBI] = BuyPriceArray[LBI] = LMTPrice;
                break;

            case "REVSHORT":
                SignalArray[LBI] = SignalArray[LBI] | 4;
                SellPriceArray[LBI] = ShortPriceArray[LBI] = LMTPrice;
                break;
        }

        AddToCompositeSignalArrayTicker "~SignalArrays""V"atcFlagEnableInIndicator );
        AddToCompositeBuyPriceArrayTicker "~SignalArrays""O"atcFlagEnableInIndicator );
        AddToCompositeSellPriceArrayTicker "~SignalArrays""H"atcFlagEnableInIndicator );
        AddToCompositeShortPriceArrayTicker "~SignalArrays""L"atcFlagEnableInIndicator );
        AddToCompositeCoverPriceArrayTicker "~SignalArrays""C"atcFlagEnableInIndicator );
    }
}

Ticker Name();
Action "";

if ( ParamTrigger"Buy""BUY" ) ) Action "BUY";
if ( ParamTrigger"Sell""SELL" ) ) Action "SELL";
if ( ParamTrigger"Short""SHORT" ) ) Action "SHORT";
if ( ParamTrigger"Cover""COVER" ) ) Action "COVER";
if ( ParamTrigger"Reverse to Long""REVLONG" ) ) Action "REVLONG";
if ( ParamTrigger"Reverse to Short""REVSHORT" ) ) Action "REVSHORT";
if ( ParamTrigger"Delete Signal Compoiste""DELETE" ) ) DeleteCompositeTicker "~SignalArrays" );
AddTradeToCompositeTickerActionLastValueClose ) );

RequestTimedRefresh0.1 );
if ( SetForeignTicker "~SignalArrays" ) )
{
    SignalArray    NzForeignTicker "~SignalArrays""V"False ) );
    Buy         IIfSignalArray 11);
    Sell         IIfSignalArray 21);
    Short         IIfSignalArray 41);
    Cover         IIfSignalArray 81);

    BuyPrice     NzForeignTicker "~SignalArrays""O" ) );
    SellPrice     NzForeignTicker "~SignalArrays""H" ) );
    ShortPrice    NzForeignTicker "~SignalArrays""L" ) );
    CoverPrice     NzForeignTicker "~SignalArrays""C" ) );

    PlotShapesIIfBuyshapeSmallUpTriangleshapeNone ), 50BuyPrice);
    PlotShapesIIfSellshapeSmallDownTriangleshapeNone ), 40SellPrice);
    PlotShapesIIfShortshapeHollowDownTriangleshapeNone ), 40ShortPrice);
    PlotShapesIIfCovershapeHollowUpTriangleshapeNone ), 50CoverPrice);

}
RestorePriceArrays();

PlotC""1128 );
_SECTION_END();

Quick-Order Bar (QBar)

Because the project has now reached a stage where it requires the Control panel to function, I have not included demo code at this time. The Control panel will be covered in one of the next posts and, when that is up and running, it will be easier to test most of the features of the TDash Graphical Interface.

The QBar is a small tool bar that tracks the Y-position of the cursor. It is designed for fast real-time trading and has only three buttons: Buy, Sell, and Reverse. You can add/delete buttons, for example, you could add a button to Cancel the last order placed. Order parameters are set in the Control panel.

The main advantage of the QBar is that you can place an order with a single-click. You do not have to move your eyes away from the last bar on your chart to place the order, you simply move your mouse cursor to the required price and click.

The QBar and the Order Markers share some functionality. The main difference is that the QBar is designed for placing new orders, and that the Order Markers are designed to display and modify pending orders.

The QBar has two display modes:

  1. Auto-Hide Off. The QBar displays continuously. When the cursor moves out of the QBar area the QBar remains in its last position.
  2. Auto-Hide ON. The QBar displays only when the cursor is in the Qbar area, i.e., in the vertical column space reserved for the QBar. When the cursor leaves the QBar area, it remains positioned at the last price selected. This mode allows the display of trade and system information in the QBar area when the QBar is auto-hidden.

The QBar also has an “Auto Modify” mode. When this is turned on you can place an order and, holding down the left mouse button, drag the QBar to a new price. As long as you keep the mouse button down the order on the TWS will be modified, in real-time, each time the price changes.

Below is a short video that shows what the QBar looks like. I removed the Order markers while working on the QBar.

Processing TDash Commands

If you watched the small video in earlier post on Order Markers you may have noticed that whenever the mouse cursor hovered over, or a mouse button was clicked while over an Order Marker, the “TDashAction” variable shown at the right top of the video changed. This is a unique variable that is solely responsible for communicating TDash Action to the Auto-Trading module. This method allows TDash to be linked to your current trade automation module. This post explains how this works.

Because the cursor can only be in one location at a time, all TDash commands are exclusive. Hence we do not need to assign individual names to each parameter, instead we can use a single string variable to tell us the name of the button and action performed. Compared to using traditional Param()s, where each Param() assigns a value to a unique variable, this eliminates about 50-100 variable names, and allows us to use a single Switch() command to process the commands. It also makes code easier to read, modify, and maintain.

To make this work requires that buttons are organized as follows:

  1. Buttons are assigned to uniquely named groups like “Markers”, “Menus”, “Trading”, “Setup”, “Account”, “Status”, etc. For example, the Order Markers covered in the previous post would fall in the “Marker” group, the forthcoming QBar controls will fall in the “QBar” group, etc.
  2. All buttons must have an upper “Header”, and may have an optional lower “Label”. The visible header and label is unique to the group it belongs to. A typical example would be a Buy @ LMT button, “Buy” would be the Header, and “LMT” would be the lower Label. In this case the lower label may take on different values, such as “MKT”, “STP”, and “STPLMT”. Header-only buttons are typically used for trigger functions which have no changing state to display, for example “Reset” or “Abort” buttons.
  3. Since all buttons respond to mouse movement and button clicks, buttons can have four possible possible action states:
      Hover: the cursor hovers over the button
      LeftClick: the left button is clicked while the cursor is over the button
      LeftDown: the left button is held down while the cursor is over the button (Dragging)
      LeftRelease: The left button is released while over the button

These four action states are encoded by prefixing a “~” for a Leftclick, “~~” for a LeftDown condition, and “~~~” for the LeftRelease. Concatenating the action prefix, group name, visible header string, and visible Label string into a csv string provides us with a “TDashAction” string that uniquely defines the button and its transient states. Because for multi-state buttons the lower label describes the state of the button, this string also tells us the state of the button. For additional clarity we can also change the button-color to indicate its state.

For example, dragging the Buy Order Marker shown in the previous post would, over consecutive executions, generate the following sequence of changes in the TDashAction variable:

    Marker,Buy // cursor hovers over the button
    ~Marker,Buy // left-click on the button is detected
    ~~Marker,Buy // the left mouse button is down on the button (dragging)
    ~~~Marker,Buy // the left mouse button is released while over the button

This type of encoding is used for all TDash buttons and controls. While this method may be a little esoteric it makes reading the code very easy, and allows us to add controls without having to dream up a new set of variable names. We just look at the displayed TDashAction string and we know the name for the action to be decoded in our Switch().

This method makes decoding TDash action using a switch() simple and easy to debug. Note that in most cases not all possible states need a case statement. A major advantage of using a Switch() statement is that one can, at any time, easily add and/or remove buttons.

Below is a simple Switch(0 to decode the “Marker Buy” command used in the previous post:

TDashAction =  VarGetText"TDashAction" );

switch ( TDashAction )
{

case "Marker,Buy":
    // perform tasks in response to hovering over the Marker
    _TRACE"TDashAction: " TDashAction );
    break;

case "~Marker,Buy":
    // Perform action in response to a left click on the Marker
    _TRACE"TDashAction: " TDashAction );
    break;

case "~~Marker,Buy":
    // React to dragging the marker
    _TRACE"TDashAction: " TDashAction );
    break;

case "~~~Marker,Buy":
    // Perform action in response to releasing the mouse button
    _TRACE"TDashAction: " TDashAction );
    break;
}

Introducing Order Markers

In the previous post I developed a method to link prices between the TDash window and the Main Chart window. In this post I show how I used this technique to develop “Order Markers” that can be used to modify orders, display order status, and cancel orders.

In the final TDash Order Markers will be created automatically whenever an order is found Pending. The initial orders will be placed with another tool (the QBar) that will be covered in a subsequent post.

To test the Order markers you need to create two side-by-side windows in AmiBroker. If you use multiple monitors you can place the Main Chart at the right side of the left monitor, and TDash on left side of the right monitor. This will work fine and gives you a lot of space to work with. However, depending on the monitor sizes and their horizontal positions, the Markers may not appear perfectly at the same height from your desk, however the prices shown and plotted will be accurate.

To install the programs, first open and copy this TDashInclude.afl include file (not listed below due to its size) to the AmiBroker default Include folder. The TDashInclude.afl file is a development version and contains a lot of code that you don’t need at this time – please ignore the unused functions. The include will be cleaned up when the functions all work as they should. Next Apply MainMarkers.afl (listed below) to the main chart at the left, and Apply TDashMarkers.afl (listed below) to the TDash window at the right.

The demo code presented here creates three Order Markers, each can be moved by dragging it to a new price level. At start-up the Order markers will be parked at the top of the TDash window, just click on them to activate them. Remember that in the final TDash inactive Order Markers will not show, only Order Markers for Pending orders will show.

The price line and value displayed on the main chart will track the Order Marker, and the Order Markers will track a changing Y-axis on the Main chart. To allow setting an exact price Increment (+) and Decrement (-) controls are provided on the Marker. To cancel the order click the Cancel control (X). In this test cancel will only gray-out the marker and park it at the top of the TDash window, in the final program a canceled Marker will disappear after the cancellation has been confirmed.

It should look and work as shown in this video below.

// TDashMainMarkers.afl

PersistentPath StaticVarGetText"~PersistentPath" ); // Global
if ( PersistentPath == "" ) 
{
    PersistentPath "PersistentVariables\\";
    fmkdirPersistentPath );
    StaticVarSetText"~PersistentPath"PersistentPath );
}

function PersistentVarRemoveVarName )
{
    global PersistentPath;
    Fn PersistentPath VarName ".pva";
    fh fdeleteFn ) ;
    return fh;
}

function PersistentVarGetVarName )
{
    global PersistentPath;
    fh fopenPersistentPath VarName ".pva""r" );
    if ( fh )
    {
        String fgetsfh );
        fclosefh );
        Number StrToNum( String );
    }
    else Number Null;
    return Number;
}

function PersistentVarSetVarNameNumber )
{
    global PersistentPath;
    String NumToStrNumber );
    fh fopenPersistentPath VarName ".pva""w" );
    if ( fh )
    {
        fputsStringfh );
        fclosefh );
    }
    return fh;
}

function MAinLinkWitTDashMArkername )
{
    local xMY;
    // On first run try to get persistent price
    if ( IsNullStaticVarGet"~MarkerInit_" MArkerName ) ) )
    {
        Price PersistentVarGet"MainChartPrice_" MarkerName );
        if ( Price )
        {
            Miny Status"axisminy" );
            Maxy Status"axismaxy" );
            PxChartRange Status"pxchartheight" );
            Pricerange MaxY MinY;
            TDashYPixels = ( MaxY Price ) * PxChartRange Pricerange 5;
            StaticVarSet"~TDashYPixels_" MarkerNameTDashYPixels );
            StaticVarSet"~MainChartPrice_" MarkerNamePrice );
            StaticVarSet"~Exists_" MarkerNameTrue );
        }
        StaticVarSet"~MarkerInit_" MArkerNameTrue );
    }

    MArkerEnabled NzStaticVarGet"~Exists_" MarkerName ) );
    if ( MArkerEnabled )
    {
        pxWidth Status"pxWidth" );
        Miny Status"axisminy" );
        Maxy Status"axismaxy" );
        pxchartright Status"pxchartright" );
        pxchartbottom Status"pxchartbottom" );
        pxcharttop Status"pxcharttop" );
        PxChartRange Status"pxchartheight" );
        pxheight Status"pxheight" );
        Pricerange MaxY MinY;

        // TDash releases get TDash y-pixel value and convert to price
        if ( NzStaticVarGet"~LeftButtonRelease" ) ) )         
        {
            TDashYPixels NzStaticVarGet"~TDashYPixels_" MarkerName ) );        // Y pixels from TDash window
            Price Maxy - ( Pricerange PxChartRange * ( TDashYPixels ) );
            StaticVarSet"~LeftButtonRelease"False );
            StaticVarSet"~MainChartPrice_" MarkerNamePrice );
        }
        else
            if ( NzStaticVarGet"~NowDragging_" MarkerName ) ) == )     // TDash marker tracks main window
            {
                Price NzStaticVarGet"~MainChartPrice_" MarkerName ) );
                TDashYPixels = ( MaxY Price ) * PxChartRange Pricerange 5;
                StaticVarSet"~TDashYPixels_" MarkerNameTDashYPixels );         // Y pixels calculated from price in main chart
            }
            else
                if ( NzStaticVarGet"~NowDragging_" MarkerName ) ) )             // Main window tracks TDash window
                {
                    TDashYPixels NzStaticVarGet"~TDashYPixels_" MarkerName ) );
                    Price Maxy - ( Pricerange PxChartRange * ( TDashYPixels ) );
                    StaticVarSet"~MainChartPrice_" MarkerNamePrice );  // Price for main chart calculated from TDash y pixels
                }

        Price NzStaticVarGet"~MainChartPrice_" MarkerName ) );

        MArkerColor  NzStaticVarGet"~MarkerColor_" MarkerName ) );
        MArkerTextColor NzStaticVarGet"~MarkerTextColor_" MarkerName ) );
        MArkerPenColor NzStaticVarGet"~MarkerPenColor_" MarkerName ) );

        GfxSetBkMode);
        GfxSelectPenMArkerColor1);
        GfxMoveTo0TDashYPixels );
        GfxLineTopxwidthTDashYPixels );

        GfxSetBkMode);
        GfxSetBkColorMArkerColor );
        GfxSetTextColorMArkerTextColor );
        GfxSelectFont"Lucida Console"FontSize 10FontWeight 700 );
        GfxDrawTextNumToStrPrice1.2False ), pxchartrightTDashYPixels FontSizepxwidthTDashYPixels Fontsize37 );
    }
}

function DrawQBarPriceLine()
{
    if( NzStaticVarGet"~QBarVisible" ) ) )
    {
        pxWidth Status"pxWidth" );
        Miny Status"axisminy" );
        Maxy Status"axismaxy" );
        pxchartright Status"pxchartright" );
        pxchartbottom Status"pxchartbottom" );
        pxcharttop Status"pxcharttop" );
        PxChartRange Status"pxchartheight" );
        pxheight Status"pxheight" );
        Pricerange MaxY MinY;

    QBarColor  NzStaticVarGet"~QBarColor" ) );
    QBarTextColor NzStaticVarGet"~QBarTextColor" ) );
    QBarPenColor NzStaticVarGet"~QBarPenColor" ) );

    QBarColor NzStaticVarGet"~ActionColor" ) );
    QBarTextColor NzStaticVarGet"~ActionTextColor" ) );

    QBarYPixel NzStaticVarGet"~QBarYPixel" ) );        // Y pixels from TDash window
    Price Maxy - ( Pricerange PxChartRange * ( QBarYPixel ) );
    GfxSetBkMode);
    GfxSelectPenQBarColor1);
    GfxMoveTo0QBarYPixel );
    GfxLineTopxwidthQBarYPixel );

    GfxSetBkMode);
    GfxSetBkColorQBarColor );
    GfxSetTextColorQBarTextColor );
    GfxSelectFont"Lucida Console"FontSize 10FontWeight 700 );
    GfxDrawTextNumToStrPrice1.2False ), pxchartrightQBarYPixel FontSizepxwidthQBarYPixel Fontsize37 );
    }
}


_SECTION_BEGIN"Global Parameters" );
if ( ParamTrigger"Clear StaticVar (TEST)""CLEAR" ) ) // to simulate start up
{
    StaticVarRemove"*" );
}

if ( ParamTrigger"Clear Persistent Var""CLEAR" ) ) // to simulate start up
{
    PersistentVarRemove"MainChartPrice_" "TARGET" );
    PersistentVarRemove"MainChartPrice_" "MAIN" );
    PersistentVarRemove"MainChartPrice_" "STPLOSS" );
}
_SECTION_END();

GfxSetOverlayMode);
MAinLinkWitTDash"BUY" );
MAinLinkWitTDash"MAIN" );
MAinLinkWitTDash"SELL" );
DrawQBarPriceLine();
PlotC""colorBlackstyleBar );
RequestTimedRefresh0.1 );
// TDashMarkers
#pragma nocache
#include <TDashInclude.afl>

_SECTION_BEGIN"TDASH MARKERS" );
setBackGroundColorParamColor"TDash background"colorLightBlue ) );

// Set default font size
FontSize NzStaticVarGet"~FontSize" ) );
FontName "ARIAL";
Fontweight 700;
GfxSelectFontFontNameFontSizeFontWeight );

// Columns. Most parameters are for experimentation and can be hard-coded
NumberCols Param"Number Button Columns"8120);
ButtonWidth Status"pxwidth" ) / NumberCols;
MaxButtonWidth Param"Max Button Width"1501500);
ButtonWidth MinButtonWidthMaxButtonWidth );
MaxMarkerWidth Param"Max Button Width"1501500);
MarkerWidth MinButtonWidthMaxMarkerWidth );

// Rows
NumberRows Param"Number Button Rows"20150);
ButtonHeight Status"pxheight" ) / NumberRows;
MaxButtonHeight Param"Max Button Height"501100);
ButtonHeight MinButtonHeightMaxButtonHeight );
MinButtonHeight Param"Min Button Height"501100);
ButtonHeight MaxButtonHeightMinButtonHeight );

MarkerHeight ButtonHeight;

TipsOn ParamToggle"Help Tips""HIDE|SHOW");
ShowTips ParamToggle"Help Tips""HIDE|SHOW");
ClickSound ParamToggle"Click Sound""OFF|ON");

SetChartOptions0chartHideQuoteMarker );
GfxSetOverlayMode2  );

// mouse
MX GetCursorXPosition);
MY GetCursorYPosition);
LeftClick NzStaticVarGet"~LeftClick" ) ); //Left Click SV set at end of code
LeftDown GetCursorMouseButtons() == 1;

OnTDash     = !IsNullMX ) AND !IsNullMY );
LeftClick LeftClick AND OnTDash;    // Only accept mouse clicks when cursor is inside TDash window
LeftDown = ( LeftDown OR LeftClick ) AND OnTDash;

PrevLeftButtonState NzStaticVarGet"~LeftButtonState" ) );
LeftButtonRelease LeftDown PrevLeftButtonState AND OnTDash;
StaticVarSet"~LeftButtonState"LeftDown ); // Release is also detected outside TDash window

// testing markers
MArkerTip "Drag Price Marker to desired price Level. Click 'X' to cancel, '+' to increment, and '-' to decrement";
ServiceMarkers"TARGET"colorBrightGreencolorBlackcolorBlackMArkerTip );
ServiceMarkers"MAIN"colorBluecolorWhitecolorBlackMArkerTip   );
ServiceMarkers"STPLOSS"colorRedcolorBlackcolorBlackMArkerTip   );

// testing
AddRowTogfxTitle"                TDashAction: "+VarGetText"TDashAction"), colorBlackFontSize=10 );

// The following lines are always located at the end of the code 
ShowTipBrushColor colorYellowOutlineColor colorBlackTextColor colorBlackTipWidth 150TipHeight 1008TipsOn );
LeftClick GetCursorMouseButtons() == 9;
StaticVarSet"~LeftClick"LeftClick );
RequestTimedRefresh0.1 );
_SECTION_END();

Linking the TDash Window with the Main Chart

The most important requirement for a Trading Dashboard is to be able to place orders by dragging price markers and have these prices reflected on your main chart. To accomplish this relationship, the Main Chart and TDash windows must be bi-directionally linked so that changing prices in one window are being tracked in the other.

If both the Main chart and Trading Dashboard window had an identical price range and window size, this could simply be done by sharing the price and pixel ranges using Static Variables. However, to make this work in a multi window layout where windows may not be aligned accurately, or when the TDash window is located on another monitor, is a little more complex.

The problem can be solved by aligning the upper edge of the windows with the upper edge of the AmiBroker window, and use this as the common reference to calculate prices and pixel values.

The code below demonstrates how the Y-Pixel position and prices, between the Trading Dashboard window and the main chart, can be made to track each other. To apply this technique in an actual application you would want to add graphical price markers that you can drag, start up initialization, and provide a fine-adjustment to set prices exactly. Some of this will be covered in the next post.

To test the code, create a two-window Layout, arrange them horizontally, apply the TDashLinkDemo code in the right window, and apply the MainChartLinkDemo code in the left window. Since this test code is not initialized at start-up, you need to click in the TDash window to set an initial price and make the price-line appear.

Click the left mouse button in the TDash window at a new price level to set the price-line to a different value. Click and hold the left mouse button down to drag the price to a new value. Note that the price-line in the main chart window tracks all changes. Price lines would be used to display pending orders and to enable you to set order prices with respect to real-time chart patterns.

When you release the left mouse button in the TDash window it locks in the current price. The only way to change the price is by clicking or dragging it in the TDash window.

If you drag the Y-axis in the main chart the corresponding price-line in the TDash window tracks this movement. This is needed to keep price markers in the TDash window in sync when the main chart y-axis changes.

In a multi-threading environment, where formulas execute asynchronously, we cannot assume that transient conditions saved in a Static Variable in one window will always be detected in another. To ensure reliable mouse-click detection in the Main chart program, the “~LeftButtonRelease” Static Variable, which is set in the control window, is reset in the main chart after it has been detected. This way no triggers will ever be missed.

The short video below illustrates how this works and what it looks like in AmiBroker.

// TDashLinkDemo

function LinkWithMainChart()
{
MX GetCursorXPosition);
MY GetCursorYPosition);
OnTDash = !IsNullMX ) AND !IsNullMY ); // Is cursor in TDash?
LeftClick GetCursorMouseButtons() == AND OnTDash;
LeftDown = ( GetCursorMouseButtons() == OR LeftClick ) AND OnTDash;
PrevLeftDownState NzStaticVarGet"~LeftDown" ) );
LeftButtonRelease LeftDown &ltPrevLeftDownState;
StaticVarSet"~LeftDown"LeftDown );

if ( LeftClick StaticVarSet"~NowDragging"True );
else if ( LeftButtonRelease )
{
StaticVarSet"~NowDragging"False );
StaticVarSet"~LeftButtonRelease"True );
}
if ( NzStaticVarGet"~NowDragging" ) ) ) StaticVarSet"~TDashYPixels"MY );
}

function DrawTDashPriceLine()
{
pxWidth Status"pxWidth" );
TDashYPixels StaticVarGet"~TDashYPixels" );
GfxSetBkMode);
GfxSelectPencolorRed1);
GfxMoveTo0TDashYPixels );
GfxLineTopxwidthTDashYPixels );
}

function TDashDisplayPrice()
{
MY GetCursorYPosition);
TDashYPixles StaticVarGet"~TDashYPixels" );
MAinChartPrice NzStaticVarGet"~MainChartPrice" ) );
GfxSetTextAlign) ;
GfxSelectFont"Lucida Console"12600 );
GfxTextOut"TDash price: $" NumToStrMAinChartPrice1.2False ), 0TDashYPixles );
GfxTextOut"TDash Pixels: " NumToStrTDashYPixles1.0False ), 0TDashYPixles 20 );
}

RequestTimedRefresh0.1 );
GfxSetOverlayMode2);
LinkWithMainChart();
DrawTDashPriceLine();
TDashDisplayPrice(); // Test only

// MainChartLinkDemo

function LinkWithTDash()
{
pxWidth Status"pxWidth" );
Miny Status"axisminy" );
Maxy Status"axismaxy" );
Pricerange MaxY MinY;
pxchartbottom Status"pxchartbottom" );
pxcharttop Status"pxcharttop" );
PxChartRange Status"pxchartheight" );
pxheight Status"pxheight" );

if ( NzStaticVarGet"~LeftButtonRelease" ) ) )
{
TDashYPixels StaticVarGet"~TDashYPixels" );
PricePerPixel Pricerange PxChartRange;
MAinChartPrice Maxy - ( PricePerPixel * ( TDashYPixels ) );
StaticVarSet"~LeftButtonRelease"False );
StaticVarSet"~MainChartPrice"MAinChartPrice );
}
else
if ( NzStaticVarGet"~NowDragging" ) ) == )
{
MAinChartPrice NzStaticVarGet"~MainChartPrice" ) );
PixelPerprice PxChartRange Pricerange;
PriceToPixelvalue = ( MaxY MAinChartPrice ) * Pixelperprice 5;
StaticVarSet"~TDashYPixels"PriceToPixelvalue );
}
}

function DrawTDashPriceLine()
{
pxWidth Status"pxWidth" );
TDashYPixels StaticVarGet"~TDashYPixels" );
GfxSetBkMode);
GfxSelectPencolorRed1);
GfxMoveTo0TDashYPixels );
GfxLineTopxwidthTDashYPixels );
}

function MainDisplayPrice()
{
pxWidth Status"pxWidth" );
TDashYPixels StaticVarGet"~TDashYPixels" );
MAinChartPrice NzStaticVarGet"~MainChartPrice" ) );
GfxSetTextAlign) ;
GfxSelectFont"Lucida Console"12600 );
GfxTextOut"  Main price: $" NumToStrMAinChartPrice1.2False ), pxwidthTDashYPixels );
GfxTextOut"Main Pixels: " NumToStrTDashYPixels1.0False ), pxwidthTDashYPixels 20 );
}

RequestTimedRefresh0.1 );
GfxSetOverlayMode);
LinkWithTDash();
DrawTDashPriceLine();
MainDisplayPrice(); // Test only

PlotC""colorBlackstyleBar );

Designing a Real-Time Trading Dashboard

In this category I will document my progress in developing a Real-Time Trading Dashboard (TDash). This is a one-man project and it will strongly reflect my personal needs and likes. Posts will appear as significant parts are completed. There may be many revisions and you should expect some bugs. My primary reason for sharing this work is to try and introduce some new ways of doing things.

No doubt, if you inspect my code, you will come across many code snippets and techniques you have seen before. While I respect proprietary code, I readily make use of code I find in the public domain. I herewith say “Thank You” to all those who answer questions and share code on the public forums; without their generosity I might not have taken on this project.

This is an advanced project and, when all is working as planned, the program could contain several thousand lines of code. It may take several months for the project to reach functionality. Since it would take too much time to explain everything in detail, focus will be on explaining ideas and on showing you how to use the functions developed. At this time code is written for single stock and single system operation.

Designing a Real-Time Trading Dashboard (TDash) may seem simple at first glance but once you start you’ll discover there are many problems to solve. There are so many different ways to go about it that just deciding on the best way often takes a significant amount of time. In fact it often takes more time to decide on how to do things than to write the code.

I tried to consult other traders on what are desirable features, however, almost everyone wants things done differently. To save time I decided to just do it my way. Most features can easily be adjusted and you are encouraged to use ideas and code you like, and develop your own TDash.

Some may tell you that the TWS offers everything they need and, for some traders, this may be so. Please explore the advanced features of the TWS before rejecting it, it does have a lot of hidden features.

The TDash system will have one program for the TDash window, one for the Main Chart window, and one or more include files. When you look into the include files, you may see functions which are not called at this time. Some of these unused function are still in their development stage. Unless you use #Pragma NoCache, the length of the include files will not significantly affect your program’s execution time.

Work will be divided into two major parts: The graphical interface (gfx) and order processing for Bar-Replay, and the IBc.

My personal design objectives are:

  1. Produce a least-effort and low-stress Real-Time Trading Dashboard. Above all, it must be fun and intuitive to use.
  2. It will be designed for fast (minute time frame) Intraday trading, but it should also be useful in EOD trading.
  3. Consolidate all trading controls (for IBc/TWS), System setup, and Chart management in a single graphical user interface executing in its own window.
  4. Place orders and set prices with reference to the main chart, i.e., not by entering prices on a TWS executing on another monitor.
  5. Have all code in include files so that the Trading Dashboard can be used with any main chart and is easily updated.
  6. Allow it to be used with an Interactive Brokers account, and in Bar-Replay.
  7. The project will initially be designed to trade a single stock. Portfolio features may be added later.
  8. Eventually I would like to add features that would make using the Trading Dashboard feel like playing a video game, i.e., it should provide trading hints (rules) and performance feedback to promote better trading.

Deciding on a Layout

The Layout below shows four windows, but you can use as many as you like. The only requirement is that the TDash is to your right of the main chart and that these two windows are accurately aligned at the top of the AmiBroker window.

To be able to drag orders and price markers in one window, and have the prices accurately reflected in another, they must share a common reference. I will use AmiBroker’s upper window edge for common reference. This requires that both windows are accurately aligned at the upper edge of the window. This is easily done by dragging windows by their upper edge or corner until they meet the top of the AmiBroker window.

The Trading dashboard is located to the right of the main chart. These are the only Layout requirements that you must adhere to.

The first requirement is to develop code that links both windows so that dragging a price marker in the TDash window will track a price-line in the main chart and, if the main chart scales up or down, the markers in the TDash window track the price in the Main Chart. This two-way tracking must work independent of the settings of the TDash window (Ticker, zoom condition, Y-axis range, etc.). The next post will show how this can be done.

For best performance, you should enable the higher chart refresh rates. Tomasz explained how to do this in post 151255 on the main AmiBroker list. Please read the correction in comments below. Making errors while editing your Registry can cause serious computer problems, if you haven’t done this before, please seek professional help.

Collecting and Plotting Ticks v2

The program below shows how you can collect tick-based data and display it in its own pane.

Note: There was an error in the earlier version that is fixed, v2 only collect ticks if the Total Volume for the day changes. Feel free to report any further problems (on the main list or by private email please).

The example shows Last, Bid and Ask price. You can repeat the four lines that plot each price as often as you like to add other types of tick-based data. A typical chart looks like this:

function TickArray2TickerTickPriceNameNumTicksReset )
{
Tickprice GetRTDataForeignTickPriceNameTicker );
PrevVolume NzStaticVarGet"~TotalVolume_" TickPriceName ) );
TotVol GetRTDataForeign"TotalVolume"Ticker );
StaticVarSet"~TotalVolume_" TickPriceNameTotVol );

if ( TotVol &gtPrevVolume )
{
InIndicator Status"Action" ) == 1;
StaticVarSet"LastQuotePrice"TickPrice );
TA Null                                            // Clear Output array
NumTicks MinBarCount 2NumTicks ); // Stay in array size
for ( NumTicks&gt;= 0n-- )
{
StaticVarGetTickPriceName + ( ) );
StaticVarSetTickPriceName n);
TABarCount n] = T    // Fill return array
}
StaticVarSetTickPriceName 0TickPrice );
TA[BarCount-1] = TickPrice;
}
else
{
TA Null                                               // Clear Output array
NumTicks MinBarCount 2NumTicks );     // Stay in array size
for ( NumTicks&gt;= 0n-- )
{
StaticVarGetTickPriceName );
TABarCount n] = T    // Fill return array
}
}
return TA;
}

function TickArraysResetTickPriceName )
{
global NumTicks;
StaticVarSet"Init"+TickPricename);
for ( NumTicks&gt;= 0n-- ) StaticVarRemoveTickPriceName  );
}

GraphXSpace 20;
TickerName Name();
Clear        ParamTrigger"Clear Tick Charts""CLEAR" );
NumTicks    Param"Tick-Array Length"2031000);

TickPriceName "Last";
if ( IsNullStaticVarGet"Init"+TickPriceName ) ) OR Clear TickArraysResetTickPriceName );
TA TickArray2TickerNameTickPriceNameNumTicksClear);
PlotTATickPriceName11);

TickPriceName "Bid";
if ( IsNullStaticVarGet"Init"+TickPriceName ) ) OR Clear TickArraysResetTickPriceName );
TA TickArray2TickerNameTickPriceNameNumTicksClear);
PlotTATickPriceName41);

TickPriceName "Ask";
if ( IsNullStaticVarGet"Init"+TickPriceName ) ) OR Clear TickArraysResetTickPriceName );
TA TickArray2TickerNameTickPriceNameNumTicksClear);
PlotTATickPriceName51);

Setting prices with your cursor

To place LMT or STP orders quickly is easiest done by moving the horizontal cursor-line over the desired price and making a left mouse click. The demo code below shows how you can lock in prices this way. To Chart-Trade additional code must be added to make the click perform one task of many, and only do so when required. When you Apply the code below to an indicator you will see a dashed line at the cursor price. This dashed line refreshes only once per second with a local database, however, it will speed up when your chart is refreshed more frequently when working with live data.

Suppose you want to place a LMT order at $60.00 on the chart below. To do this you move the dashed line over the $60.00 price and click the Left mouse button. This will place a stationary solid line. You can now move your cursor to another price and click again, the solid line will move to this new location. In actual trading you can follow the price in real time and adjust your Limit prices so that they stay exactly where you want them. The chart produced by this demo code looks like this:

placingline.png

In Chart-Trading the first click that places the solid line would also place your order on the TWS. Each subsequent click would modify the order to the new price. This way you can adjust several prices on your chart, setting and moving around Entries, Targets, Stops, etc.

RequestTimedRefresh);
SetChartOptions2chartHideQuoteMarker );
LButtonDown GetCursorMouseButtons() == 9;
MousePrice GetCursorYPosition();

if ( MousePrice )
{
    StaticVarSet"MousePrice"MousePrice );
    if ( LButtonDown )
        StaticVarSet"ClickedMousePrice"MousePrice );
}

LB BarCount 1;
MousePrice NzStaticVarGet"Mouseprice" ), Null );
ClickedMousePrice StaticVarGet"ClickedMousePrice" );

PlotC""1128 );
PlotMousePrice""colorWhitestyleNoRescale styleDashed styleNoLabel00);
PlotText"CURSOR " NumToStrMousePrice[LB], 1.2 ), LB 5MousePrice[LB], colorBlackcolorWhite );
PlotClickedMousePrice""colorBlackstyleNoLabel styleNoRescale00);
PlotText"ORDER PLACED $" NumToStrClickedMousePrice[LB], 1.2 ), LB 5ClickedMousePrice[LB], colorBlackcolorWhite );

Parsing TWS Error messages

Here is some simple code you can use to enable you to start developing your own error-parsing functions. In this example, error messages are hard-coded to facilitate code development, i.e. you don’t have to be live with IB to test it. I suggest that you inspect the IB list of error messages, copy the errors that are important to you, and add them to the hard-coded list. Since IB doesn’t list the error messages in the exact format you will receive them, you may have to place some orders with errors to see their exact format. When Applied to an Indicator, the code can be exercised by selecting the Error Message with the slider in the Param window. It will display the current error and parsing results in the Chart Title. Be sure to set Parameter properties to wrap the Title.

parsingerrors.png

Param"Example Error Msg"0010);
switch ( )
{
case 0:
    TWSLastErrorMsg "ID=15071. Error 135. Can't find order with id =:15071";
    break;
case 1:
    TWSLastErrorMsg "ID=2094, Error 103, Duplicate ORder id";
    break;
case 2:
    TWSLastErrorMsg "ID=-1, Error 1102, Connectivity between IB AND TWS Has been restored - data MAintained";
    break;
case 3:
    TWSLastErrorMsg "ID=-1, Error 2100, New account data requested";
    break;
case 4:
    TWSLastErrorMsg "ID=-1, Error 1100, Connectivity between IB AND TWS has been lost";
    break;
case 5:
    TWSLastErrorMsg "Connection established OK, Next Order Id=2080";
    break;
case 6:
    TWSLastErrorMsg "ID=6125. Error 202. Order Canceled - reason:";
    break;
default:
    TWSLastErrorMsg "";
    break;
}

function GetErrorCodeTWSLastErrorMsg )
{
    StrFindTWSLastErrorMsg" Error " );
    ErrStr StrMidTWSLastErrorMsg6);
    ErrNum    StrToNumErrStr );
    ErrStr    NumToStrErrNum1.0False );
    return ErrStr;
}

function ExtractErrorIDTWSLastErrorMsg )
{
    StrFindTWSLastErrorMsg"ID=" );
    IDStr StrMidTWSLastErrorMsg2);
    IDNum    StrToNumIDStr );
    IDStr    NumToStrIDNum1.0False );
    return IDStr;
}

TWSErrorCode GetErrorCodeTWSLastErrorMsg );
TWSErrorOrderID ExtractErrorIDTWSLastErrorMsg );
Title "\n" +
        "Extracting OrderIDs and Error codes from TWS Error Messages" "\n" +
        "Example Error Msg: " TWSLastErrorMsg "\n" +
        "        Error Code: " TWSErrorCode "\n" +
        " TWS Error OrderID: " TWSErrorOrderID;
Next Page »