@@ -22,30 +22,43 @@ pub fn should_requote(last_bid: Option<f64>, new_bid: f64, threshold_bps: u32) -
2222}
2323
2424/// True when a side's order should be re-signed: no prior order, the price
25- /// moved at least `threshold_bps`, or the last post is late enough in its TTL so a
26- /// replacement lands before the live order expires. The age gate keeps a stable
27- /// market from going dark when the price-move gate alone would never fire before
28- /// the TTL lapses, but waits until two-thirds of the TTL to limit how long old
29- /// signed orders overlap with their replacements. `last` is
25+ /// moved at least `threshold_bps`, or the last post is old enough that a
26+ /// replacement should land while the live order still has `repost_lead_secs` of
27+ /// life. The age gate keeps a stable market from going dark when the price-move
28+ /// gate alone would never fire before the TTL lapses. `last` is
3029/// `(price, posted_at_unix)`.
3130pub fn should_requote_now (
3231 last : Option < ( f64 , u64 ) > ,
3332 new_bid : f64 ,
3433 threshold_bps : u32 ,
3534 now : u64 ,
3635 ttl_secs : u64 ,
36+ repost_lead_secs : u64 ,
3737) -> bool {
3838 match last {
3939 None => true ,
4040 Some ( ( prev, posted_at) ) => {
41- let aged = now. saturating_sub ( posted_at) >= requote_age_secs ( ttl_secs) ;
41+ let aged =
42+ now. saturating_sub ( posted_at) >= requote_age_secs ( ttl_secs, repost_lead_secs) ;
4243 aged || should_requote ( Some ( prev) , new_bid, threshold_bps)
4344 }
4445 }
4546}
4647
47- fn requote_age_secs ( ttl_secs : u64 ) -> u64 {
48- ttl_secs. saturating_mul ( 2 ) / 3
48+ /// The order age at which a side reposts: `ttl_secs - repost_lead_secs`, so the
49+ /// replacement is signed while the live order still has `repost_lead_secs` of
50+ /// life and the two overlap instead of leaving a gap.
51+ ///
52+ /// The lead is capped at half the TTL. That cap is a safety floor against a
53+ /// large or misconfigured lead driving the repost age to zero (which would
54+ /// re-sign the whole ladder every tick), but it has a cost: below `ttl ≈ 2 ×
55+ /// lead` the effective overlap is `ttl/2`, not the configured lead. So as the
56+ /// TTL drops the gap-free margin shrinks back toward the indexer's deadline
57+ /// margin. Keep `ttl_secs ≥ 2 × repost_lead_secs` to actually get the lead you
58+ /// asked for.
59+ fn requote_age_secs ( ttl_secs : u64 , repost_lead_secs : u64 ) -> u64 {
60+ let lead = repost_lead_secs. min ( ttl_secs / 2 ) ;
61+ ttl_secs. saturating_sub ( lead)
4962}
5063
5164#[ cfg( test) ]
@@ -70,15 +83,38 @@ mod tests {
7083
7184 #[ test]
7285 fn requotes_near_expiry_even_when_price_is_flat ( ) {
86+ // 30s TTL, 10s lead → repost at age 20 (30 - 10).
7387 // First quote always.
74- assert ! ( should_requote_now( None , 1.0 , 10 , 0 , 30 ) ) ;
75- // 5s into a 30s TTL, flat price → not yet.
76- assert ! ( !should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 5 , 30 ) ) ;
77- // 15s in, flat price → wait to reduce old/new signed-order overlap .
78- assert ! ( !should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 15 , 30 ) ) ;
79- // 20s in (two-thirds of TTL ), flat price → re-quote to avoid a gap.
80- assert ! ( should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 20 , 30 ) ) ;
88+ assert ! ( should_requote_now( None , 1.0 , 10 , 0 , 30 , 10 ) ) ;
89+ // 5s into the TTL, flat price → not yet.
90+ assert ! ( !should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 5 , 30 , 10 ) ) ;
91+ // 15s in, flat price → still before the repost age .
92+ assert ! ( !should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 15 , 30 , 10 ) ) ;
93+ // 20s in (lead before expiry ), flat price → re-quote to avoid a gap.
94+ assert ! ( should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 20 , 30 , 10 ) ) ;
8195 // A big move still re-quotes before the age gate.
82- assert ! ( should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.002 , 10 , 1 , 30 ) ) ;
96+ assert ! ( should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.002 , 10 , 1 , 30 , 10 ) ) ;
97+ }
98+
99+ #[ test]
100+ fn lead_time_sets_repost_age_and_clamps_at_half_ttl ( ) {
101+ // ttl 240, lead 60 → repost at age 180 (240 - 60), 60s overlap.
102+ assert ! ( !should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 179 , 240 , 60 ) ) ;
103+ assert ! ( should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 180 , 240 , 60 ) ) ;
104+
105+ // The clamp: an oversized lead is capped at ttl/2, so the repost age
106+ // never collapses toward zero. ttl 120, lead 1000 → age 60, not 0.
107+ assert ! ( !should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 59 , 120 , 1000 ) ) ;
108+ assert ! ( should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 60 , 120 , 1000 ) ) ;
109+
110+ // The cost of the clamp: at ttl = 2 × lead the overlap is exactly the
111+ // lead; below that it silently shrinks to ttl/2. ttl 90, lead 60 → the
112+ // lead is capped to 45, so the real overlap is 45s, not 60s.
113+ assert ! ( !should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 44 , 90 , 60 ) ) ;
114+ assert ! ( should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 45 , 90 , 60 ) ) ;
115+
116+ // Zero lead → repost only at the deadline (age == ttl).
117+ assert ! ( !should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 239 , 240 , 0 ) ) ;
118+ assert ! ( should_requote_now( Some ( ( 1.0 , 0 ) ) , 1.0 , 10 , 240 , 240 , 0 ) ) ;
83119 }
84120}
0 commit comments