Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/B3-ENTRYPOINT-COMPLIANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ contract supervision is a post-trade analytics concern.
| <a id="gap-25"></a>GAP-25 | 7.1.20 | In-flight modification semantics (priority loss rules) | partial | medium | — |
| <a id="gap-26"></a>GAP-26 | 8.3 | Daily GTC/GTD restatement at session boundary (carry-over + re-emit ER) | done | — | At each daily-reset boundary the exchange emits a private restatement `ExecutionReport_Modify` (`OrdStatus=RESTATED 'R'`, `ExecRestatementReason=GT_RESTATEMENT 1`) for every surviving GTC and unexpired-GTD order, plus parked GTC stop / stop-limit orders. The restatement echoes the order's real `OrdType` / `TimeInForce` / `ExpireDate` / `StopPx`, reports new-trading-day quantities (`CumQty=0`, `LeavesQty == OrderQty == open`, iceberg-aware), and changes no book state (no UMDF frame, no `RptSeq` advance, no eviction). Runs after `GtdExpirySweeper` so past-dated GTD orders are cancelled, not restated. ([#498](https://github.com/pedrosakuma/B3MatchingPlatform/issues/498), [#507](https://github.com/pedrosakuma/B3MatchingPlatform/pull/507)) Day-order boundary expiry is tracked separately by [#506](https://github.com/pedrosakuma/B3MatchingPlatform/issues/506). |
| <a id="gap-27"></a>GAP-27 | 15.4 | Self-Trading Prevention (STPC) | missing | medium (in-scope per ADR 0012) | covered by [#14](https://github.com/pedrosakuma/B3MatchingPlatform/issues/14) |
| <a id="gap-28"></a>GAP-28 | 15.5 | Market Protections (price collars / fat-finger / max value) | partial | medium (in-scope per ADR 0012) | Engine-side static per-instrument price bands, auction-phase TOP collars, per-instrument max order quantity, and per-instrument max order value are implemented with existing ER reject reasons. The gateway dynamic last-trade-relative `priceBandPercent` remains as an outer decode-time guardrail. Outstanding: EntryPoint `protectionPrice` semantics for market orders are deferred. Tracked by [#500](https://github.com/pedrosakuma/B3MatchingPlatform/issues/500). |
| <a id="gap-28"></a>GAP-28 | 15.5 | Market Protections (price collars / fat-finger / max value) | done | | Engine-side static per-instrument price bands, auction-phase TOP collars, per-instrument max order quantity, and per-instrument max order value are implemented with existing ER reject reasons. The gateway dynamic last-trade-relative `priceBandPercent` remains as an outer decode-time guardrail. EntryPoint `protectionPrice` is echoed on `ExecutionReport_New` / `ExecutionReport_Modify` for Market-with-leftover-as-Limit (`OrdType=K`) remainders that rest at the last execution price. Triggered Stop Protect → MWL behavior, if B3 requires it, is tracked separately and is not a GAP-28 blocker. Tracked by [#500](https://github.com/pedrosakuma/B3MatchingPlatform/issues/500) and [#530](https://github.com/pedrosakuma/B3MatchingPlatform/issues/530). |
| <a id="gap-29"></a>GAP-29 | 15.1 | User-Defined Spreads (UDS) — synthetic multi-leg instruments | missing | low (boundary case; borderline between exchange-side and broker-side) | — |
| <a id="gap-30"></a>GAP-30 | 16.6 | Sweep & Cross | partial | low (in-scope per ADR 0012) | Matching semantics for `CrossType=AgainstBook` are implemented (#218), sweep-phase UMDF `Trade_53.trdSubType` emits `SWEEP_TRADE` (109), and EntryPoint `ExecutionReport_New` / `ExecutionReport_Trade` now echo `crossType` + `crossPrioritization` for cross-generated ERs (#529). Remaining wire refinement: residual-cancel `ExecRestatementReason=210` is intentionally out of scope while residuals rest. |
| <a id="gap-31"></a>GAP-31 | 7.1.19 / UMDF v2.2.0 `SecurityDefinition_12` | `SecurityDefinition_12` does not emit option fields (`strikePrice`, `putOrCall`, `exerciseStyle`, `contractMultiplier`, `noUnderlyings`, `optPayoutType`, `maturityMonthYear`). | missing | high | tracked via [RFC 0002](rfc/0002-equity-options-support.md) issue OPT-02 |
Expand Down
12 changes: 12 additions & 0 deletions src/B3.Exchange.Contracts/Ports.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using B3.Exchange.Matching;
using MatchingRejectEvent = B3.Exchange.Matching.RejectEvent;
using MatchingSide = B3.Exchange.Matching.Side;
using MatchingOrderType = B3.Exchange.Matching.OrderType;

namespace B3.Exchange.Contracts;

Expand Down Expand Up @@ -72,6 +73,17 @@ bool WriteExecutionReportModify(SessionId session, long securityId, long orderId
DurabilityHandle durability = default,
InvestorId? investorId = null);

bool WriteExecutionReportModify(SessionId session, long securityId, long orderId,
ulong clOrdIdValue, ulong origClOrdIdValue,
MatchingSide side, long newPriceMantissa, long newRemainingQty, ulong transactTimeNanos, uint rptSeq,
MatchingOrderType ordType, long? protectionPriceMantissa,
ulong receivedTimeNanos = ulong.MaxValue,
DurabilityHandle durability = default,
InvestorId? investorId = null)
=> WriteExecutionReportModify(session, securityId, orderId, clOrdIdValue, origClOrdIdValue,
side, newPriceMantissa, newRemainingQty, transactTimeNanos, rptSeq,
receivedTimeNanos, durability, investorId);

bool WriteExecutionReportReject(SessionId session, in MatchingRejectEvent e, ulong clOrdIdValue,
DurabilityHandle durability = default);

Expand Down
9 changes: 6 additions & 3 deletions src/B3.Exchange.Core/ChannelDispatcher.Sinks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public void OnOrderModified(in OrderModifiedEvent e)
side: e.Side, newPriceMantissa: e.NewPriceMantissa,
newRemainingQty: e.NewRemainingQuantity,
transactTimeNanos: e.TransactTimeNanos, rptSeq: e.RptSeq,
ordType: e.OrdType,
protectionPriceMantissa: e.ProtectionPriceMantissa,
receivedTimeNanos: _currentReceivedTimeNanos,
durability: CurrentDurability,
investorId: e.InvestorId);
Expand Down Expand Up @@ -524,8 +526,8 @@ public void OnIcebergReplenished(in IcebergReplenishedEvent e)
/// can resolve the owner) and emit ER_New back to the active session.
/// No MBO frame is emitted because stops are not on the public book
/// until they trigger. The wire ER reuses the regular OrderAccepted
/// shape and therefore does not carry StopPx/OrdType=Stop on the
/// wire — an MVP limitation tracked in the issue body.
/// shape and carries the stop OrdType so ER_New echoes the accepted
/// stop semantics. StopPx is still not present on ER_New.
/// </summary>
public void OnStopOrderAccepted(in StopOrderAcceptedEvent e)
{
Expand All @@ -544,7 +546,8 @@ public void OnStopOrderAccepted(in StopOrderAcceptedEvent e)
RemainingQuantity: e.Quantity,
EnteringFirm: e.EnteringFirm,
InsertTimestampNanos: e.InsertTimestampNanos,
RptSeq: e.RptSeq);
RptSeq: e.RptSeq,
OrdType: e.StopType);
_outbound.WriteExecutionReportNew(_currentSession, _currentFirm, _currentClOrdId, accepted, _currentReceivedTimeNanos, CurrentDurability);
_metrics?.IncExecutionReport(ExecutionReportKind.New);
}
Expand Down
17 changes: 13 additions & 4 deletions src/B3.Exchange.Gateway/ExecutionReportEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ public static int EncodeExecReportNew(Span<byte> dst,
Matching.Side side, ulong clOrdIdValue, long secondaryOrderId, long securityId, long orderId,
ulong execId, ulong transactTimeNanos, Matching.OrderType ordType, Matching.TimeInForce tif,
long orderQty, long? priceMantissa, ReadOnlySpan<byte> memo = default, ulong receivedTimeNanos = UTCTimestampNullValue,
long? protectionPriceMantissa = null,
Matching.CrossType? crossType = null, Matching.CrossPrioritization? crossPrioritization = null)
{
int total = TotalSize(ExecReportNewBlock, memo.Length);
Expand All @@ -171,7 +172,10 @@ public static int EncodeExecReportNew(Span<byte> dst,
ulong nullTs = UTCTimestampNullValue;
MemoryMarshal.Write(body.Slice(72, 8), in nullTs); // MarketSegmentReceivedTime
long nullPx = PriceNullMantissa;
MemoryMarshal.Write(body.Slice(80, 8), in nullPx); // ProtectionPrice
long protectionPx = ordType == Matching.OrderType.MarketWithLeftover
? protectionPriceMantissa ?? PriceNullMantissa
: PriceNullMantissa;
MemoryMarshal.Write(body.Slice(80, 8), in protectionPx); // ProtectionPrice
body[92] = EncodeOrdType(ordType);
body[93] = EncodeTif(tif);
MemoryMarshal.Write(body.Slice(96, 8), in orderQty);
Expand All @@ -195,7 +199,9 @@ public static int EncodeExecReportModify(Span<byte> dst,
long securityId, long orderId, ulong execId, ulong transactTimeNanos,
long leavesQty, long cumQty, long orderQty, long priceMantissa,
ReadOnlySpan<byte> memo = default, ulong receivedTimeNanos = UTCTimestampNullValue,
Matching.InvestorId? investorId = null)
Matching.InvestorId? investorId = null,
Matching.OrderType ordType = Matching.OrderType.Limit,
long? protectionPriceMantissa = null)
{
int total = TotalSize(ExecReportModifyBlock, memo.Length);
if (dst.Length < total) throw new ArgumentException("buffer too small for ER_Modify", nameof(dst));
Expand All @@ -218,8 +224,11 @@ public static int EncodeExecReportModify(Span<byte> dst,
MemoryMarshal.Write(body.Slice(88, 8), in orderId);
MemoryMarshal.Write(body.Slice(96, 8), in origClOrdIdValue);
long nullPx = PriceNullMantissa;
MemoryMarshal.Write(body.Slice(104, 8), in nullPx); // ProtectionPrice
body[116] = OrdTypeLimit; // OrdType - replace can only be on limit
long protectionPx = ordType == Matching.OrderType.MarketWithLeftover
? protectionPriceMantissa ?? PriceNullMantissa
: PriceNullMantissa;
MemoryMarshal.Write(body.Slice(104, 8), in protectionPx); // ProtectionPrice
body[116] = EncodeOrdType(ordType); // OrdType
body[117] = TifDay; // TimeInForce - replace inherits original; default Day
MemoryMarshal.Write(body.Slice(120, 8), in orderQty);
MemoryMarshal.Write(body.Slice(128, 8), in priceMantissa);
Expand Down
12 changes: 8 additions & 4 deletions src/B3.Exchange.Gateway/FixpOutboundEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ public bool WriteExecutionReportNew(in OrderAcceptedEvent e, ulong receivedTimeN
_sessionId(), _nextMsgSeqNum(), e.InsertTimestampNanos,
e.Side, clOrd, e.OrderId, e.SecurityId, e.OrderId,
(ulong)e.RptSeq, e.InsertTimestampNanos,
OrderType.Limit, TimeInForce.Day,
e.OrdType, TimeInForce.Day,
e.RemainingQuantity, e.PriceMantissa,
memo.Span, receivedTimeNanos, e.CrossType, e.CrossPrioritization);
memo.Span, receivedTimeNanos, e.ProtectionPriceMantissa, e.CrossType, e.CrossPrioritization);
return AppendAndEnqueueLocked(exact, durability);
}
}
Expand Down Expand Up @@ -154,7 +154,9 @@ public bool WriteExecutionReportModify(long securityId, long orderId, ulong clOr
Side side, long newPriceMantissa, long newRemainingQty, ulong transactTimeNanos, uint rptSeq,
ulong receivedTimeNanos = ulong.MaxValue,
DurabilityHandle durability = default, ReadOnlyMemory<byte> memo = default,
Matching.InvestorId? investorId = null)
Matching.InvestorId? investorId = null,
OrderType ordType = OrderType.Limit,
long? protectionPriceMantissa = null)
{
if (!_isOpen()) return false;
var exact = PooledOutboundFrame.Rent(ExecutionReportEncoder.TotalSize(ExecutionReportEncoder.ExecReportModifyBlock, memo.Length));
Expand All @@ -168,7 +170,9 @@ public bool WriteExecutionReportModify(long securityId, long orderId, ulong clOr
securityId, orderId, (ulong)rptSeq, transactTimeNanos,
leavesQty: newRemainingQty, cumQty: 0, orderQty: newRemainingQty, priceMantissa: newPriceMantissa,
memo: memo.Span, receivedTimeNanos: receivedTimeNanos,
investorId: investorId);
investorId: investorId,
ordType: ordType,
protectionPriceMantissa: protectionPriceMantissa);
return AppendAndEnqueueLocked(exact, durability);
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/B3.Exchange.Gateway/FixpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,9 +616,12 @@ public bool WriteExecutionReportModify(long securityId, long orderId, ulong clOr
ulong receivedTimeNanos = ulong.MaxValue,
DurabilityHandle durability = default,
ReadOnlyMemory<byte> memo = default,
B3.Exchange.Matching.InvestorId? investorId = null)
B3.Exchange.Matching.InvestorId? investorId = null,
B3.Exchange.Matching.OrderType ordType = B3.Exchange.Matching.OrderType.Limit,
long? protectionPriceMantissa = null)
=> _outboundEncoder.WriteExecutionReportModify(securityId, orderId, clOrdIdValue, origClOrdIdValue,
side, newPriceMantissa, newRemainingQty, transactTimeNanos, rptSeq, receivedTimeNanos, durability, memo, investorId);
side, newPriceMantissa, newRemainingQty, transactTimeNanos, rptSeq, receivedTimeNanos, durability, memo, investorId,
ordType, protectionPriceMantissa);

public bool WriteExecutionReportRestate(in B3.Exchange.Matching.OrderRestatedEvent e, ulong ownerClOrdId,
DurabilityHandle durability = default, ReadOnlyMemory<byte> memo = default)
Expand Down
14 changes: 14 additions & 0 deletions src/B3.Exchange.Gateway/GatewayRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ public bool WriteExecutionReportModify(ContractsSessionId session, long security
memo: default, investorId: investorId);
}

public bool WriteExecutionReportModify(ContractsSessionId session, long securityId, long orderId,
ulong clOrdIdValue, ulong origClOrdIdValue,
Side side, long newPriceMantissa, long newRemainingQty, ulong transactTimeNanos, uint rptSeq,
OrderType ordType, long? protectionPriceMantissa,
ulong receivedTimeNanos = ulong.MaxValue,
DurabilityHandle durability = default,
Matching.InvestorId? investorId = null)
{
if (!_registry.TryGet(session, out var s)) { LogMiss(session, "ExecReportModify"); return false; }
return s.WriteExecutionReportModify(securityId, orderId, clOrdIdValue, origClOrdIdValue,
side, newPriceMantissa, newRemainingQty, transactTimeNanos, rptSeq, receivedTimeNanos, durability,
memo: default, investorId: investorId, ordType: ordType, protectionPriceMantissa: protectionPriceMantissa);
}

public bool WriteExecutionReportReject(ContractsSessionId session, in RejectEvent e, ulong clOrdIdValue,
DurabilityHandle durability = default)
{
Expand Down
21 changes: 11 additions & 10 deletions src/B3.Exchange.Matching/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,12 @@ public sealed record CancelOrderCommand(
/// <summary>
/// Replace command. <see cref="NewQuantity"/> is interpreted as the new
/// <em>remaining open quantity</em> (not the original total). Replace cannot
/// change Side, SecurityId, Type or TIF — those would be a new order.
/// Priority is preserved iff (PriceMantissa unchanged AND NewQuantity &lt;= current
/// remaining qty); otherwise the engine emits a <see cref="OrderCanceledEvent"/>
/// with <see cref="CancelReason.ReplaceLostPriority"/> followed by an
/// change Side or SecurityId; Type/TIF overrides are carried by
/// <see cref="NewOrdType"/> and <see cref="NewTif"/>. Priority is preserved iff
/// the effective type can rest in place, TIF and price priority are unchanged,
/// and NewQuantity &lt;= current remaining qty; otherwise the engine emits a
/// <see cref="OrderCanceledEvent"/> with
/// <see cref="CancelReason.ReplaceLostPriority"/> followed by an
/// <see cref="OrderAcceptedEvent"/> for the replacement (which may then cross
/// or rest like a brand-new order, including emitting trades).
/// </summary>
Expand All @@ -307,12 +309,11 @@ public sealed record ReplaceOrderCommand(
ulong EnteredAtNanos)
{
/// <summary>
/// New order type for the replacement. <c>null</c> means "preserve
/// the resting order's type" (always <see cref="OrderType.Limit"/>
/// for an order that is on the book). Setting <c>OrderType.Market</c>
/// turns the priority-loss path into a market aggressor that consumes
/// liquidity and never rests; <see cref="NewTif"/> must then be
/// IOC or FOK. Issue #204.
/// New order type for the replacement. <c>null</c> means "preserve as
/// resting-limit semantics" for an order that is on the book. Setting
/// <c>OrderType.Market</c> turns the priority-loss path into a market
/// aggressor that consumes liquidity and never rests; <see cref="NewTif"/>
/// must then be IOC or FOK. Issue #204.
/// </summary>
public OrderType? NewOrdType { get; init; }

Expand Down
6 changes: 5 additions & 1 deletion src/B3.Exchange.Matching/Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public readonly record struct OrderAcceptedEvent(
ulong InsertTimestampNanos,
uint RptSeq,
byte[]? Memo = null,
OrderType OrdType = OrderType.Limit,
long? ProtectionPriceMantissa = null,
CrossType? CrossType = null,
CrossPrioritization? CrossPrioritization = null);

Expand Down Expand Up @@ -327,7 +329,9 @@ public readonly record struct OrderModifiedEvent(
ulong TransactTimeNanos,
uint RptSeq,
byte[]? Memo = null,
InvestorId? InvestorId = null);
InvestorId? InvestorId = null,
OrderType OrdType = OrderType.Limit,
long? ProtectionPriceMantissa = null);

/// <summary>
/// GAP-26 / issue #498: fired once per surviving Good-Till (GTC or
Expand Down
Loading