Class Layer1ApiSoundAlertMessage
- All Implemented Interfaces:
Layer1ApiStrategiesEchoMessagesLayer.StrategyEchoMessageFromLayer
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 inLayer1ApiAlertGuiMessagejavadoc.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
Layer1ApiAlertSettingsMessagefor 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); }@Overridepublic 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.
velox.api.layer1.simpledemo.alerts.tradeprice.SimplePriceAlertDemo- See Also:
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final classBuilder to buildLayer1ApiSoundAlertMessage.static enumstatic interface -
Field Summary
FieldsModifier and TypeFieldDescriptionfinal StringIf not null, this text will be shown on the alert popup below the main message, specified intextInfo.final StringTheLayer1ApiSoundAlertDeclarationMessage.idof a linked alert declaration
Note that if you specify this field, there should already exist a registered declaration with the given id.final StringThis ID can be used to reference alert later (stop it)final StringIf not null the alert is considered linked to specific instrumentfinal ObjectThis 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 inLayer1ApiSoundAlertCancelMessage.Layer1ApiSoundMessagesFilter.shouldCancelMessage(Layer1ApiSoundAlertMessage)final intThe priority value helps to order sound messages in a sound play queue.static final longfinal longNumber of times sound will be repeated orREPEAT_COUNT_INFINITEfor infinite replay (until cancelled by user).final DurationDelay between sound repetitions.final ImageIf not null, the icon is shown on the alert popup.final booleanIf true, popup will be shown containingtextInfo(which must not be null)final byte[]Binary data for the sound to be played.final Class<?> Class that created this message.This listener will be notified each time the sound alert status is changed
Note: after theLayer1ApiFinishable.finish()method for the strategy is called, no further method calls will be made to this listener.final StringText description of a message, will be displayed in alerts dialog -
Constructor Summary
ConstructorsConstructorDescriptionLayer1ApiSoundAlertMessage(byte[] sound, String textInfo, long repeatCount, Duration repeatDelay, Layer1ApiSoundAlertMessage.SoundAlertStatusListener statusListener, Class<?> source, Object metadata) Deprecated.This constructor does not provide the full functionalityLayer1ApiSoundAlertMessage(String alertId) Deprecated.UseLayer1ApiSoundAlertCancelMessageinstead -
Method Summary
Modifier and TypeMethodDescriptionbuilder()Creates builder to buildLayer1ApiSoundAlertMessage.booleanTrue if the message was created viaLayer1ApiSoundAlertMessage(java.lang.String)constructortoString()
-
Field Details
-
REPEAT_COUNT_INFINITE
public static final long REPEAT_COUNT_INFINITE- See Also:
-
sound
public final byte[] soundBinary 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
Text description of a message, will be displayed in alerts dialog -
showPopup
public final boolean showPopupIf true, popup will be shown containingtextInfo(which must not be null) -
repeatCount
public final long repeatCountNumber of times sound will be repeated orREPEAT_COUNT_INFINITEfor infinite replay (until cancelled by user). -
repeatDelay
Delay between sound repetitions. -
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 theLayer1ApiFinishable.finish()method for the strategy is called, no further method calls will be made to this listener.- See Also:
-
source
Class that created this message. The class must haveLayer1StrategyNameannotation present -
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 inLayer1ApiSoundAlertCancelMessage.Layer1ApiSoundMessagesFilter.shouldCancelMessage(Layer1ApiSoundAlertMessage) -
alias
If not null the alert is considered linked to specific instrument -
alertDeclarationId
The
Layer1ApiSoundAlertDeclarationMessage.idof 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 withLayer1ApiSoundAlertDeclarationMessage.isRepeated= true -
priority
public final int priorityThe 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
If not null, this text will be shown on the alert popup below the main message, specified intextInfo. This field is optional -
severityIcon
If not null, the icon is shown on the alert popup. You can add your own image or use the defaults from theLayer1DefaultAlertIcons
Default value is null
-
-
Constructor Details
-
Layer1ApiSoundAlertMessage
@Deprecated public Layer1ApiSoundAlertMessage(byte[] sound, String textInfo, long repeatCount, Duration repeatDelay, Layer1ApiSoundAlertMessage.SoundAlertStatusListener statusListener, Class<?> source, Object metadata) Deprecated.This constructor does not provide the full functionalityUse
Layer1ApiSoundAlertMessage.BuilderinsteadCreates a message that will launch an alert. -
Layer1ApiSoundAlertMessage
Deprecated.Use
Layer1ApiSoundAlertCancelMessageinsteadCreates a message that will stop an alert with specified alertId
-
-
Method Details
-
isCancelMessage
public boolean isCancelMessage()True if the message was created viaLayer1ApiSoundAlertMessage(java.lang.String)constructor- Returns:
- flag if the message is actually a cancel message
-
toString
-
builder
Creates builder to buildLayer1ApiSoundAlertMessage.- Returns:
- created builder
-