Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions src/B3.Exchange.Core/ChannelDispatcher.Sinks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,20 @@ public void OnTradingPhaseChanged(in TradingPhaseChangedEvent e)
}

/// <summary>
/// Issue #322: B3-aligned best-effort wire markers on
/// <c>securityTradingEvent</c> for halt and resume. The downstream
/// consumer just needs them to be distinct and non-NULL; the
/// <c>SecurityStatus_3</c> frame's <c>securityTradingStatus</c> still
/// carries the engine's preserved <see cref="TradingPhase"/> so the
/// post-resume phase is unambiguous.
/// Issue #583: encodes administrative halt/resume using only official
/// UMDF 2.2.0 <c>SecurityStatus_3</c> semantics. On halt,
/// <c>securityTradingStatus</c> reports the official FORBIDDEN status
/// (not the pre-halt phase); on resume, the engine's preserved
/// <see cref="TradingPhase"/> is restored as <c>securityTradingStatus</c>.
/// <c>securityTradingEvent</c> uses the official
/// SECURITY_STATUS_CHANGE / SECURITY_REJOINS_SECURITY_GROUP_STATUS
/// values to mark the divergence/rejoin with the security group's phase.
/// </summary>

public void OnInstrumentHalted(in InstrumentHaltedEvent e)
{
AssertOnLoopThread();
byte phaseByte = _phaseSnapshot.TryGetValue(e.SecurityId, out var phase)
? (byte)phase
: (byte)TradingPhase.Open;
UmdfFrameBuilder.WriteInstrumentHalted(FrameSink, e.SecurityId, phaseByte, e.RptSeq, e.TransactTimeNanos);
UmdfFrameBuilder.WriteInstrumentHalted(FrameSink, e.SecurityId, e.RptSeq, e.TransactTimeNanos);
}

public void OnInstrumentResumed(in InstrumentResumedEvent e)
Expand Down
14 changes: 14 additions & 0 deletions src/B3.Exchange.Core/ISnapshotBookSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ public interface ISnapshotBookSource
/// </summary>
uint GetCurrentRptSeq(long securityId);

/// <summary>
/// The current official UMDF 2.2.0 <c>SecurityTradingStatus</c> value
/// for <paramref name="securityId"/> (issue #583): FORBIDDEN while the
/// instrument is administratively halted, otherwise the instrument's
/// current trading-group phase. <see cref="SnapshotRotator"/> stamps
/// this into the trailing <c>SecurityStatus_3</c> packet of every
/// published snapshot.
/// </summary>
byte GetSecurityTradingStatus(long securityId);

/// <summary>
/// Iterates the resting orders for <paramref name="securityId"/> on the
/// requested side in price-time priority. Caller must fully drain the
Expand Down Expand Up @@ -55,6 +65,10 @@ public MatchingEngineSnapshotSource(MatchingEngine engine, IReadOnlyList<long> s

public uint GetCurrentRptSeq(long securityId) => _engine.GetCurrentRptSeq(securityId);

public byte GetSecurityTradingStatus(long securityId) => _engine.IsHalted(securityId, out _)
? B3.Umdf.WireEncoder.UmdfFrameBuilder.SecurityTradingStatusForbidden
: (byte)_engine.GetTradingPhase(securityId);

public IEnumerable<RestingOrderView> EnumerateBook(long securityId, Side side)
=> _engine.EnumerateBook(securityId, side);
}
6 changes: 5 additions & 1 deletion src/B3.Exchange.Core/SnapshotRotator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ namespace B3.Exchange.Core;
/// <c>SnapshotFullRefresh_Header_30</c> + as many
/// <c>SnapshotFullRefresh_Orders_MBO_71</c> chunks as the book requires (each
/// chunk capped at <see cref="SnapshotPacketBuilder.MaxEntriesPerChunk"/> or
/// the caller-supplied per-chunk cap, whichever is smaller).
/// the caller-supplied per-chunk cap, whichever is smaller), followed by a
/// trailing <c>SecurityStatus_3</c> packet reporting the instrument's current
/// official trading status (issue #583) via
/// <see cref="ISnapshotBookSource.GetSecurityTradingStatus"/>.
///
/// <para>
/// Threading: <see cref="PublishNext"/> reads the matching engine's resting
Expand Down Expand Up @@ -177,6 +180,7 @@ public int PublishFor(long securityId, ushort incrementalSequenceVersion)
firstSequenceNumber: firstSeq,
sendingTimeNanos: nowNanos,
securityId: securityId,
securityTradingStatus: _source.GetSecurityTradingStatus(securityId),
lastRptSeq: lastRptSeq,
incrementalSequenceVersion: incrementalSequenceVersion,
bids: System.Runtime.InteropServices.CollectionsMarshal.AsSpan(bidsList),
Expand Down
52 changes: 51 additions & 1 deletion src/B3.Umdf.WireEncoder/SnapshotPacketBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ namespace B3.Umdf.WireEncoder;
/// the first packet carries the <c>SnapshotFullRefresh_Header_30</c> frame
/// followed by as many <c>SnapshotFullRefresh_Orders_MBO_71</c> frames as fit
/// in the destination buffer; subsequent packets carry only Orders_71 frames.
/// The final packet carries a single <c>SecurityStatus_3</c> frame reporting
/// the instrument's current official trading status — e.g. FORBIDDEN while
/// administratively halted (issue #583) — so late subscribers can bootstrap
/// without waiting for the next incremental status transition.
/// Each packet is written to a caller-supplied <see cref="Span{T}"/> via the
/// <see cref="PacketHandler"/> delegate so the caller can pool the buffer and
/// invoke its multicast publisher synchronously.
Expand Down Expand Up @@ -63,7 +67,12 @@ public static int GetPacketCount(
bufferSize - WireOffsets.PacketHeaderSize,
maxEntriesPerChunk);
}
return packetCount;

// Issue #583: every snapshot ends with one trailing packet carrying
// the instrument's current SecurityStatus_3 state, so late
// subscribers can bootstrap FORBIDDEN/halted instruments without
// waiting for the next incremental status transition.
return checked(packetCount + 1);
}

/// <summary>
Expand All @@ -84,6 +93,12 @@ public static int GetPacketCount(
/// stamped on every packet of the snapshot so consumers see it as one
/// logical refresh.</param>
/// <param name="securityId">Instrument identifier (Trade/Order semantic).</param>
/// <param name="securityTradingStatus">Current official UMDF 2.2.0
/// <c>SecurityTradingStatus</c> value for this instrument (issue #583):
/// e.g. FORBIDDEN=18 while administratively halted, or the current
/// trading-group phase otherwise. Written as a trailing
/// <c>SecurityStatus_3</c> packet so late subscribers can bootstrap
/// without waiting for the next incremental status transition.</param>
/// <param name="lastRptSeq">Last incremental RptSeq published before this
/// snapshot was taken; consumers gate snapshot acceptance on this value.
/// Pass <c>null</c> for an empty illiquid snapshot (per B3 §7.4).</param>
Expand All @@ -103,6 +118,7 @@ public static int WriteSnapshot(
uint firstSequenceNumber,
ulong sendingTimeNanos,
long securityId,
byte securityTradingStatus,
uint? lastRptSeq,
ushort incrementalSequenceVersion,
ReadOnlySpan<UmdfWireEncoder.SnapshotEntry> bids,
Expand Down Expand Up @@ -164,6 +180,27 @@ public static int WriteSnapshot(
packetCount++;
}

// -------- Trailing packet: current SecurityStatus_3 (issue #583) ----
// Written as its own packet (not merged into the header packet) so
// the header/orders packing logic above is unaffected by the
// instrument's status and consumers can process it independently.
// securityTradingEvent is NULL (255): this is a point-in-time status
// report, not a transition event.
seqNum++;
p = UmdfWireEncoder.WritePacketHeader(buffer, channelNumber, snapshotSequenceVersion, seqNum, sendingTimeNanos);
p += UmdfWireEncoder.WriteSecurityStatusFrame(
buffer.Slice(p),
securityId: securityId,
tradingSessionId: 0,
securityTradingStatus: securityTradingStatus,
securityTradingEvent: 255,
tradeDate: 0,
tradSesOpenTimeNanos: 0,
transactTimeNanos: sendingTimeNanos,
rptSeq: 0); // schema: "Sequence number per instrument update. (Zeroed in snapshot feed)"
onPacket(buffer.Slice(0, p));
packetCount++;

return packetCount;
}

Expand All @@ -178,6 +215,11 @@ private static int SnapshotHeaderFrameSize
+ WireOffsets.SbeMessageHeaderSize
+ WireOffsets.SnapHeaderBlockLength;

private static int SecurityStatusFrameSize
=> WireOffsets.FramingHeaderSize
+ WireOffsets.SbeMessageHeaderSize
+ WireOffsets.SecurityStatusBlockLength;

private static void ValidateLayout(
int bufferSize,
int bidCount,
Expand All @@ -199,6 +241,14 @@ private static void ValidateLayout(
$"buffer too small ({bufferSize} bytes) for a single Orders_71 entry frame ({OrdersFrameSize(1)} bytes); increase buffer size.",
nameof(bufferSize));
}
// Issue #583: the trailing SecurityStatus_3 packet reuses the same
// scratch buffer, starting a fresh PacketHeader at offset 0.
if (bufferSize < WireOffsets.PacketHeaderSize + SecurityStatusFrameSize)
{
throw new ArgumentException(
$"buffer too small ({bufferSize} bytes) for the trailing SecurityStatus_3 packet ({WireOffsets.PacketHeaderSize + SecurityStatusFrameSize} bytes); increase buffer size.",
nameof(bufferSize));
}
}

private static void ConsumeEntriesThatFit(
Expand Down
58 changes: 38 additions & 20 deletions src/B3.Umdf.WireEncoder/UmdfFrameBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,27 @@ namespace B3.Umdf.WireEncoder;
public static class UmdfFrameBuilder
{
/// <summary>
/// <c>SecurityTradingEvent</c> byte written for an instrument halt
/// (<c>SecurityStatus_3</c>). Matches the B3-aligned value documented
/// in issue #322.
/// Official UMDF 2.2.0 <c>SecurityTradingStatus</c> value meaning the
/// instrument is not available for trading (administrative halt).
/// Issue #583: replaces the previous out-of-domain marker approach.
/// </summary>
public const byte SecurityTradingEventHalt = 1;
public const byte SecurityTradingStatusForbidden = 18;

/// <summary>
/// <c>SecurityTradingEvent</c> byte written for an instrument resume
/// (<c>SecurityStatus_3</c>). Matches the B3-aligned value documented
/// in issue #322.
/// Official UMDF 2.2.0 <c>SecurityTradingEvent</c> value meaning the
/// instrument's status is being maintained separately from its
/// security group's phase. Written when an instrument enters
/// administrative halt (issue #583).
/// </summary>
public const byte SecurityTradingEventResume = 2;
public const byte SecurityTradingEventSecurityStatusChange = 101;

/// <summary>
/// Official UMDF 2.2.0 <c>SecurityTradingEvent</c> value meaning the
/// instrument's status now follows its security group's phase again.
/// Written when an instrument resumes from administrative halt,
/// restoring the preserved pre-halt phase (issue #583).
/// </summary>
public const byte SecurityTradingEventSecurityRejoinsGroupStatus = 102;

/// <summary>
/// Writes an <c>Order_MBO_50</c> NEW frame (action=NEW).
Expand Down Expand Up @@ -164,14 +173,18 @@ public static void WriteTradingPhaseChanged(
}

/// <summary>
/// Writes a <c>SecurityStatus_3</c> frame for an instrument halt
/// (<c>securityTradingEvent</c> = <see cref="SecurityTradingEventHalt"/>).
/// Used for <c>OnInstrumentHalted</c>.
/// Writes a <c>SecurityStatus_3</c> frame for an instrument halt.
/// <c>securityTradingStatus</c> is always
/// <see cref="SecurityTradingStatusForbidden"/> (issue #583: the
/// official "not available for trading" status, not the pre-halt
/// phase) and <c>securityTradingEvent</c> is
/// <see cref="SecurityTradingEventSecurityStatusChange"/>, marking
/// that this instrument's status now diverges from its security
/// group's phase. Used for <c>OnInstrumentHalted</c>.
/// </summary>
public static void WriteInstrumentHalted(
IUmdfFrameSink sink,
long securityId,
byte securityTradingStatus,
uint rptSeq,
ulong transactTimeNanos)
{
Expand All @@ -183,8 +196,8 @@ public static void WriteInstrumentHalted(
dst,
securityId: securityId,
tradingSessionId: 0,
securityTradingStatus: securityTradingStatus,
securityTradingEvent: SecurityTradingEventHalt,
securityTradingStatus: SecurityTradingStatusForbidden,
securityTradingEvent: SecurityTradingEventSecurityStatusChange,
tradeDate: 0,
tradSesOpenTimeNanos: 0,
transactTimeNanos: transactTimeNanos,
Expand All @@ -193,14 +206,19 @@ public static void WriteInstrumentHalted(
}

/// <summary>
/// Writes a <c>SecurityStatus_3</c> frame for an instrument resume
/// (<c>securityTradingEvent</c> = <see cref="SecurityTradingEventResume"/>).
/// Used for <c>OnInstrumentResumed</c>.
/// Writes a <c>SecurityStatus_3</c> frame for an instrument resume.
/// <paramref name="restoredSecurityTradingStatus"/> is the pre-halt
/// phase the engine preserved while the instrument was halted, and
/// <c>securityTradingEvent</c> is
/// <see cref="SecurityTradingEventSecurityRejoinsGroupStatus"/>,
/// marking that this instrument's status now follows its security
/// group's phase again (issue #583). Used for
/// <c>OnInstrumentResumed</c>.
/// </summary>
public static void WriteInstrumentResumed(
IUmdfFrameSink sink,
long securityId,
byte securityTradingStatus,
byte restoredSecurityTradingStatus,
uint rptSeq,
ulong transactTimeNanos)
{
Expand All @@ -212,8 +230,8 @@ public static void WriteInstrumentResumed(
dst,
securityId: securityId,
tradingSessionId: 0,
securityTradingStatus: securityTradingStatus,
securityTradingEvent: SecurityTradingEventResume,
securityTradingStatus: restoredSecurityTradingStatus,
securityTradingEvent: SecurityTradingEventSecurityRejoinsGroupStatus,
tradeDate: 0,
tradSesOpenTimeNanos: 0,
transactTimeNanos: transactTimeNanos,
Expand Down
Loading