@@ -18,7 +18,7 @@ import type {
1818 WithdrawParams ,
1919 WithdrawResult ,
2020 Funding ,
21- } from '../../../../app/components/UI/Perps/controllers/types' ;
21+ UpdatePositionTPSLParams } from '../../../../app/components/UI/Perps/controllers/types' ;
2222
2323export class PerpsE2EMockService {
2424 private static instance : PerpsE2EMockService ;
@@ -36,6 +36,8 @@ export class PerpsE2EMockService {
3636
3737 private mockPositions : Position [ ] = [ ] ;
3838 private mockOrders : Order [ ] = [ ] ;
39+ // Historical orders (canceled/filled/etc.) for Activity > Orders
40+ private mockOrdersHistory : Order [ ] = [ ] ;
3941 private mockOrderFills : OrderFill [ ] = [ ] ;
4042 private mockPricesMap : Record < string , PriceUpdate > = { } ;
4143 private orderMeta : Record < string , { leverage : number } > = { } ;
@@ -83,49 +85,19 @@ export class PerpsE2EMockService {
8385 if ( profile === 'no-funds' || profile === 'no-positions' ) {
8486 this . mockPositions = [ ] ;
8587 } else if ( profile === 'position-testing' ) {
86- // Rich set of positions for UI validation
88+ // Requested scenario for tests: one BTC long position, open ETH and SOL limit orders
8789 this . mockPositions = [
88- {
89- coin : 'ETH' ,
90- entryPrice : '2500.00' ,
91- size : '1.0' , // long
92- positionValue : '2500.00' ,
93- unrealizedPnl : '125.00' ,
94- marginUsed : '833.33' ,
95- leverage : { type : 'cross' , value : 3 } ,
96- liquidationPrice : '2000.00' ,
97- maxLeverage : 40 ,
98- returnOnEquity : '0.05' , // 5%
99- cumulativeFunding : { allTime : '0' , sinceChange : '0' , sinceOpen : '0' } ,
100- takeProfitCount : 0 ,
101- stopLossCount : 0 ,
102- } ,
10390 {
10491 coin : 'BTC' ,
10592 entryPrice : '45000.00' ,
106- size : '-0.05' , // short
107- positionValue : '2250.00' ,
108- unrealizedPnl : '-50.00' ,
109- marginUsed : '225.00' ,
110- leverage : { type : 'cross' , value : 10 } ,
111- liquidationPrice : '47250.00' ,
112- maxLeverage : 40 ,
113- returnOnEquity : '-0.02' , // -2%
114- cumulativeFunding : { allTime : '0' , sinceChange : '0' , sinceOpen : '0' } ,
115- takeProfitCount : 0 ,
116- stopLossCount : 0 ,
117- } ,
118- {
119- coin : 'SOL' ,
120- entryPrice : '100.00' ,
121- size : '5' , // long
122- positionValue : '500.00' ,
123- unrealizedPnl : '25.00' ,
124- marginUsed : '25.00' ,
125- leverage : { type : 'cross' , value : 20 } ,
126- liquidationPrice : '80.00' ,
93+ size : '0.10' , // long BTC
94+ positionValue : '4500.00' ,
95+ unrealizedPnl : '150.00' ,
96+ marginUsed : '900.00' ,
97+ leverage : { type : 'cross' , value : 5 } ,
98+ liquidationPrice : '36000.00' ,
12799 maxLeverage : 40 ,
128- returnOnEquity : '0.10' , // 10%
100+ returnOnEquity : '0.167' ,
129101 cumulativeFunding : { allTime : '0' , sinceChange : '0' , sinceOpen : '0' } ,
130102 takeProfitCount : 0 ,
131103 stopLossCount : 0 ,
@@ -165,6 +137,7 @@ export class PerpsE2EMockService {
165137 } ;
166138
167139 this . mockOrders = [ ] ;
140+ this . mockOrdersHistory = [ ] ;
168141 this . mockOrderFills = [ ] ;
169142 this . orderIdCounter = 1 ;
170143 this . fillIdCounter = 1 ;
@@ -177,6 +150,57 @@ export class PerpsE2EMockService {
177150
178151 // Initialize price map with default prices
179152 this . mockPricesMap = this . buildDefaultPrices ( ) ;
153+
154+ // Seed default historical/open orders only when not explicitly requesting an empty profile
155+ if ( profile !== 'no-positions' ) {
156+ // 1) Default trade (fill): Opened long BTC with a small fee
157+ const defaultTrade : OrderFill = {
158+ orderId : `seed_fill_${ Date . now ( ) } ` ,
159+ symbol : 'BTC' ,
160+ size : '0.01' ,
161+ price : '45000.00' ,
162+ timestamp : Date . now ( ) - 60 * 60 * 1000 , // 1 hour ago
163+ side : 'buy' ,
164+ fee : '1.25' ,
165+ feeToken : 'USDC' ,
166+ pnl : '0.00' ,
167+ direction : 'Open Long' ,
168+ } ;
169+ this . mockOrderFills . push ( defaultTrade ) ;
170+
171+ // 2) Open orders: ETH and SOL limit longs
172+ const seedNow = Date . now ( ) ;
173+ const ethOrder : Order = {
174+ orderId : `seed_order_eth_${ seedNow } ` ,
175+ symbol : 'ETH' ,
176+ side : 'buy' ,
177+ orderType : 'limit' ,
178+ size : '0.50' ,
179+ originalSize : '0.50' ,
180+ price : '2400.00' ,
181+ filledSize : '0' ,
182+ remainingSize : '0.50' ,
183+ status : 'open' ,
184+ timestamp : seedNow - 30 * 60 * 1000 ,
185+ isTrigger : false ,
186+ } ;
187+ const solOrder : Order = {
188+ orderId : `seed_order_sol_${ seedNow } ` ,
189+ symbol : 'SOL' ,
190+ side : 'buy' ,
191+ orderType : 'limit' ,
192+ size : '10' ,
193+ originalSize : '10' ,
194+ price : '95.00' ,
195+ filledSize : '0' ,
196+ remainingSize : '10' ,
197+ status : 'open' ,
198+ timestamp : seedNow - 25 * 60 * 1000 ,
199+ isTrigger : false ,
200+ } ;
201+ this . mockOrders . push ( ethOrder ) ;
202+ this . mockOrders . push ( solOrder ) ;
203+ }
180204 }
181205
182206 // Mock successful order placement
@@ -258,7 +282,7 @@ export class PerpsE2EMockService {
258282 // Add to mock state
259283 this . mockPositions . push ( mockPosition ) ;
260284
261- // Create mock order fill
285+ // Create mock order fill (market execution)
262286 const mockFill : OrderFill = {
263287 orderId,
264288 symbol : params . coin ,
@@ -269,7 +293,7 @@ export class PerpsE2EMockService {
269293 fee : '2.50' ,
270294 feeToken : 'USDC' ,
271295 pnl : '0.00' ,
272- direction : params . isBuy ? 'long ' : 'short ' ,
296+ direction : params . isBuy ? 'Open Long ' : 'Open Short ' ,
273297 } ;
274298
275299 this . mockOrderFills . push ( mockFill ) ;
@@ -322,7 +346,9 @@ export class PerpsE2EMockService {
322346 * Mock deposit in USD into the perps trading account.
323347 * Increases both availableBalance and totalBalance by the provided fiat amount.
324348 */
325- public async mockDepositUSD ( amountFiat : string ) : Promise < { success : boolean } > {
349+ public async mockDepositUSD (
350+ amountFiat : string ,
351+ ) : Promise < { success : boolean } > {
326352 const delta = parseFloat ( amountFiat || '0' ) ;
327353 if ( ! Number . isFinite ( delta ) || delta <= 0 ) {
328354 return { success : false } ;
@@ -383,13 +409,16 @@ export class PerpsE2EMockService {
383409 orderId : `mock_close_${ this . orderIdCounter } ` ,
384410 symbol : existingPosition . coin ,
385411 size : Math . abs ( parseFloat ( existingPosition . size ) ) . toString ( ) ,
386- price : this . mockPricesMap [ existingPosition . coin ] ?. price || existingPosition . entryPrice ,
412+ price :
413+ this . mockPricesMap [ existingPosition . coin ] ?. price ||
414+ existingPosition . entryPrice ,
387415 timestamp : Date . now ( ) ,
388416 side : parseFloat ( existingPosition . size ) > 0 ? 'sell' : 'buy' ,
389417 fee : '0.00' ,
390418 feeToken : 'USDC' ,
391419 pnl : pnl . toFixed ( 2 ) ,
392- direction : parseFloat ( existingPosition . size ) > 0 ? 'short' : 'long' ,
420+ direction :
421+ parseFloat ( existingPosition . size ) > 0 ? 'Close Long' : 'Close Short' ,
393422 } ;
394423 this . mockOrderFills . push ( closeFill ) ;
395424
@@ -506,6 +535,14 @@ export class PerpsE2EMockService {
506535 return [ ...this . mockOrders ] ;
507536 }
508537
538+ public getMockOrdersHistory ( ) : Order [ ] {
539+ return [ ...this . mockOrdersHistory ] ;
540+ }
541+
542+ public getMockOrdersCombined ( ) : Order [ ] {
543+ return [ ...this . mockOrders , ...this . mockOrdersHistory ] ;
544+ }
545+
509546 public getMockOrderFills ( ) : OrderFill [ ] {
510547 return [ ...this . mockOrderFills ] ;
511548 }
@@ -564,6 +601,98 @@ export class PerpsE2EMockService {
564601 ] ;
565602 }
566603
604+ /** Cancel an open order by id and push a canceled state into historical orders */
605+ public async mockCancelOrder ( orderId : string ) : Promise < OrderResult > {
606+ const idx = this . mockOrders . findIndex ( ( o ) => o . orderId === orderId ) ;
607+ if ( idx === - 1 ) {
608+ return { success : false , error : 'Order not found' } ;
609+ }
610+
611+ const order = { ...this . mockOrders [ idx ] } ;
612+ // Remove from open orders
613+ this . mockOrders . splice ( idx , 1 ) ;
614+
615+ // Record canceled snapshot into orders history for Activity > Orders
616+ const canceledOrder : Order = {
617+ ...order ,
618+ status : 'canceled' ,
619+ lastUpdated : Date . now ( ) ,
620+ } ;
621+ this . mockOrdersHistory . push ( canceledOrder ) ;
622+
623+ // Notify orders subscribers
624+ this . notifyOrdersCallbacks ( ) ;
625+ return { success : true , orderId } ;
626+ }
627+
628+ /**
629+ * Mock update of Take Profit / Stop Loss settings. This creates trigger orders
630+ * so that Activity > Perps → Orders shows TP/SL entries as open orders.
631+ */
632+ public async mockUpdatePositionTPSL (
633+ params : UpdatePositionTPSLParams ,
634+ ) : Promise < OrderResult > {
635+ const position = this . mockPositions . find ( ( p ) => p . coin === params . coin ) ;
636+ if ( ! position ) {
637+ return { success : false , error : 'Position not found' } ;
638+ }
639+
640+ const now = Date . now ( ) ;
641+ // Update position fields
642+ position . takeProfitPrice = params . takeProfitPrice ;
643+ position . stopLossPrice = params . stopLossPrice ;
644+ position . takeProfitCount = params . takeProfitPrice ? 1 : 0 ;
645+ position . stopLossCount = params . stopLossPrice ? 1 : 0 ;
646+
647+ // Create trigger orders for TP/SL as open orders
648+ if ( params . takeProfitPrice ) {
649+ const tpOrder : Order = {
650+ orderId : `tp_${ this . orderIdCounter ++ } ` ,
651+ symbol : params . coin ,
652+ side : parseFloat ( position . size ) > 0 ? 'sell' : 'buy' ,
653+ orderType : 'limit' ,
654+ size : Math . abs ( parseFloat ( position . size ) ) . toString ( ) ,
655+ originalSize : Math . abs ( parseFloat ( position . size ) ) . toString ( ) ,
656+ price : params . takeProfitPrice ,
657+ filledSize : '0' ,
658+ remainingSize : Math . abs ( parseFloat ( position . size ) ) . toString ( ) ,
659+ status : 'open' ,
660+ timestamp : now ,
661+ detailedOrderType : 'Take Profit Limit' ,
662+ isTrigger : true ,
663+ reduceOnly : true ,
664+ } ;
665+ this . mockOrders . push ( tpOrder ) ;
666+ }
667+
668+ if ( params . stopLossPrice ) {
669+ const slOrder : Order = {
670+ orderId : `sl_${ this . orderIdCounter ++ } ` ,
671+ symbol : params . coin ,
672+ side : parseFloat ( position . size ) > 0 ? 'sell' : 'buy' ,
673+ orderType : 'limit' ,
674+ size : Math . abs ( parseFloat ( position . size ) ) . toString ( ) ,
675+ originalSize : Math . abs ( parseFloat ( position . size ) ) . toString ( ) ,
676+ price : params . stopLossPrice ,
677+ filledSize : '0' ,
678+ remainingSize : Math . abs ( parseFloat ( position . size ) ) . toString ( ) ,
679+ status : 'open' ,
680+ timestamp : now ,
681+ detailedOrderType : 'Stop Loss Limit' ,
682+ isTrigger : true ,
683+ reduceOnly : true ,
684+ } ;
685+ this . mockOrders . push ( slOrder ) ;
686+ }
687+
688+ // Notify orders subscribers
689+ this . notifyOrdersCallbacks ( ) ;
690+ // Also notify position subscribers for UI counters
691+ this . notifyPositionCallbacks ( ) ;
692+
693+ return { success : true } ;
694+ }
695+
567696 private calculateMockLiquidationPrice (
568697 entryPrice : number ,
569698 leverage : number ,
0 commit comments