@@ -151,6 +151,7 @@ interface RealizedPnl {
151151 amount: number ;
152152 pnl: number ;
153153 pnlPct: number ;
154+ effPrice: number ;
154155 marketFeeEntry: number ;
155156 marketFeeExit: number ;
156157 feeBts: number ;
@@ -441,8 +442,8 @@ function classifyFills(fills: FillRecord[], filterAsset: string | null): { trade
441442 let marketFeeAsset : string ;
442443
443444 const feePrec = getPrec ( f . fee . asset_id ) ;
444- const feeOk = feePrec !== undefined && ! isNaN ( f . fee . amount ) ;
445- const feeReal = feeOk ? f . fee . amount / Math . pow ( 10 , feePrec ) : 0 ;
445+ if ( feePrec === undefined || isNaN ( f . fee . amount ) ) { skipped ++ ; continue ; }
446+ const feeReal = f . fee . amount / Math . pow ( 10 , feePrec ) ;
446447
447448 if ( pAsset === BTS_ID && rAsset !== BTS_ID ) {
448449 direction = 'buy ';
@@ -527,29 +528,6 @@ function analyzePair(trades: TradeFill[], matchMode: 'fifo' | 'sequential' = 'se
527528 const totalBuyQuote = buys . reduce ( ( s , t ) => s + t . quoteAmount , 0 ) ;
528529 const totalSellQuote = sells . reduce ( ( s , t ) => s + t . quoteAmount , 0 ) ;
529530
530- /**
531- * Compute the effective price net of market fees.
532- *
533- * Per BitShares core (db_market.cpp fill_limit_order):
534- * order_receives = receives - pay_market_fees(seller, receives_asset, receives, is_maker)
535- *
536- * The `fee` field in fill_order_operation is the market fee deducted from receives.
537- * For buys (pays=quote, receives=base): fee is in base → effective buy price = quote / (base - fee)
538- * For sells (pays=base, receives=quote): fee is in quote → effective sell price = (quote - fee) / base
539- */
540- function effectiveTradePrice ( t : TradeFill ) : number {
541- if ( t . marketFeeReal <= 0 ) return t . price ;
542- if ( t . direction === 'buy' ) {
543- const netBase = t . baseAmount - t . marketFeeReal ;
544- if ( netBase <= 0 ) return t . price ;
545- return t . quoteAmount / netBase ;
546- } else {
547- const netQuote = t . quoteAmount - t . marketFeeReal ;
548- if ( netQuote <= 0 ) return t . price ;
549- return netQuote / t . baseAmount ;
550- }
551- }
552-
553531 // Merge all trades chronologically.
554532 // FIFO: buys add to queue, sells consume oldest lots (queue front).
555533 // Sequential: buys add to queue, sells consume newest lots (queue back / LIFO).
@@ -558,17 +536,18 @@ function analyzePair(trades: TradeFill[], matchMode: 'fifo' | 'sequential' = 'se
558536 const realizedPnls : RealizedPnl [ ] = [ ] ;
559537 let unmatchedSellBase = 0 ;
560538
561- // First pass: match with gross prices to get base PnL, then with effective
562- // prices to get market-fee-adjusted PnL.
563539 for ( const trade of all ) {
564540 const grossPrice = trade . price ;
565- const effPrice = effectiveTradePrice ( trade ) ;
566541
567542 if ( trade . direction === 'buy' ) {
543+ // Enter lot with net amount (gross receives minus market fee on receives)
544+ const lotAmount = trade . baseAmount - trade . marketFeeReal ;
545+ if ( lotAmount < 1e-12 ) continue ;
546+ const lotEffPrice = trade . quoteAmount / lotAmount ;
568547 inventory . push ( {
569- amount : trade . baseAmount ,
548+ amount : lotAmount ,
570549 grossPrice : grossPrice ,
571- effPrice : effPrice ,
550+ effPrice : lotEffPrice ,
572551 time : trade . time ,
573552 entryOrderId : trade . orderId ,
574553 entryIsMaker : trade . isMaker ,
@@ -591,6 +570,7 @@ function analyzePair(trades: TradeFill[], matchMode: 'fifo' | 'sequential' = 'se
591570 amount : matched ,
592571 pnl : grossPnl ,
593572 pnlPct : grossPnlPct ,
573+ effPrice : lot . effPrice ,
594574 marketFeeEntry : 0 ,
595575 marketFeeExit : 0 ,
596576 feeBts : 0 ,
@@ -625,7 +605,7 @@ function analyzePair(trades: TradeFill[], matchMode: 'fifo' | 'sequential' = 'se
625605 // ─── Market fee allocation (aggregated per order) ──────────────────────
626606 // A limit order may be filled across multiple fill_order_operations with
627607 // the same orderId. Aggregate all fills' market fees per order, then
628- // allocate pro-rata using the fill's total acquired amount as denominator.
608+ // allocate pro-rata using the fill's net acquired amount as denominator.
629609 // (Market fee is paid on acquisition; the portion tied to unsold inventory
630610 // is not yet realised.)
631611 const entryOrderFees = new Map < string , { feeInQuote : number ; totalAcquired : number } > ( ) ;
@@ -638,7 +618,7 @@ function analyzePair(trades: TradeFill[], matchMode: 'fifo' | 'sequential' = 'se
638618 const netBase = t . baseAmount - t . marketFeeReal ;
639619 e . feeInQuote += netBase > 0 ? t . marketFeeReal * ( t . quoteAmount / netBase ) : 0 ;
640620 }
641- e . totalAcquired += t . baseAmount ;
621+ e . totalAcquired += t . baseAmount - t . marketFeeReal ;
642622 entryOrderFees . set ( t . orderId , e ) ;
643623 }
644624
@@ -679,15 +659,15 @@ function analyzePair(trades: TradeFill[], matchMode: 'fifo' | 'sequential' = 'se
679659 const xTotal = exitTotalMatched . get ( r . exitOrderId ) || 1 ;
680660 r . feeBts = BLOCKCHAIN_FEE_PER_FILL * ( r . amount / eTotal ) + BLOCKCHAIN_FEE_PER_FILL * ( r . amount / xTotal ) ;
681661 r . pnlNet -= r . feeBts ;
682- // Use effective cost basis (gross cost + entry market fee)
683- const costBasis = r . buyPrice * r . amount + r . marketFeeEntry ;
662+ const costBasis = r . effPrice * r . amount ;
684663 r . pnlNetPct = costBasis > 0 ? ( r . pnlNet / costBasis ) * 100 : 0 ;
685664 }
686665
666+ const totalBuyBaseNet = buys . reduce ( ( s , t ) => s + t . baseAmount - t . marketFeeReal , 0 ) ;
687667 const totalRealizedPnl = realizedPnls . reduce ( ( s , r ) => s + r . pnl , 0 ) ;
688668 const totalMarketFees = realizedPnls . reduce ( ( s , r ) => s + r . marketFeeEntry + r . marketFeeExit , 0 ) ;
689669 const totalRealizedPnlNet = realizedPnls . reduce ( ( s , r ) => s + r . pnlNet , 0 ) ;
690- const netPosition = totalBuyBase - totalSellBase ;
670+ const netPosition = totalBuyBaseNet - totalSellBase ;
691671 const baseAsset = trades [ 0 ] ?. baseAsset ?? '' ;
692672 const quoteAsset = trades [ 0 ] ?. quoteAsset ?? '' ;
693673
@@ -789,10 +769,13 @@ function printSummary(pairs: PairAnalysis[], accountId: string, start: string, e
789769 }
790770 }
791771 console . log ( ` Note: PnL uses gross prices. Net PnL deducts market fees (charged by` ) ;
792- console . log ( ` asset issuer on receives) and blockchain operation fees (BTS` ) ;
793- console . log ( ` per limit_order_create). Market fees are converted to quote asset.` ) ;
794- console . log ( ` If inventory predates the window or crosses asset pairs,` ) ;
795- console . log ( ` the matched lots may not reflect true trade economics.` ) ;
772+ console . log ( ` asset issuer on receives, both issuer and network portions) and` ) ;
773+ console . log ( ` blockchain operation fees (BTS per limit_order_create). Market` ) ;
774+ console . log ( ` fees are converted to quote asset. Buy lots are entered at net` ) ;
775+ console . log ( ` receives (gross minus buy-side market fee) so inventory matching` ) ;
776+ console . log ( ` reflects what the account actually held. If inventory predates the` ) ;
777+ console . log ( ` window or crosses asset pairs, the matched lots may not reflect` ) ;
778+ console . log ( ` true trade economics.` ) ;
796779 console . log ( '' ) ;
797780}
798781
@@ -805,14 +788,15 @@ function printPnlDetail(pairs: PairAnalysis[]) {
805788 console . log ( ` ── ${ pairLabel } — Realized PnL Detail` ) ;
806789 console . log ( '' ) ;
807790
808- const hdr = ' # Buy Price Sell Price Amount PnL PnL% MktFee OpFeeBTS Net PnL Net% Legs Entry Time Exit Time' ;
791+ const hdr = ' # Buy Price EffBuy Sell Price Amount PnL PnL% MktFee OpFeeBTS Net PnL Net% Legs Entry Time Exit Time' ;
809792 console . log ( hdr ) ;
810793 console . log ( ' ' + '─' . repeat ( hdr . length - 1 ) ) ;
811794
812795 for ( let i = 0 ; i < pair . realizedPnls . length ; i ++ ) {
813796 const r = pair . realizedPnls [ i ] ;
814797 const idx = String ( i + 1 ) . padStart ( 2 ) ;
815798 const bp = fmt ( r . buyPrice , 8 ) . padStart ( 11 ) ;
799+ const ep = fmt ( r . effPrice , 8 ) . padStart ( 11 ) ;
816800 const sp = fmt ( r . sellPrice , 8 ) . padStart ( 11 ) ;
817801 const amt = fmt ( r . amount , 4 ) . padStart ( 9 ) ;
818802 const pnlStr = fmt ( r . pnl , 6 ) . padStart ( 9 ) ;
@@ -824,7 +808,7 @@ function printPnlDetail(pairs: PairAnalysis[]) {
824808 const mk = ( r . entryIsMaker ? 'M' : 'T' ) + '/' + ( r . exitIsMaker ? 'M' : 'T' ) + ' ' ;
825809 const et = ( r . entryTime || '' ) . slice ( 0 , 22 ) . padEnd ( 22 ) ;
826810 const xt = ( r . exitTime || '' ) . slice ( 0 , 22 ) . padEnd ( 22 ) ;
827- console . log ( ` ${ idx } ${ bp } ${ sp } ${ amt } ${ pnlStr } ${ pctStr } ${ mktFeeStr } ${ feeStr } ${ netStr } ${ netPctStr } ${ mk } ${ et } ${ xt } ` ) ;
811+ console . log ( ` ${ idx } ${ bp } ${ ep } ${ sp } ${ amt } ${ pnlStr } ${ pctStr } ${ mktFeeStr } ${ feeStr } ${ netStr } ${ netPctStr } ${ mk } ${ et } ${ xt } ` ) ;
828812 }
829813 console . log ( '' ) ;
830814 }
0 commit comments