Worked Example: Market Data Gap Detection in Beeks Analytics for Markets (BAM)
This configuration example shows how a stack probe is configured to receive market data. Calculations (including when market data gaps occur) are performed in the VMX-Capture stack probes, and are then presented to VMX-Analysis as pre-aggregated statistics.
Mappings are performed so that market data is labelled for the correct channel, feed, and side (A or B).
The example also covers anomalies, which provide extra information about the gaps e.g. the exact sequence numbers that are affected by particular issues. Anomalies are specific to market data gaps.
Stack Probe Configuration for NASDAQ Totalview ITCH
We will take the processing of NASDAQ Totalview ITCH as our example configuration.
The probe setup is simple because traffic is only captured in a single direction. This simplification can be achieved because the Beeks Analytics for Markets configuration only captures market data that is incoming FROM a venue (as opposed to market data that is published and consumed internally, or market data that is published for external consumers).
See the Beeks Analytics Data Guide for more of a description of what the Beeks Analytics for Markets template covers.
The extracts presented here would be found in the following file, in a typical BAM deployment:
<confdir>agent/pmux/<PMUX_NAME>/<PORT_NAME>_nasdaq_totalview_itch_50.stack.json
The stack.json file is laid out using the JSON standard that is common for all stack probes. This is the start of the stack.json file on an example Beeks Analytics for Markets deployment:
{
"probe"
: {
"parameters"
: {
"name"
:
"Port1_nasdaq_totalview_itch_50"
,
"debug"
:
false
,
"filter"
:
"udp and (dst 233.49.196.111 or dst 233.54.12.111) "
,
"protocols"
: [
{
"type"
:
"module"
,
"value"
:
"ethernet"
}
....
In the rest of this worked example, we’ll step through each of these sections in turn.
Stack probes configured per port and Filter Settings
PORT_NAME is used so that the system can be configured to provide different statistics depending on which port is receiving particular market data streams. The stack probe configurations are identical for the different ports, with the following exceptions:
The probe name has to be unique.
The BPF traffic filter needs to be able to uniquely identify the traffic for the different ports.
In our example:
Stack Config File | Filter Value |
---|---|
Port1_nasdaq_totalview_itch_50.stack.json | udp and (dst 233.49.196.111 or dst 233.54.12.111) |
Port2_nasdaq_totalview_itch_50.stack.json | udp and (dst 233.54.12.101) |
Note that the gap detection exclusion filter and feed mapping parts of the configuration also use IP addresses and ports, so these files also differ between the Port1 and Port2 probe configurations.
Stack layer overview
The following layers are present in the stack:
"protocols"
: [
{
"type"
:
"module"
,
"value"
:
"ethernet"
},
{
"type"
:
"module"
,
"value"
:
"ip"
},
{
"type"
:
"module"
,
"value"
:
"premapper"
,
"id"
:
"gap_detection_exclusion_filter"
},
{
"type"
:
"module"
,
"value"
:
"acd"
,
"id"
:
"dec_payload"
}
]
Premapper layer
A premapper is used because there are only certain channels (defined by the UDP port) that we want to calculate gaps on. It wouldn’t be appropriate to include these in the BPF filter for the stack probe because this would prevent us from calculating ethernet or IP statistics for these other channels. The premapper configuration in the stack probe references a separate filter file. The gap detection exclusion filter file for Port1 is as follows:
{
"comment"
:
"Only forward packet to next decoder layer if port in mapping list"
,
"actions"
: [
{
"compositeMap"
: {
"key"
: [
"ip.dst_host"
,
"ip.dst_port"
],
"mapping"
: [
{
"key"
: {
"ip.dst_host"
:
"233.54.12.111"
,
"ip.dst_port"
:
"26477"
},
"actions"
: [
{
"nextDecoderPacketData"
: {}
}
]
},
{
"key"
: {
"ip.dst_host"
:
"233.49.196.111"
,
"ip.dst_port"
:
"26477"
},
"actions"
: [
{
"nextDecoderPacketData"
: {}
}
]
}
]
}
}
]
}
Decoder layer (ACD)
The ACD decoder layer is used to decode the actual market data fields. It references the standard ACD decoder for ITCH5, which is provided in a central store as part of the VMX-Capture deployment:
"dec_payload"
: {
"parameters"
: {
"decoder_json_config"
:
"/opt/tsa/share/decoders/acd/market_data/US/itch5/itch5.acd.json"
}
}
Here is an abbreviated version of the itch5.acd.json
file. You can see how it defines the fields that are processed for each message type:
{
"DecoderOptions"
: {
"Name"
:
"itch5"
,
"TCPStack"
:
"ethernet,ip,soupbintcp,acd"
,
"UDPStack"
:
"ethernet,ip,moldudp64,acd"
,
"Specification"
:
"NQTVITCHspecification.pdf"
,
"Version"
:
"5.1 March 3, 2018"
,
"Notes"
:
"Timestamps are represented as nanoseconds since midnight"
},
"TypeDefinitions"
: {
"UINT16_BE"
: {
"type"
:
"uint"
,
"size"
:
2
,
"endian"
:
"big"
},
"UINT32_BE"
: {
"type"
:
"uint"
,
"size"
:
4
,
"endian"
:
"big"
},
"UINT64_BE"
: {
"type"
:
"uint"
,
"size"
:
8
,
"endian"
:
"big"
},
"TIMESTAMP"
: {
"type"
:
"uint"
,
"size"
:
6
,
"endian"
:
"big"
},
"STOCK"
: {
"type"
:
"string"
,
"size"
:
8
},
"CHAR"
: {
"type"
:
"char"
,
"size"
:
1
},
"MESSAGE_TYPE"
: {
"type"
:
"char"
,
"size"
:
1
}
},
"MessageHeader"
: {
"fields"
: [
{
"name"
:
"MessageType"
,
"type"
:
"MESSAGE_TYPE"
,
"flags"
: [
"msg_type"
]
}
]
},
"Messages"
: {
"C"
: {
"name"
:
"OrderExecutedWithPriceMessage"
,
"fields"
: [
{
"name"
:
"StockLocate"
,
"type"
:
"UINT16_BE"
},
{
"name"
:
"TrackingNumber"
,
"type"
:
"UINT16_BE"
},
{
"name"
:
"Timestamp"
,
"type"
:
"TIMESTAMP"
},
{
"name"
:
"OrderRefNumber"
,
"type"
:
"UINT64_BE"
},
{
"name"
:
"ExecutedShares"
,
"type"
:
"UINT32_BE"
},
{
"name"
:
"MatchNumber"
,
"type"
:
"UINT64_BE"
},
{
"name"
:
"Printable"
,
"type"
:
"CHAR"
},
{
"name"
:
"ExecutionPrice"
,
"type"
:
"UINT32_BE"
}
]
},
"X"
: {
"name"
:
"OrderCancelMessage"
,
"fields"
: [
{
"name"
:
"StockLocate"
,
"type"
:
"UINT16_BE"
},
{
"name"
:
"TrackingNumber"
,
"type"
:
"UINT16_BE"
},
{
"name"
:
"Timestamp"
,
"type"
:
"TIMESTAMP"
},
{
"name"
:
"OrderRefNumber"
,
"type"
:
"UINT64_BE"
},
{
"name"
:
"CancelledShares"
,
"type"
:
"UINT32_BE"
}
]
},
"U"
: {
"name"
:
"OrderReplaceMessage"
,
"fields"
: [
{
"name"
:
"StockLocate"
,
"type"
:
"UINT16_BE"
},
{
"name"
:
"TrackingNumber"
,
"type"
:
"UINT16_BE"
},
{
"name"
:
"Timestamp"
,
"type"
:
"TIMESTAMP"
},
{
"name"
:
"OriginalOrderRefNumber"
,
"type"
:
"UINT64_BE"
},
{
"name"
:
"NewOrderRefNumber"
,
"type"
:
"UINT64_BE"
},
{
"name"
:
"Shares"
,
"type"
:
"UINT32_BE"
},
{
"name"
:
"Price"
,
"type"
:
"UINT32_BE"
}
]
}
},
"CalculatedFields"
: {
"fields"
: [
{
"name"
:
"beeks.payload_timestamp"
,
"type"
:
"timestamp"
,
"actions"
: [
{
"action"
:
"assign"
,
"df_source"
:
"Timestamp"
},
{
"action"
:
"add"
,
"predefined_source"
:
"packet_epoch_date_ns"
,
"timezone"
:
"America/New_York"
}
]
}
]
}
}
For more details on ACD configuration for different fields, see the Beeks Analytics Decoder Information document.
Transform collector configuration
The transform collector provides the mapper functions. These ensure that the properties that are passed to VMX-Analysis as anomalies (or fed into the pre-aggregator as statistics) are correctly mapped from fields that have been decoded.
"transform_collector"
: [
{
"type"
:
"module"
,
"value"
:
"mapper"
,
"id"
:
"mdFeedMapper"
},
{
"type"
:
"module"
,
"value"
:
"mapper"
,
"id"
:
"internalEntitiesMapper"
},
{
"type"
:
"module"
,
"value"
:
"mapper"
,
"id"
:
"protocolNormalizingMapper"
}
]
Let’s look at the function of each of these mappers in turn.
mdFeedMapper
The mdFeedMapper is used for all market data stack probes in the Beeks Analytics for Markets template. However, there is a different json definition for each stack probe. The definition file (which for BAM can be found in the <confdir>/agent/pmux/<pmux_name>/mdMapper
directory) maps IPs and ports to human-readable line, channel, side and site variables. These variables will be used higher up the stack processing. Here is an example of an extract from a Port1_nasdaq_totalview_itch_50.stack.mapper.json
configuration file:
{
"datafields"
: {
"beeks.line"
:
"string"
,
"beeks.channel"
:
"string"
,
"beeks.side"
:
"string"
,
"beeks.site"
:
"string"
},
"actions"
: [
{
"compositeMap"
: {
"key"
: [
"ip.dst_host"
,
"ip.dst_port"
],
"default"
: [
{
"assign"
: {
"beeks.line"
:
"Unassigned"
,
"beeks.channel"
:
"Unassigned"
,
"beeks.side"
:
"Unassigned"
,
"beeks.site"
:
"Unassigned"
}
}
],
"mapping"
: [
{
"key"
: {
"ip.dst_host"
:
"233.54.12.111"
,
"ip.dst_port"
:
"26477"
},
"actions"
: [
{
"assign"
: {
"beeks.line"
:
"nasdaq_totalview_itch_50_New_York_A"
,
"beeks.channel"
:
"nasdaq_totalview_itch_50_New_York_1_data"
,
"beeks.side"
:
"A"
,
"beeks.site"
:
"New_York"
}
}
]
}
]
}
}
]
}
The datafields
object lists the new fields that are being set.
Stack Probe Configuration: Performance Considerations
Why don’t we create one large market data mapping file that can be used by all probes? The answer is that this would result in a less efficient probe performance. That is why, for BAM configurations, the market data definitions are stored in a separate database that ‘seeds’ the config generator. The config generator writes the json configuration files, and ensures that these are as efficient as possible.
internalEntitiesMapper
An entities mapper is used for all stack probes in the Beeks Analytics for Markets template. Whereas the mdFeedMapper maps the entities that are public for the external market data feeds that are being monitored, the internalEntities mapper maps the internal IP addresses.
Given that market data is published to a multicast address, and the IP address of the consumer is not seen in the packets, the standard mapping here for all market data probes is a placeholder, which would allow, for example, the static mapping of a particular multicast group to defined consumers. This is what the placeholder mapping looks like:
{
"datafields"
: {
"beeks.ds1"
:
"string"
,
"beeks.intGrp1"
:
"string"
},
"actions"
: [
{
"map"
: {
"comment"
:
"Use MC Group to assign one or more dest intentity"
,
"key"
:
"ip.dst_host"
,
"default"
: [
{
"assign"
: {
"beeks.ds1"
:
"unassigned"
,
"beeks.intGrp1"
:
"unassigned"
}
}
],
"mapping"
: {}
}
}
]
}
For a fuller example of the internal entity mapping used for Beeks Analytics for Markets, see the Worked Example: Order Entry Stack Probes in Beeks Analytics for Markets worked example.
protocolNormalizingMapper
The protocolNormalizingMapper is used for both market data and order stack probes in the Beeks Analytics for Markets template. Note that it is only used for stack probes that decode messages - for the TCP decoders (for example), you will only find the entities mapper.
The protocolNormalizingMapper is often shared between different market data protocols. For example, many of the multicast feeds used for BAM reference the <confdir>/agent/global/mapper/acd.stack.mapper.json
standard ACD mapper for the protocol normalisation.
This mapper shows the simplicity of the stack configuration when combined with ACD protocol decoding - an 8 line JSON file (excluding parentheses!) calculates App-to-Wire latency for all multicast market data feeds that use the ACD decoder:
{
"datafields"
: {
"beeks.wiretime"
:
"int"
},
"actions"
: [
{
"isSet"
: {
"datafield"
:
"beeks.payload_timestamp"
,
"true"
: [
{
"assignExpr"
: {
"beeks.wiretime"
:
"df['TIMESTAMP'] - df['beeks.payload_timestamp']"
}
}
]
}
}
]
}
For a more complex example of a protocol normalising mapper, see the example of the FIX decoder mapper in Worked Example: Order Entry Stack Probes in Beeks Analytics for Markets .
Stat Collector configuration
The stat collectors specify which statistics to compute for the packets that match the BPF. There are four stat collectors defined for most BAM market data processing:
"stat_collector"
: [
{
"type"
:
"module"
,
"value"
:
"vmxgenericaggregatorinputeventconnector"
,
"id"
:
"mdStats"
},
{
"type"
:
"module"
,
"value"
:
"vmxgenericaggregatorinputeventconnector"
,
"id"
:
"udpStats"
},
{
"type"
:
"module"
,
"value"
:
"vmxanomalyconnector"
,
"id"
:
"coll_vmxanomalyconnector"
},
{
"type"
:
"module"
,
"value"
:
"vmxgenericaggregatorinputeventconnector"
,
"id"
:
"ipStatsExtgrp"
}
]
You will see that 3 of these stat collector modules are of the same type of module - the vmxgenericaggregatorinputeventconnector
type (this is sometimes referred as the GAIE connector). One is of a different type - the vmxanomalyconnector
.
See the Core Data Feed Guide for examples of how to configure the kafka stat collector module, which allows you to output via CDF-T. The rest of the configuration in this section can easily be adapted to output via the kafka stat collector.
Generic Aggregator Input Event Connectors (vmxgenericaggregatorinputeventconnector, aka GAIE) for Market Data - Example output to a Preagg
In BAM we have a different VMX GAIE Connector stat_collector module for each aggregator that we want to publish to.
This is an example for the ipStatsExtgrp aggregator definition. The metadata about the aggregator (how often it pushes its statistics etc) is defined in the stack probe configuration:
"ipStatsExtgrp"
: {
"parameters"
: {
"blocking"
:
false
,
"pool_size"
:
1000
,
"publish_interval_us"
:
10000000
,
"timestamp"
:
"TIMESTAMP"
,
"connector_id"
:
"IP_ExtGrp_statsAgent"
,
"node_path_stats_json_filename"
:
"$VMX_HOME/../../server/config/agent/global/preagg/IP_stats_Rx_ExtGrp.stack.agg.json"
}
}
The agg.json file then defines the structure of the aggregator:
{
"expressionDefinitions"
: {
"vp"
: {
"datafield"
: {
"name"
:
"beeks.vp"
}
},
"extgroup"
: {
"datafield"
: {
"name"
:
"beeks.extgroup"
}
},
"probeName"
: {
"datafield"
: {
"name"
:
"probe_name"
}
},
"session"
: {
"variable"
: {
"expression"
:
"{ip.src_host}:{ip.dst_host}"
}
}
},
"aggregations"
: {
"leafNode"
: {
"keys"
: [
{
"vp"
:
"vp"
},
{
"extgroup"
:
"extgroup"
},
{
"probeName"
:
"probeName"
},
{
"session"
:
"session"
}
],
"fieldSets"
: [
"allFields"
],
"propertySets"
: [
]
}
},
"fieldDefinitions"
: {
"rx_packets"
: {
"statistic"
: {
"name"
:
"ip.packets"
}
},
"rx_packet_bytes"
: {
"statistic"
: {
"name"
:
"ip.packet_bytes"
}
},
"rx_packet_bytes_1ms"
: {
"statistic"
: {
"name"
:
"ip.packet_bytes"
,
"microburstNS"
:
1000000
}
},
"ip_conversations"
: {
"activeStreams"
: {
"type"
:
"ip"
}
}
},
"fieldSets"
: {
"allFields"
: [
"rx_packets"
,
"rx_packet_bytes"
,
"rx_packet_bytes_1ms"
,
"ip_conversations"
]
},
"propertySets"
: {
}
}
The nodePaths
section needs to provide config for breakdown of every level in the aggregator.
The columns
section specifies the stats that feed into the Aggregator; these are then used by the Aggregator definitions in VMX-Analysis.
In Beeks Analytics for Markets, multiple probes write to this pre-agg - all of the market data protocol probes, plus the TCP_ingress.stack.json probe (for all of the different Visibility Points). This shows how multiple different probes can consolidate their statistics in a single aggregator structure.
The leaf_nodes object in the preagg configuration file defines the aggregator levels, and the field_sets and field_definitions define the columns for which statistics will be calculated.
Some of the values for the leaf nodes are static values which are defined in the tables section of the stack probe configuration, e.g. in our Nasdaq ITCH example:
"tables"
: {
"static_datafields"
: [
{
"name"
:
"beeks.vp"
,
"type"
:
"string"
,
"value"
:
"VP"
},
{
"name"
:
"beeks.extgroup"
,
"type"
:
"string"
,
"value"
:
"Nasdaq_NY_Primary"
},
{
"name"
:
"beeks.switchport"
,
"type"
:
"string"
,
"value"
:
"Port1"
},
{
"name"
:
"beeks.protocol"
,
"type"
:
"string"
,
"value"
:
"UDP"
},
{
"name"
:
"beeks.venue"
,
"type"
:
"string"
,
"value"
:
"Nasdaq"
},
{
"name"
:
"beeks.feed"
,
"type"
:
"string"
,
"value"
:
"Nasdaq TotalView ITCH 5.0"
}
]
}
The other values (e.g. ip.packets, ip_conversations) are passed up from the lower protocol levels of the stack probe.
Generic Aggregator Input Event Connectors (vmxgenericaggregatorinputeventconnector, aka GAIE) for Market Data - Example calculations
The mdStats module is also a useful example, because it shows how the preagg definition can specify the particular calculations that can be performed on the data. This is a typical MD_stats.stack.agg.json
file for a BAM deployment. Note again how this same preagg structure is shared by all of the market data stack probes. This is what the agg.json configuration looks like:
{
"expressionDefinitions"
: {
"vp"
: {
"variable"
: {
"expression"
:
"{beeks.wiretime:?}{beeks.vp}"
}
},
"extgroup"
: {
"datafield"
: {
"name"
:
"beeks.extgroup"
}
},
"switchport"
: {
"datafield"
: {
"name"
:
"beeks.switchport"
}
},
"feed"
: {
"datafield"
: {
"name"
:
"beeks.feed"
}
},
"side"
: {
"datafield"
: {
"name"
:
"beeks.side"
}
},
"channel"
: {
"datafield"
: {
"name"
:
"beeks.channel"
}
}
},
"aggregations"
: {
"leafNode"
: {
"keys"
: [
{
"vp"
:
"vp"
},
{
"extgroup"
:
"extgroup"
},
{
"switchport"
:
"switchport"
},
{
"feed"
:
"feed"
},
{
"side"
:
"side"
},
{
"channel"
:
"channel"
}
],
"fieldSets"
: [
"allFields"
],
"propertySets"
: []
}
},
"fieldDefinitions"
: {
"packets"
: {
"statistic"
: {
"name"
:
"ip.packets"
}
},
"packets_1ms"
: {
"statistic"
: {
"name"
:
"ip.packets"
,
"microburstNS"
:
1000000
}
},
"packets_10ms"
: {
"statistic"
: {
"name"
:
"ip.packets"
,
"microburstNS"
:
10000000
}
},
"packets_100ms"
: {
"statistic"
: {
"name"
:
"ip.packets"
,
"microburstNS"
:
100000000
}
},
"packets_1s"
: {
"statistic"
: {
"name"
:
"ip.packets"
,
"microburstNS"
:
1000000000
}
},
"packet_bytes"
: {
"statistic"
: {
"name"
:
"ip.packet_bytes"
}
},
"packet_bytes_1ms"
: {
"statistic"
: {
"name"
:
"ip.packet_bytes"
,
"microburstNS"
:
1000000
}
},
"packet_bytes_10ms"
: {
"statistic"
: {
"name"
:
"ip.packet_bytes"
,
"microburstNS"
:
10000000
}
},
"packet_bytes_100ms"
: {
"statistic"
: {
"name"
:
"ip.packet_bytes"
,
"microburstNS"
:
100000000
}
},
"packet_bytes_1s"
: {
"statistic"
: {
"name"
:
"ip.packet_bytes"
,
"microburstNS"
:
1000000000
}
},
"msgs"
: {
"statistic"
: {
"name"
:
"stack.msg_count"
}
},
"gaps"
: {
"statistic"
: {
"name"
:
"stack.gap_count"
}
},
"missed_msgs"
: {
"statistic"
: {
"name"
:
"stack.missed_msg_count"
}
},
"oos_msgs"
: {
"statistic"
: {
"name"
:
"stack.out_of_order_msg_count"
}
},
"wiretime"
: {
"mean"
: {
"datafield"
:
"beeks.wiretime"
}
},
"wiretime50"
: {
"percentile"
: {
"datafield"
:
"beeks.wiretime"
,
"value"
:
50
}
},
"wiretime95"
: {
"percentile"
: {
"datafield"
:
"beeks.wiretime"
,
"value"
:
95
}
},
"wiretime99"
: {
"percentile"
: {
"datafield"
:
"beeks.wiretime"
,
"value"
:
99
}
},
"wiretime_min"
: {
"minimum"
: {
"datafield"
:
"beeks.wiretime"
}
},
"wiretime_max"
: {
"maximum"
: {
"datafield"
:
"beeks.wiretime"
}
},
"ip_conversations"
: {
"activeStreams"
: {
"type"
:
"ip"
}
}
},
"fieldSets"
: {
"allFields"
: [
"packets"
,
"packets_1ms"
,
"packets_10ms"
,
"packets_100ms"
,
"packets_1s"
,
"packet_bytes"
,
"packet_bytes_1ms"
,
"packet_bytes_10ms"
,
"packet_bytes_100ms"
,
"packet_bytes_1s"
,
"msgs"
,
"gaps"
,
"missed_msgs"
,
"oos_msgs"
,
"wiretime"
,
"wiretime50"
,
"wiretime95"
,
"wiretime99"
,
"wiretime_min"
,
"wiretime_max"
,
"ip_conversations"
]
},
"propertySets"
: {}
}
Note how the statistic keyword means that that field is passed directly (without further calculations) as a statistic to the aggregator, whereas you can also use the minimum, mean, maximum and percentile functions to produce be more specific about how the data provided by the stack will be summarised.
Passing market data gap anomalies to VMX-Analysis (vmxanomalyconnector)
An Anomaly is a special type of event that is passed to VMX-Analysis. You can almost think of them as being halfway between the pre-aggregated statistics (which are described above) and the agent events (which we describe in the next worked example). They allow grouped anomalies to be collected by VMX-Analysis into alerts.
The VMX Anomaly Connector has the following parameters set for it in the stack probe config file:
"coll_vmxanomalyconnector"
: {
"parameters"
: {
"blocking"
:
false
,
"pool_size"
:
1000
,
"buffer_size"
:
"336"
,
"type"
:
"SEQBRK"
,
"gap_writer_json_filename"
:
"$VMX_HOME/../../server/config/agent/global/gapWriter/md.stack.gw.json"
}
}
SEQBRK is a specific type of alert rule, configured in VMX-Analysis.
It references a gap writer configuration, which defines the fields which will be passed to the anomalies:
{
"properties"
: {
"srcIp"
: {
"variable"
: {
"expression"
:
"{ip.src_host}"
}
},
"dstIp"
: {
"variable"
: {
"expression"
:
"{ip.dst_host}"
}
},
"srcPort"
: {
"variable"
: {
"expression"
:
"{ip.src_port}"
}
},
"dstPort"
: {
"variable"
: {
"expression"
:
"{ip.dst_port}"
}
},
"vp"
: {
"variable"
: {
"expression"
:
"{beeks.vp}"
}
},
"protocol"
: {
"variable"
: {
"expression"
:
"{beeks.protocol}"
}
},
"venue"
: {
"variable"
: {
"expression"
:
"{beeks.venue}"
}
},
"feed"
: {
"variable"
: {
"expression"
:
"{beeks.feed}"
}
},
"site"
: {
"variable"
: {
"expression"
:
"{beeks.site}"
}
},
"line"
: {
"variable"
: {
"expression"
:
"{beeks.line}"
}
},
"side"
: {
"variable"
: {
"expression"
:
"{beeks.side}"
}
},
"channel"
: {
"variable"
: {
"expression"
:
"{beeks.channel}"
}
}
}
}