You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There is a stray pm2 token appended to a block comment terminator, which will likely break TypeScript compilation. Remove the unexpected text to restore valid syntax.
/** * Cancel leftover orders for a given exchange, symbol, and strategyKey. */pm2privateasynccancelAllOrders(exchange: ccxt.Exchange,
The volume strategy uses setInterval with an async loop without any re-entrancy/locking, so overlapping executions can occur if one cycle takes longer than the interval, potentially placing/canceling orders concurrently and causing inconsistent state. Consider adding a per-strategy mutex/flag and skipping if a run is already in progress, or using a self-scheduling setTimeout pattern.
constloop=async()=>{if(tradesExecuted>=numTrades){this.logger.log(`Volume strategy [${strategyKey}] completed after ${numTrades} trades. `+`Total PnL: ${totalPnL>=0 ? '+' : ''}${totalPnL.toFixed(8)}`);constinst=this.strategyInstances.get(strategyKey);if(inst?.intervalId)clearInterval(inst.intervalId);this.strategyInstances.delete(strategyKey);awaitthis.strategyInstanceRepository.update({ strategyKey },{status: 'stopped',updatedAt: newDate()},);return;}consttradeNumber=tradesExecuted+1;try{awaitPromise.all([this.cancelAllOrders(ex1,symbol,strategyKey),this.cancelAllOrders(ex2,symbol,strategyKey),]);const[bal1,bal2]=awaitPromise.all([ex1.fetchBalance(),ex2.fetchBalance(),]);const[base,quote]=symbol.split('/');constex1Base=Number(bal1.free[base]??0);constex1Quote=Number(bal1.free[quote]??0);constex2Base=Number(bal2.free[base]??0);constex2Quote=Number(bal2.free[quote]??0);// Use ex1 orderbook as global referenceconstbook1=awaitex1.fetchOrderBook(symbol);constbid1=book1.bids[0]?.[0];constask1=book1.asks[0]?.[0];if(!bid1||!ask1)thrownewError('Empty orderbook on ex1');constglobalMid=(bid1+ask1)/2;constpriceForCapacity=globalMid>0 ? globalMid : (minPrice||1e-12);constmakerSide: 'buy'|'sell'=postOnlySide;consttakerSide: 'buy'|'sell'=makerSide==='buy' ? 'sell' : 'buy';// Capacity if ex1 = maker, ex2 = takerconstcapacity1=Math.min(makerSide==='buy' ? ex1Quote/priceForCapacity : ex1Base,makerSide==='buy' ? ex2Base : ex2Quote/priceForCapacity,);// Capacity if ex2 = maker, ex1 = takerconstcapacity2=Math.min(makerSide==='buy' ? ex2Quote/priceForCapacity : ex2Base,makerSide==='buy' ? ex1Base : ex1Quote/priceForCapacity,);letmakerEx=capacity1>=capacity2 ? ex1 : ex2;lettakerEx=capacity1>=capacity2 ? ex2 : ex1;letmaxCapacity=capacity1>=capacity2 ? capacity1 : capacity2;if(!maxCapacity||maxCapacity<=0||maxCapacity<minAmt){this.logger.warn(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: insufficient combined balance. `+`ex1Base=${ex1Base.toFixed(8)} ex1Quote=${ex1Quote.toFixed(8)} ex2Base=${ex2Base.toFixed(8,)} ex2Quote=${ex2Quote.toFixed(8)}`,);return;}// Maker orderbook (reuse ex1 book if makerEx is ex1)constmakerBook=makerEx===ex1 ? book1 : awaitmakerEx.fetchOrderBook(symbol);constmakerBid=makerBook.bids[0]?.[0];constmakerAsk=makerBook.asks[0]?.[0];if(!makerBid||!makerAsk)thrownewError(`Empty orderbook on maker exchange ${makerEx.id}`);constmid=(makerBid+makerAsk)/2;constmakerRawPrice=Math.max(mid,minPrice||1e-12);constmakerPrice=priceToPrec(makerRawPrice);letrawAmt=Math.min(baseTradeAmount,maxCapacity)*0.99;letamount=amtToPrec(rawAmt);if(!amount||amount<=0||amount<minAmt){this.logger.warn(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: computed amount too small. `+`amount=${amount} minAmt=${minAmt} maxCapacity=${maxCapacity}`,);return;}// Helper to recompute capacity after switching maker/takerconstcomputeCapacity=(me: ccxt.Exchange,te: ccxt.Exchange)=>{constbMaker=me===ex1 ? bal1 : bal2;constbTaker=te===ex1 ? bal1 : bal2;constmBase=Number(bMaker.free[base]??0);constmQuote=Number(bMaker.free[quote]??0);consttBase=Number(bTaker.free[base]??0);consttQuote=Number(bTaker.free[quote]??0);constmMax=makerSide==='buy' ? mQuote/priceForCapacity : mBase;consttMax=makerSide==='buy' ? tBase : tQuote/priceForCapacity;returnMath.min(mMax,tMax);};letmakerOrder: any;lettakerOrder: any;letlastTakerPrice: number|undefined;// -----------------------// MAKER ORDER (with fallback)// -----------------------try{makerOrder=awaitmakerEx.createOrder(symbol,'limit',makerSide,amount,makerPrice,{postOnly: true},);this.logger.log(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: `+`Maker order placed: ${makerOrder.id}`);}catch(e: any){this.logger.warn(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: maker failed on ${makerEx.id}: ${e.message}. `+`Switching maker/taker and recomputing amount.`,);// Swap rolesconstnewMaker=makerEx===ex1 ? ex2 : ex1;constnewTaker=makerEx===ex1 ? ex1 : ex2;makerEx=newMaker;takerEx=newTaker;constcap=computeCapacity(makerEx,takerEx);if(!cap||cap<=0||cap<minAmt){thrownewError(`Alternate maker/taker insufficient balance for Trade ${tradeNumber} / ${numTrades}`,);}constnewAmt=amtToPrec(Math.min(baseTradeAmount,cap));if(!newAmt||newAmt<=0||newAmt<minAmt){thrownewError(`Alternate maker amount below minAmt for Trade ${tradeNumber} / ${numTrades}`,);}amount=newAmt;makerOrder=awaitmakerEx.createOrder(symbol,'limit',makerSide,amount,makerPrice,{postOnly: true},);this.logger.log(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: `+`Alternate maker order placed: ${makerOrder.id}`);}// -----------------------// 30ms SAFETY DELAY// -----------------------awaitnewPromise(resolve=>setTimeout(resolve,30));// -----------------------// TAKER ORDER (LIMIT + IOC, with fallback)// -----------------------try{consttakerLimitPrice=makerPrice;lastTakerPrice=takerLimitPrice+(takerLimitPrice*0.0000001);takerOrder=awaittakerEx.createOrder(symbol,'limit',takerSide,amount,takerLimitPrice,{timeInForce: 'IOC'},);this.logger.log(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: `+`Taker order placed: ${takerOrder.id}`);}catch(e: any){this.logger.warn(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: taker failed on ${takerEx.id}: ${e.message}. `+`Switching taker and recomputing amount.`,);constnewTaker=takerEx===ex1 ? ex2 : ex1;takerEx=newTaker;constcap=computeCapacity(makerEx,takerEx);if(!cap||cap<=0||cap<minAmt){try{if(makerOrder?.id)awaitmakerEx.cancelOrder(makerOrder.id,symbol);}catch(_){}thrownewError(`Alternate taker insufficient balance for Trade ${tradeNumber} / ${numTrades}`,);}constnewAmt=amtToPrec(Math.min(baseTradeAmount,cap));if(!newAmt||newAmt<=0||newAmt<minAmt){try{if(makerOrder?.id)awaitmakerEx.cancelOrder(makerOrder.id,symbol);}catch(_){}thrownewError(`Alternate taker amount below minAmt for Trade ${tradeNumber} / ${numTrades}`,);}amount=newAmt;consttakerLimitPrice=makerPrice;lastTakerPrice=takerLimitPrice;takerOrder=awaittakerEx.createOrder(symbol,'limit',takerSide,amount,takerLimitPrice,{timeInForce: 'IOC'},);this.logger.log(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: `+`Alternate taker order placed: ${takerOrder.id}`);}// -----------------------// LOG + FILL STATUS + PNL TRACKING// -----------------------awaitnewPromise(resolve=>setTimeout(resolve,200));// Brief delay for order status updateconst[makerRes,takerRes]=awaitPromise.all([makerEx.fetchOrder(makerOrder.id,symbol),takerEx.fetchOrder(takerOrder.id,symbol),]);constmakerFilled=makerRes.filled??0;consttakerFilled=takerRes.filled??0;constmakerAvgPrice=makerRes.average??makerPrice;consttakerAvgPrice=takerRes.average??(lastTakerPrice??makerPrice);// Calculate PnLlettradePnL=0;if(makerFilled>0&&takerFilled>0){constfilledAmount=Math.min(makerFilled,takerFilled);if(makerSide==='buy'){// We bought at makerAvgPrice and sold at takerAvgPricetradePnL=(takerAvgPrice-makerAvgPrice)*filledAmount;}else{// We sold at makerAvgPrice and bought at takerAvgPricetradePnL=(makerAvgPrice-takerAvgPrice)*filledAmount;}totalPnL+=tradePnL;}this.logger.log(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: `+`Maker ${makerSide.toUpperCase()}${amount} @ ${makerPrice} on ${makerEx.id} `+`status=${makerRes.status} filled=${makerFilled}/${amount} avgPrice=${makerAvgPrice}`,);this.logger.log(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: `+`Taker ${takerSide.toUpperCase()}${amount} @ ${lastTakerPrice??'N/A'} on ${takerEx.id} `+`status=${takerRes.status} filled=${takerFilled}/${amount} avgPrice=${takerAvgPrice}`,);this.logger.log(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: `+`PnL: ${tradePnL>=0 ? '+' : ''}${tradePnL.toFixed(8)}${quote} | `+`Cumulative: ${totalPnL>=0 ? '+' : ''}${totalPnL.toFixed(8)}${quote}`);tradesExecuted++;}catch(err: any){this.logger.error(`[${strategyKey}] Trade ${tradeNumber} / ${numTrades}: Error in trade cycle: ${err.message}`,);}};constintervalId=setInterval(loop,Math.max(baseIntervalTime,1)*1000);this.strategyInstances.set(strategyKey,{isRunning: true, intervalId });this.logger.log(`Volume strategy [${strategyKey}] started on ${exchangeName} for ${symbol}. `+`postOnlySide=${postOnlySide} numTrades=${numTrades}`,);
The test reads the entered amount via textContent() on what appears to be an input (total_input), which typically requires inputValue() (or reading the value attribute). This can make the assertion unreliable and cause intermittent failures depending on DOM structure.
/**
* Cancel leftover orders for a given exchange, symbol, and strategyKey.
- */pm2 + */
private async cancelAllOrders(
exchange: ccxt.Exchange,
pair: string,
strategyKey: string,
) {
Suggestion importance[1-10]: 10
__
Why: The stray pm2 after the block comment (*/pm2) is invalid TypeScript and will break compilation of cancelAllOrders. Removing it is a clear, high-impact correctness fix.
High
Prevent overlapping trade cycles
setInterval will trigger a new async loop() even if the previous one is still running, causing overlapping cancels/orders and inconsistent tradesExecuted/PnL. Add an inFlight guard (or switch to recursive setTimeout) to ensure only one cycle runs at a time.
Why: Using setInterval with an asyncloop() can overlap executions if a cycle takes longer than the interval, which can cause duplicated cancels/orders and inconsistent tradesExecuted/totalPnL. An inFlight guard (or switching to recursive setTimeout) meaningfully improves runtime correctness.
Medium
Fix limit slippage direction
The slippage direction is inverted for a limit “execution” style order: a buy should place higher (more aggressive) and a sell should place lower, otherwise orders may not fill and the strategy appears “stuck”. Flip the multipliers so the limit price moves toward execution, not away from it.
const bps = params.slippageBps ?? 10;
const entryPriceRaw =
- side === 'buy' ? last * (1 - bps / 10_000) : last * (1 + bps / 10_000);+ side === 'buy' ? last * (1 + bps / 10_000) : last * (1 - bps / 10_000);
const entryPrice = pricePrec(entryPriceRaw);
Suggestion importance[1-10]: 6
__
Why: Current pricing makes buy limits less aggressive (last * (1 - bps)), and sell limits more aggressive (last * (1 + bps)), which can lead to non-filling orders when the intent is “execute with slippage”. Flipping the multipliers is a reasonable functional fix given executeLimitTrade() is used for entry.
Low
Use correct exchange precision
Precision is being derived from ex1 even when makerEx/takerEx is ex2, which can produce invalid price/amount formats and failed orders on the active exchange. Compute precision using the exchange you actually place the order on (especially for makerPrice and amount after role swaps).
Why: The suggestion is generally sound, but here ex1 and ex2 are both initialized for the same exchangeName, so priceToPrecision/amountToPrecision should be identical across accounts and not a likely failure source. Still, using makerEx for precision would make the code more robust if the logic ever expands to different exchanges.
Low
Security
Avoid storing private keys
Persisting dto stores signerPk1/signerPk2 in the database, which is a severe credential leak. Save a sanitized copy of parameters (omit or mask private keys) while still using the original dto in-memory for execution.
Why: Saving parameters: dto persists signerPk1/signerPk2 to the DB, which is a severe secret/credential leak. Persisting a sanitized object (omitting private keys) materially improves security without affecting runtime execution.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Update multiple strategy
Multiple Fixes.