@@ -70,10 +70,20 @@ interface BasicStrategyConfig {
7070interface FxStrategyConfig {
7171 type: "hyperfx"
7272 /**
73- * Array of (amount, price) coordinates defining the exotic token price curve.
74- * price = exotic tokens per 1 USD at that order amount (e.g. 1600 for cNGN at 1600 NGN/USD).
73+ * Bid price curve: exotic tokens per 1 USD when the filler *buys* exotic from a user
74+ * (exotic→stable leg). Should have a higher exotic-per-USD rate than the ask curve so
75+ * the filler pays out fewer stablecoins per exotic token received.
7576 */
76- priceCurve: Array < {
77+ bidPriceCurve: Array < {
78+ amount : string
79+ price : string
80+ } >
81+ /**
82+ * Ask price curve: exotic tokens per 1 USD when the filler *sells* exotic to a user
83+ * (stable→exotic leg). Should have a lower exotic-per-USD rate than the bid curve so
84+ * the filler sends fewer exotic tokens per stablecoin received.
85+ */
86+ askPriceCurve : Array < {
7787 amount : string
7888 price : string
7989 } >
@@ -95,61 +105,65 @@ function fmtNum(v: number): string {
95105}
96106
97107// Render a 2D point set as ASCII chart rows (chart body + axis row)
108+ // Axes: vertical = amount ($, high at top), horizontal = price/bps (low left, high right)
98109function renderCurveAscii ( points : Array < { x : number ; y : number } > , yLabel : string , chartWidth = 30 , chartHeight = 5 ) : string [ ] {
99110 const sorted = [ ...points ] . sort ( ( a , b ) => a . x - b . x )
100- const minX = sorted [ 0 ] . x
101- const maxX = sorted [ sorted . length - 1 ] . x
102- const minY = Math . min ( ...sorted . map ( ( p ) => p . y ) )
103- const maxY = Math . max ( ...sorted . map ( ( p ) => p . y ) )
104- const xRange = maxX - minX || 1
105- const yRange = maxY - minY || 1
111+ // After flipping: horizontal = y (price/bps), vertical = x (amount)
112+ const minH = Math . min ( ...sorted . map ( ( p ) => p . y ) )
113+ const maxH = Math . max ( ...sorted . map ( ( p ) => p . y ) )
114+ const minV = sorted [ 0 ] . x
115+ const maxV = sorted [ sorted . length - 1 ] . x
116+ const hRange = maxH - minH || 1
117+ const vRange = maxV - minV || 1
106118
107119 const grid : string [ ] [ ] = Array . from ( { length : chartHeight } , ( ) => Array ( chartWidth ) . fill ( " " ) )
108120 const plotted : Array < { col : number ; row: number } > = [ ]
109121
110122 for ( const p of sorted ) {
111- const col = Math . round ( ( ( p . x - minX ) / xRange ) * ( chartWidth - 1 ) )
112- const row = Math . round ( ( 1 - ( p . y - minY ) / yRange ) * ( chartHeight - 1 ) )
123+ const col = Math . round ( ( ( p . y - minH ) / hRange ) * ( chartWidth - 1 ) )
124+ const row = Math . round ( ( 1 - ( p . x - minV ) / vRange ) * ( chartHeight - 1 ) )
113125 grid [ row ] [ col ] = "●"
114126 plotted . push ( { col, row } )
115127 }
116128
117- // Connect consecutive points with dashes
118- for ( let i = 0 ; i < plotted . length - 1 ; i ++ ) {
119- const a = plotted [ i ] ,
120- b = plotted [ i + 1 ]
121- for ( let c = a . col + 1 ; c < b . col ; c ++ ) {
122- const row = Math . round ( a . row + ( ( c - a . col ) / ( b . col - a . col ) ) * ( b . row - a . row ) )
123- if ( grid [ row ] [ c ] === " " ) grid [ row ] [ c ] = "─"
129+ // Connect consecutive points with vertical connectors (sorted by row now)
130+ const byRow = [ ...plotted ] . sort ( ( a , b ) => a . row - b . row )
131+ for ( let i = 0 ; i < byRow . length - 1 ; i ++ ) {
132+ const a = byRow [ i ] ,
133+ b = byRow [ i + 1 ]
134+ for ( let r = a . row + 1 ; r < b . row ; r ++ ) {
135+ const col = Math . round ( a . col + ( ( r - a . row ) / ( b . row - a . row ) ) * ( b . col - a . col ) )
136+ if ( grid [ r ] [ col ] === " " ) grid [ r ] [ col ] = "│"
124137 }
125138 }
126139
127- // Extend last point flat to right edge
128- const last = plotted [ plotted . length - 1 ]
129- for ( let c = last . col + 1 ; c < chartWidth ; c ++ ) {
130- if ( grid [ last . row ] [ c ] === " " ) grid [ last . row ] [ c ] = "─"
140+ // Extend top point flat to left edge (lowest price, highest amount extends left)
141+ const top = byRow [ 0 ]
142+ for ( let c = 0 ; c < top . col ; c ++ ) {
143+ if ( grid [ top . row ] [ c ] === " " ) grid [ top . row ] [ c ] = "─"
131144 }
132145
133- // Y-axis prefix: show max at top row , label at mid, min at bottom row
134- const maxYStr = fmtNum ( maxY ) . padStart ( 4 )
135- const minYStr = fmtNum ( minY ) . padStart ( 4 )
136- const midLabel = ` ${ yLabel . trim ( ) . slice ( 0 , 3 ) . padEnd ( 3 ) } `
146+ // Y-axis prefix: vertical axis = amount ($), show maxV at top, label at mid, minV at bottom
147+ const maxVStr = ( "$" + fmtNum ( maxV ) ) . padStart ( 5 )
148+ const minVStr = ( "$" + fmtNum ( minV ) ) . padStart ( 5 )
149+ const midLabel = ` ${ yLabel . trim ( ) . slice ( 0 , 4 ) . padEnd ( 4 ) } `
137150 const midRow = Math . floor ( chartHeight / 2 )
138151 const rows : string [ ] = grid . map ( ( row , i ) => {
139152 let prefix : string
140- if ( i === 0 ) prefix = `${ maxYStr } │`
141- else if ( i === chartHeight - 1 ) prefix = minY !== maxY ? `${ minYStr } │` : ` │`
153+ if ( i === 0 ) prefix = `${ maxVStr } │`
154+ else if ( i === chartHeight - 1 ) prefix = minV !== maxV ? `${ minVStr } │` : ` │`
142155 else if ( i === midRow ) prefix = `${ midLabel } │`
143- else prefix = ` │`
156+ else prefix = ` │`
144157 return prefix + row . join ( "" )
145158 } )
146159
147- // X-axis row: embed min/max amounts within the fixed chartWidth region
148- const minXStr = "$" + fmtNum ( minX )
149- const maxXStr = "$" + fmtNum ( maxX )
150- const dashes = chartWidth - minXStr . length - maxXStr . length
151- const axisContent = dashes >= 1 ? minXStr + "─" . repeat ( dashes ) + maxXStr : "─" . repeat ( chartWidth )
152- rows . push ( ` └${ axisContent } ` )
160+ // X-axis row: horizontal axis = price/bps, show min left and max right
161+ const minHStr = fmtNum ( minH )
162+ const maxHStr = fmtNum ( maxH )
163+ const dashes = chartWidth - minHStr . length - maxHStr . length
164+ const axisContent = dashes >= 1 ? minHStr + "─" . repeat ( dashes ) + maxHStr : "─" . repeat ( chartWidth )
165+ rows . push ( ` └${ axisContent } ` )
166+ rows . push ( ` ${ " " . repeat ( Math . floor ( chartWidth / 2 ) - 1 ) } ${ yLabel . trim ( ) } ` )
153167 return rows
154168}
155169
@@ -165,10 +179,15 @@ function getStrategyBanner(config: StrategyConfig): string {
165179 title = "BASIC STRATEGY ACTIVE"
166180 subtitle = "adaptive BPS spread curve"
167181 } else {
168- const points = config . priceCurve . map ( ( p ) => ( { x : parseFloat ( p . amount ) , y : parseFloat ( p . price ) } ) )
169- chartRows = renderCurveAscii ( points , " tok" )
182+ const bidPoints = config . bidPriceCurve . map ( ( p ) => ( { x : parseFloat ( p . amount ) , y : parseFloat ( p . price ) } ) )
183+ const askPoints = config . askPriceCurve . map ( ( p ) => ( { x : parseFloat ( p . amount ) , y : parseFloat ( p . price ) } ) )
184+ chartRows = [
185+ ...renderCurveAscii ( bidPoints , " bid" ) ,
186+ "" ,
187+ ...renderCurveAscii ( askPoints , " ask" ) ,
188+ ]
170189 title = "HYPERFX STRATEGY ACTIVE"
171- subtitle = "adaptive FX price curve routing"
190+ subtitle = "adaptive bid/ask FX price curve routing"
172191 }
173192
174193 // innerWidth = 44 accommodates the longest chart row: " └" + 30×"─" + " order($)" = 44 chars
@@ -370,13 +389,15 @@ program
370389 )
371390 }
372391 case "hyperfx" : {
373- const pricePolicy = new FillerPricePolicy ( { points : strategyConfig . priceCurve } )
392+ const bidPricePolicy = new FillerPricePolicy ( { points : strategyConfig . bidPriceCurve } )
393+ const askPricePolicy = new FillerPricePolicy ( { points : strategyConfig . askPriceCurve } )
374394 return new FXFiller (
375395 privateKey ,
376396 configService ,
377397 chainClientManager ,
378398 contractService ,
379- pricePolicy ,
399+ bidPricePolicy ,
400+ askPricePolicy ,
380401 strategyConfig . maxOrderUsd ,
381402 strategyConfig . exoticTokenAddresses ,
382403 bidStorageService ,
@@ -553,14 +574,25 @@ function validateConfig(config: FillerTomlConfig): void {
553574 }
554575
555576 if ( strategy . type === "hyperfx ") {
556- // Validate price curve
557- if ( ! strategy . priceCurve || ! Array . isArray ( strategy . priceCurve ) || strategy . priceCurve . length < 2 ) {
558- throw new Error ( "FX strategy must have a 'priceCurve ' array with at least 2 points ")
577+ // Validate bid price curve
578+ if ( ! strategy . bidPriceCurve || ! Array . isArray ( strategy . bidPriceCurve ) || strategy . bidPriceCurve . length < 2 ) {
579+ throw new Error ( "FX strategy must have a 'bidPriceCurve ' array with at least 2 points ")
580+ }
581+
582+ for ( const point of strategy . bidPriceCurve ) {
583+ if ( point . amount === undefined || point . price === undefined ) {
584+ throw new Error ( "Each FX bidPriceCurve point must have 'amount ' and 'price '")
585+ }
586+ }
587+
588+ // Validate ask price curve
589+ if ( ! strategy . askPriceCurve || ! Array . isArray ( strategy . askPriceCurve ) || strategy . askPriceCurve . length < 2 ) {
590+ throw new Error ( "FX strategy must have an 'askPriceCurve ' array with at least 2 points ")
559591 }
560592
561- for ( const point of strategy . priceCurve ) {
593+ for ( const point of strategy . askPriceCurve ) {
562594 if ( point . amount === undefined || point . price === undefined ) {
563- throw new Error ( "Each FX price curve point must have 'amount ' and 'price '")
595+ throw new Error ( "Each FX askPriceCurve point must have 'amount ' and 'price '")
564596 }
565597 }
566598
0 commit comments