Risk Rule Developers Guide
This document describes how to develop, test, and deploy custom risk rules in the Deltix Execution Server (Ember).
Prerequisites
Please familiarize yourself with Ember risk rule concepts (projections and case tables) by reading the Ember Risk and Positions Tutorial before reading this document. Also, it may help to read the Trading Data Model document that describes how Ember models trading requests and events.
Concepts
Risk rules limit losses from trading mistakes. Mistakes can range from a bug in trading algorithm that performs an infinite loop of order submissions to an API to a user accidentally sending a test sequence of orders to a production environment.
Conceptually, risk rules filter outbound trading requests and process inbound trading events. Each risk rule may veto new trade request if it exceeds configured limits or halt trading. The following diagrams show flow of trading requests (new order submissions, order modifications and cancellations), as well as trading events (order acks, cancellations, fills, etc.).
Ember uses an event sourcing design. Parts of the system (such as: external API clients, trading algorithms, and exchange connectors) exchange messages to process trading activity.
Trading Requests Flow:
For example, when a FIX client sends a new order request:
- The FIX gateway creates an
OrderNewRequest
message and places it into the OMS queue. - OMS reads the
OrderNewRequest
message from the queue, validates order parameters, and checks risk limits. If everything is good, it figures out the order destination and places the order into the queue of the destination service (e.g. trading connector or some algorithm). - The trading connector reads the
OrderNewRequest
from the queue and converts it into an API call of the destination trading venue.
Trading Events Flow:
Order Event flow is usually reverse:
- The trading connector receives a notification from a native API of the destination trading venue (e.g., a FIX message or a websocket message), converts it into a Deltix
OrderEvent
(sayOrderTradeReportedEvent
, if this was a fill) and places it into the OMS queue. - OMS applies the fill to the order state, enriches the event, and forwards it to the FIX gateway.
- The FIX gateway reads the
OrderEvent
from the queue and sends a FIX Execution Report back to the API client.
The main point is that all trading activity can be seen as a series of messages. Risk rule checks are happening inside the OMS thread and the risk rule API is basically a set of callbacks:
public interface RiskOrderMessageHandler {
/**
* Process new order submission request before it goes to destination venue
* @param order order state
* @param request original order submission request
* @param observer veto/halt callback. Is NULL during warm-up mode.
*/
void onNewOrderRequest(RiskOrder order, OrderNewRequest request, RiskObserver observer);
/**
* Process order cancellation request before it goes to destination venue
* @param order current state of order being cancelled
* @param request the cancel request itself
* @param observer veto/halt callback. Is NULL during warm-up mode.
*/
void onCancelOrderRequest(RiskOrder order, OrderCancelRequest request, RiskObserver observer);
/**
* Process order replacement (cancel/replace) request before it goes out to destination venue
* @param order current state of the order that is modified
* @param request cancel/replace request
* @param observer veto/halt callback. Is NULL during warm-up mode.
*/
void onReplaceOrderRequest(RiskOrder order, OrderReplaceRequest request, RiskObserver observer);
/**
* Process order state event after it was processed by OMS
* @param orderState current state of the order (the event already applied)
* @param event event that needs to be processed
* @param previousState order state prior to this event
* @param observer veto/halt callback. Is NULL during warm-up mode.
*/
void onOrderEvent(RiskOrder orderState, OrderEvent event, OrderState previousState, RiskObserver observer);
}
For example, an onNewOrderRequest
callback is called when OMS processes a OrderNewRequest
(new order). The OMS state associated with the order is provided in the RiskOrder
object. RiskObserver
is used to notify OMS of a risk limit breach:
public interface RiskObserver {
/**
*
* @param path projection path that explains where the limit is defined
* @param limitName identifies the limit
* @param rejectCode See DeltixRiskCodes (extendable)
* @param reason human-readable order reject message
*/
void onBreach(ConstProjectionPath path, String limitName, int rejectCode, CharSequence reason);
}
A request processing risk limit breach causes an immediate request rejection. When multiple risk rules are deployed, a risk limit breach reported by a single risk rule is sufficient to initiate a request rejection.
In the case of event processing, there is nothing to reject (for example, we can't refuse an order fill report). However, a risk rule should keep the current trading state (e.g., actual position size, even if it exceeds a limit). Also, in special cases, a risk rule may signal OMS to halt trading based on just a processed event.
As a result of a series of callbacks, each risk rule typically maintains some kind of in-memory state. For example, if a risk rule is counting the number of requests per second, this state should include timestamps on N last requests. For position-aware limits, this may be the actual position side (based on reported fills), as well as the outstanding position (based on the remaining quantity of open orders).
Ember OMS keeps track of each order state, as well as provides a PositionView
that maintains basic position information like actual size, realized P&L, and average cost. More on PositionView
interface later.
State persistence and HA mode
So far, we discussed the processing of live trading messages and reacting in real time to a risk breach. This section discusses how the risk limit state is preserved from system restarts.
Ember writes all trading messages into Ember Journal. On system restart, journaled messages are played back. This is sometimes called a "Memento pattern". This playback involves risk rule callbacks - these are called for all historical messages again, in the original order. When this happens, the RiskObserver
parameter of each callback call is NULL (no need to complain about things in the past). Also, the RiskManagerContext
interface provides an isLive()
check that only returns true once the system switches to live message processing.
- When a system administrator or risk manager updates risk limit numbers, these actions are recorded to the Ember Journal and also played back on restart.
- Journal playback happens very fast. If risk rules are written well, disk read speed is the main limiting factor.
- Ember includes a special service called Journal Compaction that is designed to keep the journal size under the preconfigured maximum size, while preserving all active orders and positions.
- When Ember runs in a cluster, a follower node runs a hot copy of the leader. This includes risk rules. However, from the follower's risk rule point of view, a stream of trading messages appears as continuation of journal playback. The follower sees all live order requests, including the ones that breach limits but can not react to them (the
RiskObserver
parameter is NULL). It is the responsibility of the leader's risk rule to report the breach (however, the follower does see anOrderRejectEvent
caused by the leader's veto).
Predefined Risk Rules
Ember provides the following risk rules out-of-the box:
- Order Size Limit, Order Price Limit (fat finger)
- Duplicate order check
- Short/Long Position Size Limit per-Instrument, per-Currency, per-Root.
- Maximum number of positions that can be opened.
- Order Acknowledgement and Order Lifetime Timeouts (halts trading if broker is too low acknowledging new orders).
- Order Submission Frequency (starts rejecting new orders if their frequency exceeds limit) and similar.
- Net Open Position / Gross Open Position
- Maximum Loss
*This list constantly grows. Check with the Ember support team for the most up-to-date list.
Custom Risk Rules
Ember provides an API to develop and register risk rules. Each risk rule can:
- Initialize itself from a replay of historical trading messages (so-called bootstrap).
- Intercept and veto order submissions, replacements, or cancellation requests.
- Process trading events from an execution venue, such as:
- order execution
- cancel confirmation
- replacement confirmation
- Halt trading at the server level or at a level of a specific projection.
There are no restrictions on the number of risk rules registered in Ember. Just remember that a large number of complex risk rules may negatively impact order submission latency.
Projection Path Tree
In many cases, individual risk limits are defined for different groups of orders, for example, the maximum number of positions a specific trader may have or a maximum position size on an individual exchange. This grouping is implemented by the concept of projections in Ember Risk (See the Risk and Positions Tutorial for an in-depth description of this concept).
When implementing a custom risk rule, you may rely on this grouping implemented by the risk rules framework.
For example, imagine that you need to implement a Maximum Order Count risk rule. And imagine that we want to apply this limit to control how many orders each trader can place on a specific exchange. The risk framework takes care of order grouping. It instantiates multiple copies of your risk rule (one for each order group). Your task is to maintain order count for a single group. In this specific case, simply count the order count requests each instance can see.
Concurrency
Risk rules are executed by Ember OMS. Ember uses a reactive programming model where a single OMS thread processes all inbound and outbound messages. The OMS core is designed for lock-free, non-blocking, low latency event processing. This requires a certain discipline when developing custom risk rules.
Risk rule implementation must be very efficient to avoid contentions in the system. Risk rules intercept orders before they reach an execution venue and process execution events before algorithms and clients receive them. Ineffective risk rules increase latency and reduce system throughput.
In theory, custom risk rules may offload some expensive computations or external API calls to separate threads, but special care must be taken to avoid thread synchronization issues that impact performance.
Memory Usage
Generally, a risk rule should not allocate new object instances per-request or per-event callbacks. Allocating even short-lived objects causes Java Garbage Collection pauses which negatively impact latency characteristics of the system.
- Use object pooling and flyweight programming patterns, the rest of the system. There is a simple
deltix.anvil.util.ObjectPool
class provided for your convenience. - Use
deltix.anvil.util.AsciiStringBuilder
instead of Java Strings or StringBuilder. - Use
deltix.dfp.Decimal64
(represented as Java long) instead of java.lang.BigDecimal. - Use
deltix.anvil.util.CharSequenceParser
instead of Java text-to-type converters. - Use
deltix.anvil.util.Timer
instead of Java or JODA timers. - Use Deltix GFLog instead of Java Util Logging or zerogc log4j.
- Use Timebase for external messaging and additional persistence instead of the traditional JDBC and the like.
Risk Limit Modification
Risk limits defined for a specific projection are passed as an Ember message. All limit update messages are stored in Ember Journal and replicated on Ember Cluster. Risk Update messages are also replicated on Ember Cluster (just like all other Ember messages).
Persistence
Just like OMS and trading algorithms running in Ember, custom risk rules should be fully deterministic. The state of each rule state should be fully determined by a sequence of journaled messages (orders, fills, etc.), including limit updates. On startup, Ember reloads historical messages and restores state.
Access to Ember services
Risk rules can access services like TimeBase or Timer and Clock via the RiskManagerContext interface passed during initialization.
Deployment Parameters
You can define custom deployment parameter as follows:
customLimits {
"MaxShortPositionExposure, MaxLongPositionExposure" {
factory: Deltix.sample.risk.limit.MyCustomRiskRulesFactory,
settings {
marketClosingTime = "17:00:00"
timeZone = "America/New_York"
}
}
}
Limits
Once a system operator defines risk projections and risk limits to check in these projections, the next step is to define risk limit cases.
As we mentioned before, limit changes are captured by RiskUpdateRequests that are processed as normal trading messages.
For example, for a risk table like shown below:
Account | MaxOrderSize |
---|---|
GOLD | 300 |
SILVER | 200 |
BRONZE | 100 |
An internal risk update message that defines individual risk cases looks somewhat like this:
RiskUpdateRequest {
requestId: …
projection: "Account”
commands: [
{
type: "INSERT”,
conditions: [{"Account", "Gold”}],
limits: [{"MaxOrderSize", "300"}]
},
{
type: "INSERT”,
conditions: [{"Account", "Silver”}],
limits: [{"MaxOrderSize", "200"}]
},
…
]
}
When the Ember risk rules service processes a command like this, it locates the corresponding risk rule instances in the projection tree and calls their setLimits
method.
@Override
public void setLimits(RiskLimits riskLimits) {
setMaxQuantity((riskLimits == null) ? -1 : riskLimits.getIntLimit(MaxOrderSize, -1));
}
In this case, RiskLimits
is essentially a map containing individual limits defined for this instance in the projection tree.
For the above risk table there, are three calls dispatched to individual instances of the MaxOrderSize
limit, each transmitting 100, 200, and 300 as the MaxOrderSize
limit value.
Restrictions
- Risk rules should not trade. Risk rules should only be used as trade signal filters.
- Risk rules do not have an ability to override each other.
- Risk rules should not modify outbound trading requests. For example, it is not acceptable for a risk rule to reduce order size to meet a risk limit.
Common Pitfalls
References to past / inactive orders
To avoid allocating new objects for each order, Ember OMS uses object pooling. For each order source, Ember OMS keeps all active and fixed number inactive orders in a memory cache. Once an older inactive order is expunged from the cache, it is recycled back into the object pool.
Risk rules should not interfere with this order reference management. This basically means that a risk rule should NOT keep references to inactive orders. Make sure your custom risk rule always handles this in event callback:
@Override
public void onOrderEvent(RiskOrder order, OrderEvent event, OrderState previousState, @Nullable RiskObserver observer) {
if (observer != null) { // do not process historical orders reported during bootstrap
if (isEligible(order)) {
if (!previousState.isFinal() && order.getState().isFinal()) {
myListOfActiveOrders.remove (order);
}
}
}
...
Tutorial: Implementation of a simple risk rule
This section describes how to implement a risk rule using the Sample Quantity risk rule as an example. To keep code sections brief, only critical pieces are shown. The full source code of this example is available on github here.
Each risk rule in Ember uses these classes:
- CustomCustomRiskRuleFactory is responsible for creating each risk rule instance and describing the limit value in the
RiskLimitDefinition
that it returns. In most cases this is a boilerplate code. - RiskRule is the interface that the risk rule must implement.
RiskRule
extendsRiskOrderMessageHandler
, which defines the order events methods called by OMS during order lifecycle. - AbstractRiskRule provides boilerplate implementation of
RiskOrderMessageHandler
and a few helper methods. Custom risk rule implementations should extend this class. - RiskLimits is an interface used by risk rules to access their limit value.
- RiskOrder contains trading order information.
- RiskObserver provides a call back method to be called by the risk rule when the limit is breached.
- RiskManagerContext provides various container services to the risk rule: EpochClock (to get system time), Timers (to define periodic jobs), Pricing Service (e.g., to lookup prices or security metadata about a contract). It is available when a risk rule is initially created and is also passed to a risk rule instance in
onLive(RiskManagerContext)
after all the historical events in the journal are processed during bootstrap.
Risk Rule
Let us define a class for the SampleQuantityRiskRule
risk rule itself:
public class SampleQuantityRiskRule extends AbstractRiskRule
{
public static final String MaxQuantity = "MaxQuantity";
private @Decimal long maxQuantity = -1; // negative if unlimited
private boolean unlimited = true;
public SampleQuantityRiskRule() {
super("Max Order Quantity");
}
}
In this example, we extended the standard class AbstractRiskRule
that provides boilerplate code to process order events.
MaxQuantity
is the unique risk limit name that this risk rule is going to check. The same risk rule class can handle multiple limits. A single risk rule class instance per projection path is used to handle all its supported limits.
The risk rule must implement the setLimits(RiskLimits)
method, where it must be prepared to accept a limit value for all the limits that it handles. The limit value is available in the RiskLimits
parameter under its name, which is MaxQuantity
in our case:
public void setLimits(RiskLimits riskLimits) {
setMaxQuantity((riskLimits == null) ? -1 : riskLimits.getIntLimit(MaxQuantity, -1));
}
In our example, the risk rule must ensure that the order quantity does not exceed the specified maximum quantity. In order to prevent the placement of an order with an invalid quantity, the risk rule implementation must override the onNewOrderRequest()
and onReplaceRequest()
methods to validate order quantity and notify the RiskObserver
when the risk limit is breached:
@Override
public void onNewOrderRequest(RiskOrder orderState, OrderNewRequest request, RiskObserver observer) {
validateQuantity(request, observer);
}
@Override
public void onReplaceOrderRequest(RiskOrder orderState, OrderReplaceRequest request, RiskObserver observer) {
validateQuantity(request, observer);
}
private void validateQuantity(OrderEntryRequest request, RiskObserver observer) {
if (observer != null && Decimal64Utils.isGreater(request.getQuantity(), maxQuantity))
observer.onBreach(getProjectionPath(), MaxQuantity, MAX_ORDER_SIZE.ordinal(), formatMessage(request));
}
Order count risk limit
Here is a slightly more complex risk rule that limits how many orders can be submitted in the current risk projection:
@Override
public final void onNewOrderRequest(RiskOrder orderState, OrderNewRequest request, RiskObserver observer) {
orderCount++;
if (observer != null)
if (maxOrderCount >= 0 && maxOrderCount < OrderCount)
observer.onBreach(getProjectionPath(), LimitDefinitions.MaxOrderCount,
DeltixRiskCodes.MAX_ORDER_COUNT.ordinal(), formatOrderCountBreachedMessage());
}
Here we assume that each risk rule instance maintains its own private order count for a given projection:
public class DailyOrderCountLimit extends AbstractRiskRule implements DailyRiskRule {
private final static int UNLIMITED = -1;
private int maxDailyOrderCount = UNLIMITED; // negative if disabled
private int dailyOrderCount;
...
Order State
On each callback invocation, Ember OMS shares what it knows about the order state. The order parameter gives access to:
- Underlying instrument, symbol, and instrument type.
- Current order price and size. Imagine that an active order has multiple
OrderReplaceRequests
pending (on their way to an exchange), each with a different price and size. The API gives you access to the full order history, including what order size and price are currently working on the exchange, or which is the maximum order size (out of the pending modification requests) observed for a given order. - Business order attributes:
account
,clearingAccount
,trader
,traderGroup
,broker
,exchange
,moduleKey/portfolioKey
,user-defined-data
, base/term currency (where appropriate), etc.
The order's instrument gives access to reference data like:
- Default exchange
tickSize
contractMultipler
- Recent market price (provided as a set of volatile fields maintained by the Ember Pricing Service)
- Root symbol (for futures)
- Leg composition (for multi-legged synthetic instruments)
- Etc.
Risk Rule Factory
In the simplest form, the risk rule factory is a boilerplate:
public class SampleRiskRulesFactory implements CustomRiskRuleFactory
{
@Override
public RiskRule create(String limitName, ProjectionPath path, RiskManagerContext context) {
if (SampleQuantityRiskRule.MaxQuantity.equals(limitName)) {
return new SampleQuantityRiskRule();
}
throw new IllegalArgumentException("Unknown risk limit: " + limitName);
}
@Override
public RiskLimitDefinition getLimitDefinition(String limitName) {
if (MaxQuantity.equals(limitName)) {
return new RiskLimitDefinition(MaxQuantity, RiskLimitDefinition.ValueType.INT, SampleQuantityRiskRule.class, "Maximum Order Quantity", this);
}
throw new IllegalArgumentException("Unknown risk limit: " + limitName);
}
}
CustomRiskRuleFactory.create()
should return a new instance of the risk rule. It can initialize it with a projection path and services, but the actual limit values
are set separately in the setLimits()
method. This method can be called multiple times, for example, when the limits are modified
in the Ember Monitor.
CustomRiskRuleFactory.getLimitDefinition()
returns a description of the limit that the risk rule validates with these attributes:
unique limit name, value type, risk rule implementation class, description, risk rule factory, and optionally,
any constraints on the limit value range via the ValueCheck
parameter. When multiple risk limits are implemented by the same risk rule class
they will have the same implementation class specified by their RiskLimitDefinitions. Risk framework will then create
a single instance of this risk rule per projection path and that instance will be shared by these limits.
Risk Rule Deployment
To deploy the risk rule:
Add a JAR file with all the risk rule supporting classes to the Execution Server runtime CLASSPATH. One way of doing it is placing this library file under the lib/custom folder of the Ember installation. All JAR files found in this folder are automatically added to a Java CLASSPATH by Ember startup scripts.
Define risk limits supported by the risk rule in the ember.conf configuration file as shown below:
risk {
customLimits: {
MaxQuantity: {
factory: deltix.ember.service.oms.risk.sample.SampleQuantityRiskRuleFactory,
settings: {
}
}
}
riskTables: {
Symbol: [MaxQuantity]
}
}
- The above configuration tells Ember about the custom risk limit
MaxQuantity
with aSampleRiskRuleFactory
factory class that should be used to create risk rule instances handling this limit. The limit can then be added to the risk tables by its name, as shown above, the same as with the built-in risk limits. The risk tables config lists projections (groupings of trading orders) and limits that can be enforced for these projections.
- Restart Ember and Ember Monitor.
Risk Rule Testing
To test your custom MaxQuantity
limit:
- In the Ember Monitor console, open the Risks tab and select the Symbol projection.
- Click Add risk rule and specify a Symbol and MaxQuantity limit value. For instance: BTCUSD and 5. Click Apply Changes.
- Submit any order for BTCUSD with a quantity greater than 5. The order should get immediately rejected by Ember with the reason message that looks like this: "Symbol[BTCUSD]:MaxQuantity: Order quantity 10 exceeds maximum 5"
Advanced Topics
Timers
Timers allow scheduling a piece of work to be done at a certain time or with some periodicity. Ember provides an allocation-free timer API for algorithm developers.
Timer jobs are executed in the main processing thread of each algorithm. Each timer job is defined using:
- A callback function that performs scheduled work
- An optional parameter passed to this callback
- The next execution time (scheduled time)
For example, here is a callback function that cancels an order:
long onOrderTimeout(long now, Cookie cookie) {
cancelOrder((Order) cookie);
return DO_NOT_RESCHEDULE;
}
The first callback parameter provides the current time (which may be slightly past the scheduled time). The second parameter provides a timer cookie parameter specified during job creation.
Use the timer service to schedule a timer job as shown below:
TimerJob cancelJob = timer.schedule(clock.time()+5000, this::onOrderTimeout, cookie);
Here we schedule a callback to be called 5 seconds from the current clock time.
You can use the returned TimerJob
object if you want to cancel the job. For example, if the order is cancelled it may be a good idea to cancel the order timeout timer:
cancelJob.cancel();
Finally, if you want to perform periodic tasks, simply return the next execution time from the callback. Otherwise, return a negative value so that the timer is not rescheduled.
See Algorithm Developer’s Guide for a complete example of timer usage.
End Of Day Processing
Special processing related to the end of the trading day is a frequent use case. Ember has a configurable once a day "End Of Day" event (e.g., 17:00 New York). If a custom risk rule wants to be notified about an end of day event, it should implement a DailyRiskRule
interface:
public interface DailyRiskRule extends RiskRule {
/** Called by Risk Manager once a day to reset previous day state */
void onDailyReset();
}
Here is one simple example:
public class DailyRequestCountLimit extends AbstractRiskRule implements DailyRiskRule {
private int dailyRequestCount;
@Override
public final void onNewOrderRequest(RiskOrder orderState, OrderNewRequest request, RiskObserver observer) {
dailyRequestCount++;
if (observer != null)
if (maxDailyRequestCount >= 0 && maxDailyRequestCount < dailyRequestCount)
observer.onBreach(getProjectionPath(), LimitDefinitions.MaxDailyRequestCount,
DeltixRiskCodes.MAX_REQUEST_COUNT.ordinal(),formatRequestCountBreachedMessage());
}
@Override
public void onDailyReset() {
dailyRequestCount = 0;
}
}
TimeBase
TimeBase streams can be read on system startup during risk rule initialization (to fetch some additional data for a risk rule) or scanned periodically. Another frequent use case is to dump the risk rule state on the timer for some external front end to display to a system operator.
Remember that each risk rule has no dedicated thread, so periodic scanning can be implemented with a trade-off: it either introduces a separate thread and synchronization/locking into your risk rule (not good). Alternatively, the TimeBase stream can be scanned from the trade message callback (in which case it pauses OMS, which is also not good).
The RiskManagerContext
provided to each risk rule during initialization has a method that gives a risk rule access to ember's TimeBase connection. Please refer to the TimeBase Java samples on how to read and write messages into TimeBase streams.
PositionView
Ember's built-in risk rules already perform routine bookkeeping for each projection. You can access this state using the PositionView
interface:
public interface PositionView {
/**
* @return Actual position size (result of past fills, negative if position is short).
*/
@Decimal
long getActualPositionSize();
/**
* @return cumulative remaining size of open BUY orders
*/
@Decimal
long getOpenBuySize();
/**
* @return cumulative remaining size of open SELL orders
*/
@Decimal
long getOpenSellSize();
/**
* @return Actual daily position size (result of fills that happened today, negative if position is short).
*/
@Decimal
long getDailyActualPositionSize();
/**
* @return BUY side exposure of this position, in main currency (can be NULL if money-to-market prices are missing)
*/
@Decimal
long getLongExposure();
/**
* @return SELL side exposure of this position, in main currency (can be NULL if money-to-market prices are missing)
*/
@Decimal
long getShortExposure();
}
Please note that PositionView
is available only for risk projections that end with Symbol, Currency, or RootSymbol. For example, for "/Symbol" or "/Exchange/Symbol", but not for "/Exchange".
PositionView
is provided to each risk rule during the "end of historical playback" notification:
public class SamplePositionRiskRule extends AbstractRiskRule {
private PositionView position;
@Override
public void onLive(RiskManagerContext context) {
this.position = getParentGroup().getPositionView();
}
...
Here is an example of using this:
@Override
public void onNewOrderRequest(RiskOrder order, OrderNewRequest request, RiskObserver observer) {
if (observer != null) {
@Decimal long actualPositionSize = Decimal64Utils.abs(position.getActualPositionSize());
if (Decimal64Utils.isGreater(actualPositionSize, maxPositionSize)) {
observer.onBreach(getProjectionPath(), MaxPosition, …);
}
}
}
PositionCounter
PositionCounter is a built-in risk rule that is always declared one level up above position projection in the projection path. It provides an aggregated view over a subtree of per-symbol or per-currency positions and is evaluated every time a symbol or currency position in its projection path changes.
PositionCounter risk rule supports these limits: MaxCurrencyPositions, MaxSymbolPositions, MaxShortExposure, MaxLongExposure, MaxNetExposure, MaxGrossExposure, MaxShortLongExposure, and MaxLoss.
Security Metadata
A custom risk rule may want to keep track of some extra information like the instrument industry sector or contract expiration. This can be achieved using a custom Instrument Factory.
First let's define our instrument class that will extract this information from InstrumentUpdate
messages broadcasted any time somebody modifies the TimeBase security metadata stream:
class CustomEngineInstrumentInfo extends EngineInstrumentInfo {
private int contractMonth;
private int contractYear;
@Timestamp
private long expirationDate = TypeConstants.TIMESTAMP_NULL;
@Decimal
private long minOrderSize = Decimal64Utils.ZERO;
public CustomEngineInstrumentInfo(PriceInfo priceInfo, InstrumentType instrumentType, SecurityMetadataProvider<EngineInstrumentInfo> smp) {
super(priceInfo, instrumentType, smp);
}
public int getContractMonth() {
return contractMonth;
}
public int getContractYear() {
return contractYear;
}
public long getExpirationDate() {
return expirationDate;
}
public long getMinOrderSize() {
return minOrderSize;
}
@Override
public void onFutureUpdate(FutureUpdate update) {
super.onFutureUpdate(update);
contractMonth = update.getContractMonth();
contractYear = update.getContractYear();
expirationDate = update.getExpirationDate();
final ObjectList<InstrumentAttribute> attributes = update.getAttributes();
if (attributes != null) {
for (int i = 0; i < attributes.size(); i++) {
final InstrumentAttribute attribute = attributes.get(i);
if (!attribute.hasKey() || isEmptyOrNull(attribute.getValue()))
continue;
if (CharSequenceUtil.equals("minOrderSize", attribute.getKey())) {
minOrderSize = Decimal64Utils.tryParse(attribute.getValue(), Decimal64Utils.ZERO);
}
}
}
}
}
As you can see, the method updateFuture
does all the work of extracting and storing this information in the custom instrument state object.
What remains is to define the instance factory in the Ember configuration:
public class CustomExtendedInstrumentInfoFactory implements ExtendedInstrumentInfoFactory<CustomEngineInstrumentInfo> {
@Override
public CustomEngineInstrumentInfo create(PriceInfo priceInfo, InstrumentType instrumentType, SecurityMetadataProvider<EngineInstrumentInfo> smp) {
return new CustomEngineInstrumentInfo(priceInfo, instrumentType, smp);
}
}
Finally, the following configuration stanza enables this custom instrument factory:
engine {
instrumentInfoFactory {
factory = "deltix.ember.service.engine.CustomExtendedInstrumentInfoFactory"
settings {}
}
}
Market Prices
You can access the recent prices of any instrument in two ways:
- Each
RiskOrder
object has agetInstrument()
method that provides access to the order's instrument prices. RiskManagerContext
has aPricingService
that allows finding instrument prices by instrument symbol.