Class LpScorecard

java.lang.Object
com.quantfinlib.fx.LpScorecard

public final class LpScorecard extends Object
Streaming per-LP execution quality: the taker-side answer to last look. FX liquidity is quotes, not firm orders — a provider may hold your order and reject it — so the practical measure of an LP is not its displayed spread but its all-in behavior: how often it rejects, how long it holds, what the market does right after a reject (the flow it declined was the flow that was about to pay you), and the effective spread it actually fills at. Every live FX desk keeps exactly this scorecard; the LpRouter consumes it to price rejects into the routing decision.

All statistics are exponentially weighted per event (configurable α), so the card tracks current LP behavior, not the session average of a provider that changed its engine at lunch. Post-reject markout is measured one horizon after each reject against the mid you feed via onMid(double, long): positive markout = the market moved the way you were trying to trade = the reject cost you real money.

Zero allocation, single writer (the execution/aggregation thread). Pending markouts live in a small ring per LP (4 slots): reject bursts — which happen precisely when the market runs and markouts are largest — are sampled rather than overwritten, so the stat cannot be biased low for exactly the LPs it must expose. Only a burst deeper than the ring within one horizon overwrites its oldest entry.

  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final int
    Pending-markout slots per LP (bursts deeper than this overwrite oldest).
  • Constructor Summary

    Constructors
    Constructor
    Description
    LpScorecard(int lpCount)
    5% event weight, 100 ms markout horizon.
    LpScorecard(int lpCount, double alpha, long markoutHorizonNanos)
     
  • Method Summary

    Modifier and Type
    Method
    Description
    long
    attempts(int lp)
    Invariant: attempts == fills + rejects (kept explicit for future outcomes).
    double
    avgHoldNanos(int lp)
    EWMA hold time across fills and rejects, in nanos.
    double
    EWMA effective half-spread paid on fills, in price units.
    long
    fills(int lp)
     
    int
     
    long
    Markouts matured across all LPs — the router-degradation canary: zero while rejects accrue means onMid(double, long) is not wired and the routing penalty is silently zero.
    void
    onFill(int lp, boolean buy, double price, double midAtRequest, long holdNanos)
    An accepted fill.
    void
    onMid(double mid, long timestampNanos)
    Composite mid update: matures every pending reject markout whose horizon has elapsed.
    void
    onReject(int lp, boolean buy, double midAtRequest, long timestampNanos, long holdNanos)
    A last-look reject.
    double
    EWMA post-reject markout in price units — positive means the market moved the way you were trying to trade after the LP declined: the realized cost of that LP's last look.
    void
    Restores the card; pending markouts reset (restore at session start).
    double
    rejectRate(int lp)
    EWMA reject probability in [0, 1]; 0 before any events.
    long
    rejects(int lp)
     
    void
    Persists the learned LP behavior — reject rates, hold times, effective spreads and post-reject markouts.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • PENDING_RING

      public static final int PENDING_RING
      Pending-markout slots per LP (bursts deeper than this overwrite oldest).
      See Also:
  • Constructor Details

    • LpScorecard

      public LpScorecard(int lpCount, double alpha, long markoutHorizonNanos)
      Parameters:
      lpCount - number of LPs (dense indices)
      alpha - EWMA weight per event, e.g. 0.05
      markoutHorizonNanos - how long after a reject the markout is read, e.g. 100ms = 100_000_000L
    • LpScorecard

      public LpScorecard(int lpCount)
      5% event weight, 100 ms markout horizon.
  • Method Details

    • onFill

      public void onFill(int lp, boolean buy, double price, double midAtRequest, long holdNanos)
      An accepted fill.
      Parameters:
      buy - our direction
      price - the all-in fill price
      midAtRequest - composite mid when the order was sent
      holdNanos - time the LP held the order before accepting
    • onReject

      public void onReject(int lp, boolean buy, double midAtRequest, long timestampNanos, long holdNanos)
      A last-look reject. The markout clock starts here: feed mids via onMid(double, long) and the move one horizon later is attributed to this reject.
    • onMid

      public void onMid(double mid, long timestampNanos)
      Composite mid update: matures every pending reject markout whose horizon has elapsed. NaN mids (one-sided composite, feed gap) are ignored — a non-price must never poison the EWMA, which would otherwise stay NaN forever and disable the router's penalty. The common no-pending case is a single compare.
    • rejectRate

      public double rejectRate(int lp)
      EWMA reject probability in [0, 1]; 0 before any events.
    • avgHoldNanos

      public double avgHoldNanos(int lp)
      EWMA hold time across fills and rejects, in nanos.
    • effectiveSpread

      public double effectiveSpread(int lp)
      EWMA effective half-spread paid on fills, in price units.
    • postRejectMarkout

      public double postRejectMarkout(int lp)
      EWMA post-reject markout in price units — positive means the market moved the way you were trying to trade after the LP declined: the realized cost of that LP's last look.
    • maturedMarkouts

      public long maturedMarkouts()
      Markouts matured across all LPs — the router-degradation canary: zero while rejects accrue means onMid(double, long) is not wired and the routing penalty is silently zero.
    • attempts

      public long attempts(int lp)
      Invariant: attempts == fills + rejects (kept explicit for future outcomes).
    • fills

      public long fills(int lp)
    • rejects

      public long rejects(int lp)
    • lpCount

      public int lpCount()
    • writeState

      public void writeState(DataOutput out) throws IOException
      Persists the learned LP behavior — reject rates, hold times, effective spreads and post-reject markouts. The pending-markout ring is intraday (a reject awaiting its horizon) and is not persisted. Format version 2 (v1, from before markout seeding, is still read). See persist.Checkpoint.
      Throws:
      IOException
    • readState

      public void readState(DataInput in) throws IOException
      Restores the card; pending markouts reset (restore at session start). Reads both format versions — a v1 checkpoint carries no per-LP markout counts, so a restored nonzero markout EWMA counts as already-seeded (it is). Throws on an LP-count mismatch or an unknown version.
      Throws:
      IOException