Class Layer1ApiSoundAlertMessage

java.lang.Object
velox.api.layer1.messages.Layer1ApiSoundAlertMessage
All Implemented Interfaces:
Layer1ApiStrategiesEchoMessagesLayer.StrategyEchoMessageFromLayer

public class Layer1ApiSoundAlertMessage extends Object implements Layer1ApiStrategiesEchoMessagesLayer.StrategyEchoMessageFromLayer
This message triggers Bookmap to show a notification/play sound alert.

Notification system examples

If you want to jump right into examples, take a look at DemoStrategies project. Here is a brief explanation of what you can find there:

  • velox.api.layer1.simpledemo.alerts.tradeprice.SimplePriceAlertDemo - a basic demo that shows how you can create and send a single type of alerts. The process of developing this addon is described step-by-step below, in the section Example of an addon leveraging notification system.
  • velox.api.layer1.simpledemo.alerts.simplegui.SimpleAlertGuiDemo - a "Hello-World" style demo showing how to incorporate your own GUI for managing notifications. Implemented step-by-step in Layer1ApiAlertGuiMessage javadoc.
  • velox.api.layer1.simpledemo.alerts.tradeprice.CustomPriceAlertDemo - a more elaborate example of the notification system usage. Shows how an addon can create alerts dynamically, using its own GUI.
  • velox.api.layer1.simpledemo.alerts.manual.Layer1ApiAlertDemo - a "synthetic" example, allowing you to see as many features of the notification system as possible. Alerts are sent manually from the strategies dialog.

Notification system step-by-step guide

The notification system has multiple moving parts and might seem intimidating at first glance. Below is a step-by-step guide to a simple addon development which sends alerts based on some market event.

First, let's define a framework for interaction with the notification system.

Just like in many other parts of Bookmap, an addon communicates with the Bookmap via special messages objects. That is, your addon should listen for messages via Layer1ApiAdminListener.onUserMessage(Object) and send them with Layer1ApiAdminProvider.sendUserMessage(Object)

Notification system workflow

The workflow looks like this:

  • Register a UI for controlling addon's notifications with Layer1ApiAlertGuiMessage (optional step, omitted in the example below)
  • Declare a group of sound alerts by sending Layer1ApiSoundAlertDeclarationMessage, store this message to later link it with actual notifications
  • Setup listening for declarations messages with a flag isAdd = false
  • Send an initial Layer1ApiAlertSettingsMessage for a new declaration, setup listening for alert settings coming from Bookmap
  • When a trigger, defined internally by the addon occurs - send an actual alert with Layer1ApiSoundAlertMessage
  • (optional) Cancel currently active alert with Layer1ApiSoundAlertCancelMessage - this will close popup created by the alert, stop sound notification playback, and cancel all planned future executions if the alert is repeated (> 1)

Example of an addon leveraging notification system

Now, lets see how it all works together in a more realistic example

Lets say we want to develop an addon which tracks the trades, and if there is a trade with price larger than 10 - we notify a user.

First, we should create an entrypoint class, so or addon can be loaded by the Bookmap (this is the same process as for any other addon).

 @Layer1Attachable
 @Layer1StrategyName("Simple price alert demo")
 @Layer1ApiVersion(Layer1ApiVersionValue.VERSION2)
 public class SimplePriceAlertDemo implements
     Layer1ApiAdminAdapter,
     Layer1ApiDataAdapter,
     Layer1ApiInstrumentAdapter,
     Layer1ApiFinishable {

     private Layer1ApiProvider provider;

     public SimplePriceAlertDemo(Layer1ApiProvider provider) {
         this.provider = provider;

         ListenableHelper.addListeners(provider, this);
     }

     @Override
     public void finish() {
     }
 }
 

Now, our addon can be loaded, and it also gets the underlying Layer1ApiProvider - the key entity in Bookmap API, which gives access to various types of events (note the subscription with ListenableHelper). Later we will also use the provider to send messages to BM. In addition, we need to implement Layer1ApiFinishable, but we don't need to add any implementation for now

Also, our class implements Layer1ApiAdminAdapter - which gives us ability to listen for messages coming from BM by implementing Layer1ApiAdminAdapter.onUserMessage(Object)

In addition, we want to listen for trades, and for that we implement Layer1ApiDataAdapter, as its method Layer1ApiDataAdapter.onTrade(String, double, int, TradeInfo) does exactly what we want. Likewise, to show the correct trade price and size - we need to know the pips and size increment of the current instrument. We get this data from Layer1ApiInstrumentAdapter.onInstrumentAdded(String, InstrumentInfo)

Thus, we store the information of the instruments by adding the aliasToInstrumentInfo map, and update it using methods of Layer1ApiInstrumentAdapter:

     private final Map<String, InstrumentInfo> aliasToInstrumentInfo = new ConcurrentHashMap<>();

     @Override
     public void onInstrumentAdded(String alias, InstrumentInfo instrumentInfo) {
         aliasToInstrumentInfo.put(alias, instrumentInfo);
     }

     @Override
     public void onInstrumentRemoved(String alias) {
         aliasToInstrumentInfo.remove(alias);
     }
 

Next step - lets listen for the trades, and start simple - write a message to logs when the trade with price > 10 occurs:

 @Override
 public void onTrade(String alias, double price, int size, TradeInfo tradeInfo) {
     InstrumentInfo instrumentInfo = aliasToInstrumentInfo.get(alias);
     double realPrice = price * instrumentInfo.pips;
     double realSize = size * (1 / instrumentInfo.sizeMultiplier);
     if (realSize != 0 && realPrice > 10) {
         // Here, instead of writing to logs, we later want to send an alert
         Log.info(String.format("Trade of price > 10 occurred, actual price={%.2f}, size={%.2f}", realPrice, realSize));
     }
 }
 

You might ask: Why can't we just use the price and size - the arguments of onTrade method? Why should we bother multiplying it by the pips and dividing by sizeMultiplier? And the answer is - Bookmap passes the price and size not as the "real" values, but as the number of increments. To make it clearer, lets say that for an instrument with pips = 0.5 there was a trade with realPrice = 50.0. Now, we want to calculate the number of price increments to express the real price:
incrementsNumber = realPrice / pips = 50.0 / 0.5 = 100.
Thus, 100 is the value you will get in price argument, when the real trade price is 50.0.

But, here we want to perform the reversed operation - get the real price from the number of price increments using pips. For this we will use the following formula:
realPrice = priceIncrementsNumber * pips

The same goes for the trade size - it is also represented as a number of increments, but instead of the increment size we have a sizeMultiplier - which is a reversed value to size increment. As an example, say that there was a trade of realSize = 50.0 with sizeMultiplier = 10. To calculate the number of size increments we will use the formula:
sizeIncrementsNumber = realSize * sizeMultiplier = 50.0 * 10 = 500
However, in this case we are interested in getting the real size from the number of size increments:
<b>realSize = sizeIncrementsNumber * (1 / sizeMultiplier)</b>

Now, instead of plain logging, we want to use the notification system. Lets go through its workflow:

Register a UI for controlling addon's notifications with Layer1ApiAlertGuiMessage

To keep this example simple, we won't implement GUI for it. However, you can take a look at Layer1ApiAlertGuiMessage javadoc

Declare a group of sound alerts by sending Layer1ApiSoundAlertDeclarationMessage, store this message to later link it with actual declarations

Lets think about our alert - how we want our user to see it. For this simple example, our alert will show popup, won't have sound notification and will be a single-shot (non-repeated) alert. It will be related to all available instruments aliases. A description for the trigger of this alert might be simply - "Trade price > 10"

With this description in mind lets create a declaration message and send it when our addon is loaded by Bookmap. This moment is indicated by UserMessageLayersChainCreatedTargeted message. Thus, lets create an initAlerts() method, which will be called each time the addon is loaded. The method will send an alert declaration, and store it for later use as a class field:

 private Layer1ApiSoundAlertDeclarationMessage declarationMessage;

 private final Object declarationLock = new Object();

 @Override
 public void onUserMessage(Object data) {
     if (data instanceof UserMessageLayersChainCreatedTargeted) {
         UserMessageLayersChainCreatedTargeted message = (UserMessageLayersChainCreatedTargeted) data;
         if (message.targetClass == SimplePriceAlertDemo.class) {
             synchronized (declarationLock) {
                 initAlerts();
             }
         }
     }
 }

 private void initAlerts() {
     declarationMessage = Layer1ApiSoundAlertDeclarationMessage.builder()
         .setTriggerDescription("Trade price > 10")
         .setSource(SimplePriceAlertDemo.class)
         .setPopupAllowed(true)
         .setAliasMatcher(alias -> true)
         .build();
     provider.sendUserMessage(declarationMessage);
 }
 

Note: We also added a synchronization lock - declarationLock. Although for now the synchronization doesn't make any sense as we access the declarationMessage from one thread, later it will be accessed from different threads. Thus, we want to prevent a race condition from happening.

If we did everything right - in Bookmap a new record should appear in the "Configure alerts" table - available via File -> Alerts -> Configure alerts. We WON'T see the notifications just yet, we are still setting them up.

Next step in the notifications workflow is strongly connected to this one:

Listen for declarations messages with a flag isAdd = false

The idea is - when your addon sees this type of declaration message - it should stop sending notifications of that type. Your addon gets this types of events when a user clicks "Remove alert" button in Configure alerts table mentioned above. Thus, lets setup listening for this type of messages:

 @Override
 public void onUserMessage(Object data) {
     if (data instanceof UserMessageLayersChainCreatedTargeted) {
         UserMessageLayersChainCreatedTargeted message = (UserMessageLayersChainCreatedTargeted) data;
         if (message.targetClass == SimplePriceAlertDemo.class) {
             synchronized (declarationLock) {
                 initAlerts();
             }
         }
     } else if (data instanceof Layer1ApiSoundAlertDeclarationMessage) {
         Layer1ApiSoundAlertDeclarationMessage obtainedDeclarationMessage = (Layer1ApiSoundAlertDeclarationMessage) data;
         if (obtainedDeclarationMessage.source == SimplePriceAlertDemo.class
             && !obtainedDeclarationMessage.isAdd) {
             synchronized (declarationLock) {
                 declarationMessage = null;
             }
         }
     }
 }
 

A couple of notes:

  • Here we have only one declaration message, while in the real application you might have multiple - in that case you can store them in a Map using id as a key, and when a message with the same id arrives - remove the value from the Map. However, the main idea of this guide is to introduce concepts one-by-one, and use as little non-necessary functionality as possible, so we won't be bothering with the multiple declarations just yet.
  • As you might have noticed, we are not using the value of the declaration message - and it will be fixed soon.

For now, lets go to the next step in the notifications workflow:

Send an initial Layer1ApiAlertSettingsMessage for a new declaration, and listen for alert settings coming from Bookmap

The settings message is connected to a declaration message, and it can be used to notify your addon about settings changes, or, vice versa - your addon can notify Bookmap about the settings changes. Now, before we send the actual notifications, let's define the settings that will be used in the alert message, and send them to Bookmap. Also, we want to set up listening for the settings messages, which will look just like the listening for the declarations messages in the previous step. Below is the result of code changes:

 private Layer1ApiAlertSettingsMessage settingsMessage;

 private void initAlerts() {
     declarationMessage = Layer1ApiSoundAlertDeclarationMessage.builder()
         .setTriggerDescription("Trade price > 10")
         .setSource(SimplePriceAlertDemo.class)
         .setPopupAllowed(true)
         .setAliasMatcher(alias -> true)
         .build();
     provider.sendUserMessage(declarationMessage);

     settingsMessage = Layer1ApiAlertSettingsMessage
         .builder()
         .setDeclarationId(declarationMessage.id)
         .setPopup(true)
         .setSource(SimplePriceAlertDemo.class)
         .build();
     provider.sendUserMessage(settingsMessage);
 }

 @Override
 public void onUserMessage(Object data) {
     if (data instanceof UserMessageLayersChainCreatedTargeted) {
         UserMessageLayersChainCreatedTargeted message = (UserMessageLayersChainCreatedTargeted) data;
         if (message.targetClass == SimplePriceAlertDemo.class) {
             synchronized (declarationLock) {
                 initAlerts();
             }
         }
     } else if (data instanceof Layer1ApiSoundAlertDeclarationMessage) {
         Layer1ApiSoundAlertDeclarationMessage obtainedDeclarationMessage = (Layer1ApiSoundAlertDeclarationMessage) data;
         if (obtainedDeclarationMessage.source == SimplePriceAlertDemo.class
             && !obtainedDeclarationMessage.isAdd) {
             synchronized (declarationLock) {
                 declarationMessage = null;
             }
         }
     } else if (data instanceof Layer1ApiAlertSettingsMessage) {
         Layer1ApiAlertSettingsMessage obtainedSettingsMessage = (Layer1ApiAlertSettingsMessage) data;
         if (obtainedSettingsMessage.source == SimplePriceAlertDemo.class) {
             settingsMessage = (Layer1ApiAlertSettingsMessage) data;
         }
     }
 }
 

Key points here: we are sending the settings message, linking it to the specific declaration by id, after this declaration has been sent. As we decided in the very beginning, our notification has a popup, but no sound alert - and it is reflected in the settings message creation.

Moving on to the next step in the workflow:

When a trigger, defined internally by the addon occurs - send an actual alert with Layer1ApiSoundAlertMessage

Now that we have everything in place, we are ready to send the actual notification:

 @Override
 public void onTrade(String alias, double price, int size, TradeInfo tradeInfo) {
     InstrumentInfo instrumentInfo = aliasToInstrumentInfo.get(alias);
     double realPrice = price * instrumentInfo.pips;
     double realSize = size * (1 / instrumentInfo.sizeMultiplier);
     if (realSize != 0 && realPrice > 10) {
         Log.info(String.format("Trade of price > 10 occurred, actual price={%.2f}, size={%.2f}", realPrice, realSize));
         synchronized (declarationLock) {
             if (declarationMessage != null) {
                 Layer1ApiSoundAlertMessage soundAlertMessage = Layer1ApiSoundAlertMessage.builder()
                     .setAlias(alias)
                     .setTextInfo(String.format("Trade actual price={%.2f}, size={%.2f}", realPrice, realSize))
                     .setAdditionalInfo("Trade of price > 10")
                     .setShowPopup(settingsMessage.popup)
                     .setAlertDeclarationId(declarationMessage.id)
                     .setSource(SimplePriceAlertDemo.class)
                     .build();
                 provider.sendUserMessage(soundAlertMessage);
             }
         }
     }
 }
 

Here we specify all the data we want to show on the alert. Note that this message is linked with the Layer1ApiSoundAlertDeclarationMessage by the alertDeclarationId field, and the settings (popup/sound notifications status) are taken from the stored Layer1ApiAlertSettingsMessage. This ensures data integrity between Bookmap and your addon.

Note that although Layer1ApiSoundAlertMessage has many fields which might look intimidating, only the source and the alertDeclarationId are mandatory.

If you did everything correctly, the addon should be able to send notifications now, and you can also control the popup state (on/off) with the Bookmap GUI (by pressing the "toggle popup" button in "Configure alerts" table)

Although the alerts work as expected, there is one more thing worth a discussion: an addon can programmatically remove a declaration, by sending a declaration message with the same id and flag Layer1ApiSoundAlertDeclarationMessage.isAdd = false. Let's see an example of how it can be done. For that we will implement the Layer1ApiFinishable.finish() method.

 @Override
 public void finish() {
     synchronized (declarationLock) {
         if (declarationMessage != null) {
             Layer1ApiSoundAlertDeclarationMessage removeDeclarationMessage = new Builder(declarationMessage)
                 .setIsAdd(false)
                 .build();
             provider.sendUserMessage(removeDeclarationMessage);
         }
     }
     aliasToInstrumentInfo.clear();
 }
 

Although Bookmap will try its best to remove any stale alerts when your addon is unloaded, it is a good practice to take care of the resources you used. Also, note that finish(), onTrade() and onUserMessage() are called from different threads, and this is why we needed synchronization.

Full example source code can be found at DemoStrategies project on Github - check out velox.api.layer1.simpledemo.alerts.tradeprice.SimplePriceAlertDemo
See Also:
  • Field Details

    • REPEAT_COUNT_INFINITE

      public static final long REPEAT_COUNT_INFINITE
      See Also:
    • sound

      public final byte[] sound
      Binary data for the sound to be played. Please do not modify array after you pass it to the message. Multiple messages can share same sound data.
    • textInfo

      public final String textInfo
      Text description of a message, will be displayed in alerts dialog
    • showPopup

      public final boolean showPopup
      If true, popup will be shown containing textInfo (which must not be null)
    • repeatCount

      public final long repeatCount
      Number of times sound will be repeated or REPEAT_COUNT_INFINITE for infinite replay (until cancelled by user).
    • repeatDelay

      public final Duration repeatDelay
      Delay between sound repetitions.
    • alertId

      public final String alertId
      This ID can be used to reference alert later (stop it)
    • statusListener

      This listener will be notified each time the sound alert status is changed
      Note: after the Layer1ApiFinishable.finish() method for the strategy is called, no further method calls will be made to this listener.
      See Also:
    • source

      public final Class<?> source
      Class that created this message. The class must have Layer1StrategyName annotation present
    • metadata

      public final Object metadata
      This field will not be used anywhere outside of strategy code, but strategy can later use this field, for example, to determine if this message should be cancelled in Layer1ApiSoundAlertCancelMessage.Layer1ApiSoundMessagesFilter.shouldCancelMessage(Layer1ApiSoundAlertMessage)
    • alias

      public final String alias
      If not null the alert is considered linked to specific instrument
    • alertDeclarationId

      public final String alertDeclarationId

      The Layer1ApiSoundAlertDeclarationMessage.id of a linked alert declaration
      Note that if you specify this field, there should already exist a registered declaration with the given id.

      Also, the fields of the alert and its linked declaration are checked for conformity, e. g. for a message with repeatCount = 1 you cannot link a declaration message with Layer1ApiSoundAlertDeclarationMessage.isRepeated = true

    • priority

      public final int priority
      The priority value helps to order sound messages in a sound play queue. If there are 2 or more messages waiting in a queue, the message with higher priority will be handled first. Order of handling messages with equal priority is not guaranteed. Default is 0
    • additionalInfo

      public final String additionalInfo
      If not null, this text will be shown on the alert popup below the main message, specified in textInfo. This field is optional
    • severityIcon

      public final Image severityIcon
      If not null, the icon is shown on the alert popup. You can add your own image or use the defaults from the Layer1DefaultAlertIcons
      Default value is null
  • Constructor Details

  • Method Details