@@ -1002,6 +1002,33 @@ describe('TradingService', () => {
10021002 expect ( mockDeps . metrics . trackPerpsEvent ) . toHaveBeenCalled ( ) ;
10031003 } ) ;
10041004
1005+ it ( 'logs error when provider returns a failure result without throwing' , async ( ) => {
1006+ const cancelParams : CancelOrderParams = {
1007+ orderId : 'order-123' ,
1008+ symbol : 'BTC' ,
1009+ } ;
1010+ mockProvider . cancelOrder . mockResolvedValue ( {
1011+ success : false ,
1012+ error : 'Order already filled' ,
1013+ } ) ;
1014+
1015+ const result = await tradingService . cancelOrder ( {
1016+ provider : mockProvider ,
1017+ params : cancelParams ,
1018+ context : mockContext ,
1019+ } ) ;
1020+
1021+ expect ( result . success ) . toBe ( false ) ;
1022+ expect ( mockDeps . logger . error ) . toHaveBeenCalledWith (
1023+ expect . objectContaining ( { message : 'Order already filled' } ) ,
1024+ expect . objectContaining ( {
1025+ controller : 'TradingService' ,
1026+ method : 'cancelOrder' ,
1027+ symbol : 'BTC' ,
1028+ } ) ,
1029+ ) ;
1030+ } ) ;
1031+
10051032 it ( 'handles provider exception during order cancel' , async ( ) => {
10061033 const cancelParams : CancelOrderParams = {
10071034 orderId : 'order-123' ,
@@ -1258,6 +1285,80 @@ describe('TradingService', () => {
12581285 expect ( result . results ) . toHaveLength ( 2 ) ;
12591286 expect ( mockProvider . cancelOrder ) . toHaveBeenCalledTimes ( 2 ) ;
12601287 } ) ;
1288+
1289+ it ( 'logs batch error when provider.cancelOrders returns partial/full failure' , async ( ) => {
1290+ const params : CancelOrdersParams = { cancelAll : true } ;
1291+ mockGetOpenOrders . mockResolvedValue ( mockOrders ) ;
1292+ mockWithStreamPause . mockImplementation (
1293+ async ( callback ) => await callback ( ) ,
1294+ ) ;
1295+ ( mockProvider . cancelOrders as jest . Mock ) . mockResolvedValue ( {
1296+ success : false ,
1297+ successCount : 0 ,
1298+ failureCount : 2 ,
1299+ results : [
1300+ {
1301+ orderId : 'order-1' ,
1302+ symbol : 'BTC' ,
1303+ success : false ,
1304+ error : 'rate limit' ,
1305+ } ,
1306+ {
1307+ orderId : 'order-2' ,
1308+ symbol : 'ETH' ,
1309+ success : false ,
1310+ error : 'not found' ,
1311+ } ,
1312+ ] ,
1313+ } ) ;
1314+
1315+ const result = await tradingService . cancelOrders ( {
1316+ provider : mockProvider ,
1317+ params,
1318+ context : { ...mockContext , getOpenOrders : mockGetOpenOrders } ,
1319+ withStreamPause : mockWithStreamPause ,
1320+ } ) ;
1321+
1322+ expect ( result . success ) . toBe ( false ) ;
1323+ expect ( mockDeps . logger . error ) . toHaveBeenCalledWith (
1324+ expect . objectContaining ( {
1325+ message : expect . stringContaining (
1326+ 'cancelOrders batch failure: 2/2 failed' ,
1327+ ) ,
1328+ } ) ,
1329+ expect . objectContaining ( {
1330+ controller : 'TradingService' ,
1331+ method : 'cancelOrders' ,
1332+ } ) ,
1333+ ) ;
1334+ } ) ;
1335+
1336+ it ( 'does NOT log batch error when using fallback path (provider.cancelOrders undefined)' , async ( ) => {
1337+ const params : CancelOrdersParams = { cancelAll : true } ;
1338+ mockGetOpenOrders . mockResolvedValue ( mockOrders ) ;
1339+ mockWithStreamPause . mockImplementation (
1340+ async ( callback ) => await callback ( ) ,
1341+ ) ;
1342+ delete mockProvider . cancelOrders ;
1343+ mockProvider . cancelOrder . mockResolvedValue ( { success : true } ) ;
1344+
1345+ await tradingService . cancelOrders ( {
1346+ provider : mockProvider ,
1347+ params,
1348+ context : { ...mockContext , getOpenOrders : mockGetOpenOrders } ,
1349+ withStreamPause : mockWithStreamPause ,
1350+ } ) ;
1351+
1352+ // Batch-level log must not fire; individual leaf logs cover per-order failures
1353+ const batchErrorCalls = (
1354+ mockDeps . logger . error as jest . Mock
1355+ ) . mock . calls . filter (
1356+ ( [ err ] : [ Error ] ) =>
1357+ err instanceof Error &&
1358+ err . message . includes ( 'cancelOrders batch failure' ) ,
1359+ ) ;
1360+ expect ( batchErrorCalls ) . toHaveLength ( 0 ) ;
1361+ } ) ;
12611362 } ) ;
12621363
12631364 describe ( 'closePosition' , ( ) => {
@@ -1462,6 +1563,37 @@ describe('TradingService', () => {
14621563 } ) ,
14631564 ) ;
14641565 } ) ;
1566+
1567+ it ( 'logs error when provider returns a failure result without throwing' , async ( ) => {
1568+ const params : ClosePositionParams = { symbol : 'BTC' } ;
1569+ const mockFailureResult : OrderResult = {
1570+ success : false ,
1571+ error : 'Insufficient liquidity' ,
1572+ } ;
1573+
1574+ mockGetPositions . mockResolvedValue ( [ mockPosition ] ) ;
1575+ mockProvider . closePosition . mockResolvedValue ( mockFailureResult ) ;
1576+ mockRewardsIntegrationService . calculateUserFeeDiscount . mockResolvedValue (
1577+ undefined ,
1578+ ) ;
1579+
1580+ const result = await tradingService . closePosition ( {
1581+ provider : mockProvider ,
1582+ params,
1583+ context : { ...mockContext , getPositions : mockGetPositions } ,
1584+ reportOrderToDataLake : mockReportOrderToDataLake ,
1585+ } ) ;
1586+
1587+ expect ( result ) . toEqual ( mockFailureResult ) ;
1588+ expect ( mockDeps . logger . error ) . toHaveBeenCalledWith (
1589+ expect . objectContaining ( { message : 'Insufficient liquidity' } ) ,
1590+ expect . objectContaining ( {
1591+ controller : 'TradingService' ,
1592+ method : 'closePosition' ,
1593+ symbol : 'BTC' ,
1594+ } ) ,
1595+ ) ;
1596+ } ) ;
14651597 } ) ;
14661598
14671599 describe ( 'closePositions' , ( ) => {
@@ -1623,6 +1755,70 @@ describe('TradingService', () => {
16231755 expect ( result . results ) . toHaveLength ( 1 ) ;
16241756 expect ( mockProvider . closePosition ) . toHaveBeenCalledTimes ( 1 ) ;
16251757 } ) ;
1758+
1759+ it ( 'logs batch error when provider.closePositions returns partial/full failure' , async ( ) => {
1760+ const params : ClosePositionsParams = { closeAll : true } ;
1761+ ( mockProvider . closePositions as jest . Mock ) . mockResolvedValue ( {
1762+ success : false ,
1763+ successCount : 0 ,
1764+ failureCount : 2 ,
1765+ results : [
1766+ { symbol : 'BTC' , success : false , error : 'insufficient liquidity' } ,
1767+ { symbol : 'ETH' , success : false , error : 'min size' } ,
1768+ ] ,
1769+ } ) ;
1770+ mockRewardsIntegrationService . calculateUserFeeDiscount . mockResolvedValue (
1771+ undefined ,
1772+ ) ;
1773+
1774+ const result = await tradingService . closePositions ( {
1775+ provider : mockProvider ,
1776+ params,
1777+ context : { ...mockContext , getPositions : mockGetPositions } ,
1778+ } ) ;
1779+
1780+ expect ( result . success ) . toBe ( false ) ;
1781+ expect ( mockDeps . logger . error ) . toHaveBeenCalledWith (
1782+ expect . objectContaining ( {
1783+ message : expect . stringContaining (
1784+ 'closePositions batch failure: 2/2 failed' ,
1785+ ) ,
1786+ } ) ,
1787+ expect . objectContaining ( {
1788+ controller : 'TradingService' ,
1789+ method : 'closePositions' ,
1790+ } ) ,
1791+ ) ;
1792+ } ) ;
1793+
1794+ it ( 'does NOT log batch error when using fallback path (provider.closePositions undefined)' , async ( ) => {
1795+ const params : ClosePositionsParams = { symbols : [ 'BTC' ] } ;
1796+ mockGetPositions . mockResolvedValue ( mockPositions ) ;
1797+ delete mockProvider . closePositions ;
1798+ mockProvider . closePosition . mockResolvedValue ( {
1799+ success : true ,
1800+ orderId : 'close-1' ,
1801+ } ) ;
1802+ mockRewardsIntegrationService . calculateUserFeeDiscount . mockResolvedValue (
1803+ undefined ,
1804+ ) ;
1805+
1806+ await tradingService . closePositions ( {
1807+ provider : mockProvider ,
1808+ params,
1809+ context : { ...mockContext , getPositions : mockGetPositions } ,
1810+ } ) ;
1811+
1812+ // Batch-level log must not fire; individual leaf logs cover per-position failures
1813+ const batchErrorCalls = (
1814+ mockDeps . logger . error as jest . Mock
1815+ ) . mock . calls . filter (
1816+ ( [ err ] : [ Error ] ) =>
1817+ err instanceof Error &&
1818+ err . message . includes ( 'closePositions batch failure' ) ,
1819+ ) ;
1820+ expect ( batchErrorCalls ) . toHaveLength ( 0 ) ;
1821+ } ) ;
16261822 } ) ;
16271823
16281824 describe ( 'updatePositionTPSL' , ( ) => {
0 commit comments