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 @@ -173,7 +173,7 @@ contract supervision is a post-trade analytics concern.
| <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-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), and sweep-phase UMDF `Trade_53.trdSubType` now emits `SWEEP_TRADE` (109). Remaining wire refinements: residual-cancel `ExecRestatementReason=210` is intentionally out of scope while residuals rest, and EntryPoint ER `crossType` / `crossPrioritization` echo remains pending. |
| <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 |
| <a id="gap-32"></a>GAP-32 | 8.3 / lifecycle | Expiring option series are not automatically moved to `Close` based on `ExpirationDate`. | missing | medium | tracked via [RFC 0002](rfc/0002-equity-options-support.md) issue OPT-03 |

Expand Down
14 changes: 7 additions & 7 deletions src/B3.Exchange.Core/ChannelDispatcher.Loop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,11 @@ internal void ProcessOne(in WorkItem item)
_metrics?.IncOrdersIn();
_currentClOrdId = prioClOrd;
BeginAggressor(prioLeg.Quantity);
_engine.Submit(prioLeg);
_engine.SubmitCrossLeg(prioLeg, cross.CrossType, cross.CrossPrioritization);
_metrics?.IncOrdersIn();
_currentClOrdId = otherClOrd;
BeginAggressor(otherLeg.Quantity);
_engine.Submit(otherLeg);
_engine.SubmitCrossLeg(otherLeg, cross.CrossType, cross.CrossPrioritization);
break;
}

Expand All @@ -407,7 +407,7 @@ internal void ProcessOne(in WorkItem item)
try
{
BeginAggressor(sweepLeg.Quantity);
_engine.SubmitCrossSweep(sweepLeg);
_engine.SubmitCrossSweep(sweepLeg, cross.CrossType, cross.CrossPrioritization);
swept = _crossSweepFilledQty.GetValueOrDefault();
}
finally
Expand All @@ -425,7 +425,7 @@ internal void ProcessOne(in WorkItem item)
_metrics?.IncOrdersIn();
_currentClOrdId = prioClOrd;
BeginAggressor(residual);
_engine.Submit(prioLeg with { Quantity = residual });
_engine.SubmitCrossLeg(prioLeg with { Quantity = residual }, cross.CrossType, cross.CrossPrioritization);
}

// Phase 3: the other leg at full OrderQty —
Expand All @@ -435,7 +435,7 @@ internal void ProcessOne(in WorkItem item)
_metrics?.IncOrdersIn();
_currentClOrdId = otherClOrd;
BeginAggressor(otherLeg.Quantity);
_engine.Submit(otherLeg);
_engine.SubmitCrossLeg(otherLeg, cross.CrossType, cross.CrossPrioritization);
}
else
{
Expand All @@ -446,11 +446,11 @@ internal void ProcessOne(in WorkItem item)
_metrics?.IncOrdersIn();
_currentClOrdId = prioClOrd;
BeginAggressor(prioLeg.Quantity);
_engine.Submit(prioLeg);
_engine.SubmitCrossLeg(prioLeg, cross.CrossType, cross.CrossPrioritization);
_metrics?.IncOrdersIn();
_currentClOrdId = otherClOrd;
BeginAggressor(otherLeg.Quantity);
_engine.Submit(otherLeg);
_engine.SubmitCrossLeg(otherLeg, cross.CrossType, cross.CrossPrioritization);
}
break;
}
Expand Down
19 changes: 13 additions & 6 deletions src/B3.Exchange.Gateway/ExecutionReportEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ private static int WriteMemoTrailer(Span<byte> dst, int blockLength, ReadOnlySpa
_ => 0,
};

private static byte EncodeCrossType(Matching.CrossType? value) => value.HasValue ? (byte)value.Value : (byte)255;

private static byte EncodeCrossPrioritization(Matching.CrossPrioritization? value)
=> value.HasValue ? (byte)value.Value : (byte)255;

private static void WriteBusinessHeader(Span<byte> body, uint sessionId, uint msgSeqNum, ulong sendingTimeNanos)
{
// OutboundBusinessHeader (sequential, Pack=1):
Expand All @@ -145,7 +150,8 @@ public static int EncodeExecReportNew(Span<byte> dst,
uint sessionId, uint msgSeqNum, ulong sendingTimeNanos,
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 orderQty, long? priceMantissa, ReadOnlySpan<byte> memo = default, ulong receivedTimeNanos = UTCTimestampNullValue,
Matching.CrossType? crossType = null, Matching.CrossPrioritization? crossPrioritization = null)
{
int total = TotalSize(ExecReportNewBlock, memo.Length);
if (dst.Length < total) throw new ArgumentException("buffer too small for ER_New", nameof(dst));
Expand Down Expand Up @@ -175,8 +181,8 @@ public static int EncodeExecReportNew(Span<byte> dst,
// V3 trailing fields: receivedTime + non-zero null sentinels.
MemoryMarshal.Write(body.Slice(144, 8), in receivedTimeNanos); // ReceivedTime (tag 35544)
// ordTagID@155 null=0 already, investorID@156 null=zeros already, strategyID@168 null=0 already.
body[162] = 255; // CrossType null
body[163] = 255; // CrossPrioritization null
body[162] = EncodeCrossType(crossType); // CrossType
body[163] = EncodeCrossPrioritization(crossPrioritization); // CrossPrioritization
body[164] = 255; // MmProtectionReset null
// V6 trailing field: tradingSubAccount@172 (uint, null=0) — covered
// by body.Clear() above. strategyID@168 (int, null=0) ditto.
Expand Down Expand Up @@ -374,7 +380,8 @@ public static int EncodeExecReportTrade(Span<byte> dst,
Matching.Side side, ulong clOrdIdValue, long secondaryOrderId,
long securityId, long orderId, long lastQty, long lastPxMantissa,
ulong execId, ulong transactTimeNanos, long leavesQty, long cumQty,
bool aggressor, uint tradeId, uint contraBroker, ushort tradeDate, long orderQty, ReadOnlySpan<byte> memo = default)
bool aggressor, uint tradeId, uint contraBroker, ushort tradeDate, long orderQty, ReadOnlySpan<byte> memo = default,
Matching.CrossType? crossType = null, Matching.CrossPrioritization? crossPrioritization = null)
{
int total = TotalSize(ExecReportTradeBlock, memo.Length);
if (dst.Length < total) throw new ArgumentException("buffer too small for ER_Trade", nameof(dst));
Expand Down Expand Up @@ -408,8 +415,8 @@ public static int EncodeExecReportTrade(Span<byte> dst,
body[154] = 255; // TradingSessionID null
body[155] = 255; // TradingSessionSubID null
body[156] = 255; // SecurityTradingStatus null
body[157] = 255; // CrossType null
body[158] = 255; // CrossPrioritization null
body[157] = EncodeCrossType(crossType); // CrossType
body[158] = EncodeCrossPrioritization(crossPrioritization); // CrossPrioritization
// strategyID@160 (int, null=0), impliedEventID@164 (6 bytes; eventID
// and noRelatedTrades both null=0) and tradingSubAccount@170 (uint,
// null=0) are covered by body.Clear() above.
Expand Down
4 changes: 2 additions & 2 deletions src/B3.Exchange.Gateway/FixpOutboundEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public bool WriteExecutionReportNew(in OrderAcceptedEvent e, ulong receivedTimeN
(ulong)e.RptSeq, e.InsertTimestampNanos,
OrderType.Limit, TimeInForce.Day,
e.RemainingQuantity, e.PriceMantissa,
memo.Span, receivedTimeNanos);
memo.Span, receivedTimeNanos, e.CrossType, e.CrossPrioritization);
return AppendAndEnqueueLocked(exact, durability);
}
}
Expand Down Expand Up @@ -112,7 +112,7 @@ public bool WriteExecutionReportTrade(in TradeEvent e, bool isAggressor, long ow
isAggressor, e.TradeId,
isAggressor ? e.RestingFirm : e.AggressorFirm,
tradeDate: 0,
orderQty: leavesQty + cumQty, memo.Span);
orderQty: leavesQty + cumQty, memo.Span, e.CrossType, e.CrossPrioritization);
return AppendAndEnqueueLocked(exact, durability);
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/B3.Exchange.Matching/Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public readonly record struct OrderAcceptedEvent(
uint EnteringFirm,
ulong InsertTimestampNanos,
uint RptSeq,
byte[]? Memo = null);
byte[]? Memo = null,
CrossType? CrossType = null,
CrossPrioritization? CrossPrioritization = null);

/// <summary>
/// Fired when a resting order's remaining quantity is reduced as a passive maker
Expand Down Expand Up @@ -89,7 +91,9 @@ public readonly record struct TradeEvent(
ulong TransactTimeNanos,
uint RptSeq,
byte[]? AggressorMemo = null,
byte[]? RestingMemo = null);
byte[]? RestingMemo = null,
CrossType? CrossType = null,
CrossPrioritization? CrossPrioritization = null);

public readonly record struct RejectEvent(
string ClOrdId,
Expand Down
36 changes: 26 additions & 10 deletions src/B3.Exchange.Matching/MatchingEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,14 @@ private void FinalizeUncrossSide(LimitOrderBook book, RestingOrder o, long lastT
/// </summary>
public void SubmitCrossSweep(NewOrderCommand cmd) => SubmitImpl(cmd, emitUnmatchedIocClose: false);

private void SubmitImpl(NewOrderCommand cmd, bool emitUnmatchedIocClose)
public void SubmitCrossSweep(NewOrderCommand cmd, CrossType crossType, CrossPrioritization crossPrioritization)
=> SubmitImpl(cmd, emitUnmatchedIocClose: false, crossType, crossPrioritization);

public void SubmitCrossLeg(NewOrderCommand cmd, CrossType crossType, CrossPrioritization crossPrioritization)
=> SubmitImpl(cmd, emitUnmatchedIocClose: true, crossType, crossPrioritization);

private void SubmitImpl(NewOrderCommand cmd, bool emitUnmatchedIocClose,
CrossType? crossType = null, CrossPrioritization? crossPrioritization = null)
{
_currentMemo = cmd.Memo;
EnterDispatch();
Expand Down Expand Up @@ -985,10 +992,10 @@ private void SubmitImpl(NewOrderCommand cmd, bool emitUnmatchedIocClose)
// rest cmd directly with cmd.PriceMantissa.
if (phase == TradingPhase.Reserved || phase == TradingPhase.FinalClosingCall)
{
RestForAuction(cmd, rules, book);
RestForAuction(cmd, rules, book, crossType, crossPrioritization);
return;
}
ExecuteAggressor(cmd, rules, book, emitUnmatchedIocClose);
ExecuteAggressor(cmd, rules, book, emitUnmatchedIocClose, crossType, crossPrioritization);
}
finally { ExitDispatch(); }
}
Expand Down Expand Up @@ -1574,8 +1581,9 @@ public void Replace(ReplaceOrderCommand cmd)
}

private void ExecuteAggressor(NewOrderCommand cmd, InstrumentTradingRules rules, LimitOrderBook book,
bool emitUnmatchedIocClose)
=> ExecuteAggressorWithOrderId(cmd, rules, book, _nextOrderId++, emitUnmatchedIocClose);
bool emitUnmatchedIocClose, CrossType? crossType = null, CrossPrioritization? crossPrioritization = null)
=> ExecuteAggressorWithOrderId(cmd, rules, book, _nextOrderId++, emitUnmatchedIocClose,
crossType, crossPrioritization);

/// <summary>
/// Issue #228 (Onda M · M1): rest the order on the book without any
Expand All @@ -1588,7 +1596,8 @@ private void ExecuteAggressor(NewOrderCommand cmd, InstrumentTradingRules rules,
/// (<see cref="NewOrderCommand.MaxFloor"/>) is honored so the visible
/// slice on the book matches the continuous-Open semantics from #211.
/// </summary>
private void RestForAuction(NewOrderCommand cmd, InstrumentTradingRules rules, LimitOrderBook book)
private void RestForAuction(NewOrderCommand cmd, InstrumentTradingRules rules, LimitOrderBook book,
CrossType? crossType = null, CrossPrioritization? crossPrioritization = null)
{
long visible = cmd.Quantity;
long hidden = 0;
Expand Down Expand Up @@ -1624,7 +1633,9 @@ private void RestForAuction(NewOrderCommand cmd, InstrumentTradingRules rules, L
RemainingQuantity: resting.RemainingQuantity,
EnteringFirm: resting.EnteringFirm,
InsertTimestampNanos: resting.InsertTimestampNanos,
RptSeq: NextRptSeq()));
RptSeq: NextRptSeq(),
CrossType: crossType,
CrossPrioritization: crossPrioritization));

RecomputeAuctionTopIfApplicable(book.SecurityId, cmd.EnteredAtNanos);
}
Expand Down Expand Up @@ -1813,7 +1824,8 @@ private static bool IsMarketLike(OrderType t)
=> t == OrderType.Market || t == OrderType.MarketWithLeftover;

private void ExecuteAggressorWithOrderId(NewOrderCommand cmd, InstrumentTradingRules rules,
LimitOrderBook book, long aggressorOrderIdForTrades, bool emitUnmatchedIocClose)
LimitOrderBook book, long aggressorOrderIdForTrades, bool emitUnmatchedIocClose,
CrossType? crossType = null, CrossPrioritization? crossPrioritization = null)
{
long aggressorRemaining = cmd.Quantity;
bool isMarket = IsMarketLike(cmd.Type);
Expand Down Expand Up @@ -1883,7 +1895,9 @@ private void ExecuteAggressorWithOrderId(NewOrderCommand cmd, InstrumentTradingR
TransactTimeNanos: cmd.EnteredAtNanos,
RptSeq: NextRptSeq(),
AggressorMemo: cmd.Memo,
RestingMemo: maker.Memo));
RestingMemo: maker.Memo,
CrossType: crossType,
CrossPrioritization: crossPrioritization));

aggressorRemaining -= tradeQty;
maker.RemainingQuantity -= tradeQty;
Expand Down Expand Up @@ -2083,7 +2097,9 @@ private void ExecuteAggressorWithOrderId(NewOrderCommand cmd, InstrumentTradingR
EnteringFirm: resting.EnteringFirm,
InsertTimestampNanos: resting.InsertTimestampNanos,
RptSeq: NextRptSeq(),
Memo: resting.Memo));
Memo: resting.Memo,
CrossType: crossType,
CrossPrioritization: crossPrioritization));
}
finally
{
Expand Down
53 changes: 53 additions & 0 deletions tests/B3.Exchange.Core.Tests/CrossOrderSemanticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,59 @@ public void AgainstBook_MaxSweepQtyZero_StillBehavesLikeAon()
Assert.Equal(100, s.Trades[0].Quantity);
}

[Fact]
public void AgainstBook_ExecutionReportsEchoSubmittedCrossAttributes()
{
var (disp, _, outbound) = NewDispatcher();
var external = new FakeSession(outbound);
var crosser = new FakeSession(outbound);

disp.EnqueueNewOrder(
new NewOrderCommand("EXT", Petr, Side.Sell, OrderType.Limit, TimeInForce.Day, Px(9.99m), 100, external.EnteringFirm, 1_000UL),
external.Id, external.EnteringFirm, clOrdIdValue: 1UL);
DrainInbound(disp);

Assert.Single(external.News);
Assert.Null(external.News[0].CrossType);
Assert.Null(external.News[0].CrossPrioritization);

var cross = new CrossOrderCommand(Buy(200, 10m, 10UL), Sell(200, 10m, 11UL), 10UL, 11UL, 999UL)
{
CrossType = CrossType.AgainstBook,
CrossPrioritization = CrossPrioritization.BuyPrioritized,
MaxSweepQty = 100,
};
disp.EnqueueCross(cross, crosser.Id, crosser.EnteringFirm);
DrainInbound(disp);

Assert.NotEmpty(crosser.News);
Assert.All(crosser.News, e =>
{
Assert.Equal(CrossType.AgainstBook, e.CrossType);
Assert.Equal(CrossPrioritization.BuyPrioritized, e.CrossPrioritization);
});
Assert.NotEmpty(crosser.Trades);
Assert.All(crosser.Trades, e =>
{
Assert.Equal(CrossType.AgainstBook, e.CrossType);
Assert.Equal(CrossPrioritization.BuyPrioritized, e.CrossPrioritization);
});
}

[Fact]
public void LimitOrder_ExecutionReportNewLeavesCrossAttributesNull()
{
var (disp, _, outbound) = NewDispatcher();
var session = new FakeSession(outbound);

disp.EnqueueNewOrder(Buy(100, 10m, 1UL), session.Id, session.EnteringFirm, clOrdIdValue: 1UL);
DrainInbound(disp);

Assert.Single(session.News);
Assert.Null(session.News[0].CrossType);
Assert.Null(session.News[0].CrossPrioritization);
}

[Fact]
public void MaxOpenOrdersPerFirm_CrossAtCapMinusOneIsRejectedForWorstCaseResidual()
{
Expand Down
Loading