Free Indicator & Programming Tutorial: Horizontal Lines Drawing Objects on Multiple Charts
Updated: Aug 30, 2021
This tutorial is based on an inquiry from the TradeStation Community Forum.
The fellow uses multiple time frame charts and wants horizontal lines (HL) he draws at a price level to be replicated across other charts at the same level.
Here is a short preview of what that indicator is going to work like:
This is going to be a more advanced development. If you are very new to EasyLanguage programming, I would recommend you to start with one of the earlier EL tutorials.
You can either download the coding for free and simply have fun using the indicator or go through the creation process step by step if you have the intention to learn how to program in EasyLanguage and reuse code snippets that simplify and speed up the development of future indicators or strategies.
In case you only download the coding, make sure you set the Event in the Properties as explained for AnalysisTechnique_UnInitialized in Step 13: Event Handler 3.
The programming is, as usual, done in Object Oriented EasyLanguage (OOEL).
Sharing Group: "auto" is the default value and replicates the horizontal lines based on the symbol (AAPL, @ES, etc.). However it can also be used to synchronize horizontal lines on different symbols. You can freely define sharing group names and where the indicator is formatted with an equal Sharing Group input, the horizontal lines are synchronized.
The indicator is required to fulfil the following distinct functionalities:
Detect new user drawn horizontal lines. #HLDrawing_Detect
Detect updates/erasure of existing lines. #HLUpdate_Detect
Convert the properties (price, color, etc.) of the horizontal line into a transmittable format (XML). #HLProperties_Convert
Send and receive horizontal lines in the transmittable format. #HL_Exchange
Draw horizontal lines based on the properties received. #HLReplicate
User input to specify sharing "groups". #SharingGroup
The tags are used in the coding to refer to the respective functionality.
We use to following OOEL namespaces/classes in order to meet the requirements:
charting to detect user interaction with the chart on objects on the chart.
elsystem.drawing & elsystem.drawingobjects for interaction/creation with drawing objects.
elsystem.collections for vectors and global dictionary (enables data exchange between charts).
elsystem.xml for transmitting horizontal lines properties as text.
For the first time in EasyLanguage tutorials on this blog, we will make use of Event Handlers.
On a high level, Event Handlers' coding is only executed on a certain event. That allows us to react on the drawing of a new horizontal line, a change of an existing one, etc.
Conceptionally, the exchange of the HLs works like this:
Multiple instances of the indicator on different charts share one common Global Dictionary (GD) object, depending on the Sharing Group. They all communicate bidirectionally with this GD object. Sending updates and reacting to updates from the GD. The GD is also managing the Instance Numbers. That means, every new indicator instance has to register with the GD and receives a distinct Instance Number. The GD, being of type Dictionary is organized with keys and their respective values. InstNo is one key in the GD and holds the highest Instance Number that was registered.
Then, for every HL, a key with its name is added to the GD. The name of the HL follows the pattern <indicator name>-<sharing group>_<local HL number>_<owner instance number>.
The HL, represented as an XML string is put in the according value.
For orientation, here is the outline of the completed indicator coding. As usual we work with regions to structure the code. In this case we also have nested regions.
Step 1: Code header, namespace references and user inputs
As the coding is following the same template consistently, I try not to repeat the same information in every tutorial. If you don't understand parts of the coding, you will likely find the answer in one of the earlier EL tutorials. Otherwise feel free to post your Question in the comments below.
Keeping it short here, iSharingGroup is defined as the only input and enables the user to define on which charts the horizontal lines are shared.
Step 2: Global variables and constants
All the variables that we assign values to in Event Handlers must be declared IntrabarPersist. Variables that are type classes are automatically IntrabarPersist.
Also, a variety of constants is defined.
The second block represents the index numbers of the Vector object we are going to put into the Tag of the HorizontalLine object.
Tag is a property of the super class DrawingObject that can hold any arbitrary value that allows us to store additional info with a drawing object.
As we exchange the HL's data in the XML format between instances of the indicator, we also set up constants for the XML element and its attributes.
Step 3: Methods 1
Method void AnalysisTechnique_InitMain()
A little different than usual, we do not perform the initial tasks in the method directly. We set up a random interval Timer instead. This is done to prevent simultaneous access to the common Global Dictionary which causes errors. The Timer calls the Event Handler gTimer_Elapsed, were the initial operations are going to be coded. In other words, we postpone the initialization by a random number of milliseconds.
Method void HLs_Init()
Specific tasks for HLs in the GD or on the chart on activation of the indicator. It is part of the initial tasks and is called within gTimer_Elapsed.
Step 4: Methods 2 (region HL_Single_Operations)
Method void HL_Register(HorizontalLine liHL)
Register could be described as the "adoption" process of HLs on the chart. It is only called when the HL was drawn on the respective chart. Not for HLs that are replications that originate from other charts.
We set a name, assign the Event Handler HL_OnClick that we want to be called if the HL is clicked at and set the vector in the Tag property.
As a last step, method GD_HL_XML_HL_Modify is called to add the HL to the GD, to share it with other instances of the indicator.
Method void HL_Update(HorizontalLine liHL)
This method is called when an HL was changed by the user. We set the new Vector to its Tag and then propagate the update to the GD.
Method Vector HL_TagVector_Determine(HorizontalLine liHL)
In order to detect property changes of HLs, we need to keep a snapshot of the current properties. If some user action happens, we can compare this snapshot with the actual properties of the HL.
This method is determining and returning the Tag Vector with the current properties of a HL.
Style and Weight must be converted, as EL does not allow us to add them into a Vector.
The Color is also converted into a string format. This is done for the comparison. If we add a reference to the Color object, it will naturally not keep the value it had when we added it to the Vector. (It would also be possible to add a copy of the Color object instead).
Step 5: Methods 3 (region HL_Single_Operations)
Method Bool HL_TagVector_ProcessComparison(HorizontalLine liHL)
The already mentioned comparison of the Tag Vector with the actual HL properties.
Method Void HL_From_GD_Modify( HorizontalLine liHL, string liHL_Name )
This method creates and updates HL that come from the GD, i.e. from another chart.
First it reads the XML element of the HL from the GD by calling method GD_HL_XML_HL_Read.
If liHL is not null, it is a change for an already existing HL. In this case there is a check (from line 212) to make sure the program does not update on the chart that was updated by the user.
Step 6: Methods 4 (region HL_Single_Operations)
Method bool HL_Ownership_Check(string liHL_Name)
The Instance Number of the indicator is part of the HL's name. Therefore, we can check if the running instance of the indicator is the owner of the HL. Owner is defined as the chart on which the HL was drawn by the user.
Step 7: Methods 5 (region GlobalDictionary_HL_Operations)
Method XmlDocument GD_HL_XML_Document_Read(string liHL_Name)
EasyLanguage provides a collection of classes to effectively work with XML data.
While our XML is stored as a string in the GD, we use these classes to create and perform changes on the XML.
This method encapsulates the creation of a new XML document or the retrieval of an existing one from the GD.
XMLDocument represents an XML document, so we always want to create an object of it.
Depending on the HL's existence in the GD or not, we either load the value string from the respective GD key or we create a new root element with our constant cHLExchangeXMLRootName.
Method void GD_HL_XML_Document_Write(string liHL_Name, XmlDocument liXMLDocument)
We have just created the read method for the XMLDocument. Now we want to create a method to write our XML data into the GD. This method adds/updates the XML as a string in the GD.
Step 8: Methods 6 (region GlobalDictionary_HL_Operations)
Method void GD_HL_XML_HL_Modify(HorizontalLine liHL)
Modify the property attributes of the XML in GD for an HorizontalLine object parameter.
Method XMLElement GD_HL_XML_HL_Read(string liHL_Name)
Returns the XMLElement from the GD for a HL's name.
Step 9: Methods 7 (region GlobalDictionary_HL_Operations)
Method void GD_HL_XML_HL_PrintDocument(string liHL_Name)
This method is not called in the program. It is in preparation of troubleshooting and prints the XML string of a HL.
Method void GD_HL_HL_Remove(string liHL_Name)
Removes a HL from the GD
Method string GD_HL_InstanceNo_getNext()
Returns the next free indicator instance number from the GD.
Step 10: Methods 8
Method String DOID_getNext()
In the chapter Requirements, we discussed the pattern of the HL names. One component is <local HL number>. This number is managed by this method.
It ensures that the HL on a chart have distinct names.
Method String ColorToARGBString( Color liColor )
Converts a Color object into a comma separated ARGB string.
Method Color ARGBStringToColor( String liARGB )
Converts a comma separated ARGB string into a Color object. In fact the reverse operation of ColorToARGBString.
Step 11: Event Handler 1
Method void gTimer_Elapsed( Object sender, TimerElapsedEventArgs args )
The Event Handler for our delayed initial tasks.
giSharingGroup is defined either with the symbol or when not set to "auto" by the user defined name.
Next the ChartingHost and GlobalDictionary are instantiated and we register our Event Handlers for the events we want to react on.
Method void HL_OnClick( Object sender, DrawingObjectEventArgs args )
We want to keep a reference to the last clicked HL in the global variable gLastClickedHL.
More on why we need this a little later.
Step 12: Event Handler 2
Method void gCH_OnChartElementClick( Object sender, ChartElementClickEventArgs args )
This Event is raised for every click on the chart. Since we have no other Event available that specifically fires for additions and changes of drawing objects, we have to "manually" check for changes. This is also the reason, why we a keep a snapshot of the properties we want to watch in the Tag Vector.
Our update to the GD is therefore happening only on the next click after changing, for example, the color of the HL.
We read all the Horizontal Lines from the chart.
Then we loop at the list and check whether it is new (when it's Tag is null) or was changed (with method HL_TagVector_ProcessComparison)
Since this is executed on every click, we also use it to reset gLastClickedHL.
Step 13: Event Handler 3
Method void gCH_OnPreDeleteElement( Object sender, PreDeleteElementEventArgs args )
This Event is raised prior to every delete on the chart.
Therefore, we have a sequence of if conditions to make sure we are in the right context.
Since we have no access to what object is about to be deleted, we check at this point gLastClickedHL making sure the delete is for one of our HLs.
Method void AnalysisTechnique_UnInitialized( elsystem.Object sender, elsystem.UnInitializedEventArgs args )
In case, the indicator is turned off/removed from a chart or the chart is deleted, we want to delete the HLs it owns from the GD. We can use the AnalysisTechnique's UnInitialized Event. In order to have it executed it must be set in the Properties pane of the Analysis Technique.
Step 14: Event Handler 4 (region GlobalDictionary_HL_Events)
Method void gGD_HL_OnItemAdded( Object sender, ItemProcessedEventArgs args )
Whenever an item is added to the GD, this event is called.
The parameter ItemProcessedEventArgs args provides us access to the key of the added item. We only want to react on items the represent HLs. (e.g. not to the InstNo key).
Therefore, we check whether it starts with the first component of our HL name pattern.
Additionally, it is checked that we are not the owner of the HL.
Method void gGD_HL_OnItemChanged( Object sender, ItemProcessedEventArgs args )
Called on every change in the GD.
Again, we check the args.key for our name pattern, then search the HL that needs to be updated.
Method void gGD_HL_OnItemDeleted( Object sender, ItemProcessedEventArgs args )
Called on every deletion in the GD.
We check the args.key for our name pattern, then search the HL that needs to be deleted and delete it from the chart.
Step 15: Main Program
No processing on bar close or tick-by-tick is required. We only trigger the initialization of the indicator objects.
Download the complete free code from our member area here.
Feel free to post your thoughts, ideas, findings and questions in the comment section.
Don't want to miss future posts? Follow me on Twitter, Instagram or subscribe to blog notifications in "My Account" > "Settings".
For users with professional requirements, I started to offer a premium version without the limitations of the version above.
Please find all the details here: https://www.tradeatomy.com/products/Global-TrendLines
Update August 2021
The latest update for TradeStation 10 changed a few things that lead to a verification error. I updated the code to make it compatible with the latest update and also added a feature that enables you to delete the lines from any chart. You can delete a line by holding CTRL pressed and click on a line. The updated code can be downloaded here.