Page 1 of 1

How commission per lot changes the optimal scalp hold time

Posted: Mon Sep 14, 2026 7:12 pm
by LondonScalper
Math that changed how long I allow a scalp to live.

Commission is fixed per lot. Spread is paid up front. If my average winner needs three minutes to develop but my edge is really a thirty-second liquidity grab, I am financing hope with time while costs are already sunk.

Simple check I run per pair
1. Round-trip cost (spread + commission) in points
2. Median M1 noise over my session window
3. Typical time-to-+1R on my A setups from the last month of clean tickets

If cost is a large fraction of the first R and winners that take longer than X minutes have worse expectancy in the log, I shorten the time stop -- or I stop trading that pair at that size. Holding "because it might run" after the idea's urgency is gone is just turning a scalp into an accidental swing with retail costs.

I do not optimise hold time to the second. I want a boring band: idea pays or fails inside a window that matches how I entered.

Anyone else size hold-time rules off cost structure, or do you keep a fixed clock regardless of pair?¨

Hi LondonScalper,

You just articulated one of the hardest lessons in short-term trading: time is an active position against you when costs are sunk. Financing hope with time turns a structural edge (a fast liquidity sweep) into a coin flip with a negative expectancy.

To answer your question: the best short-term traders absolutely tie their time constraints to the asset's specific cost/volatility structure rather than using a universal fixed clock. A 3-minute hold on a highly liquid, low-spread pair during the London open is fundamentally different from a 3-minute hold on a wider-spread cross pair in the Asian session.

When your edge is a microstructure event (like an order block reaction or a liquidity grab), the "urgency" of the move is the edge itself. If the tape doesn't accelerate immediately, the premise of the trade is dead, even if the price hasn't hit your hard stop yet.

Re: How commission per lot changes the optimal scalp hold time

Posted: Sun Sep 20, 2026 3:08 pm
by PropScalpDesk
Commission fixes the clock on a scalp

This math changed my hold-time discipline too. Spread is paid up front; commission is paid per lot whether the idea needs thirty seconds or three minutes. If my real edge is a short liquidity grab, financing a longer hold is paying rent on hope.

Check I run per pair from Frankfurt:
  • Median time-to-target on winners last month
  • Round-turn cost in points
  • Time stop written before entry — scratch if structure is not working inside that window
When commission is high relative to average win, I either widen the idea quality bar or I stop pretending micro-scalps on that symbol are viable. Extending losers to “get to breakeven” is how costs compound quietly.

I also refuse to “wait for breakeven” past the time stop. Breakeven hunting is often just unpaid holding after costs are sunk. Scratching on time is part of positive expectancy on short holds.

Do you set the time stop as a hard close, or as a management alert you still negotiate when the candle looks dramatic?

Re: How commission per lot changes the optimal scalp hold time

Posted: Thu Sep 24, 2026 7:35 pm
by PTScalper
Here is a Pine Script v5 strategy template that implements your math. It includes a dashboard table that runs your "simple check per pair" (calculating the true cost ratio and M1 noise) and actively enforces a time-stop logic to kill trades that overstay their welcome.

Code: Select all

//@version=5
strategy("Scalp Time & Cost Constraint", overlay=true, margin_long=100, margin_short=100, calc_on_every_tick=true)

// ==============================================================================
// 1. INPUTS
// ==============================================================================
grp_cost = "Cost & Target Structure"
comm_per_lot = input.float(3.0, title="Round-Trip Commission ($/Lot)", group=grp_cost)
point_value  = syminfo.pointvalue
target_1R    = input.int(100, title="Typical 1R Target (in Points)", group=grp_cost)
max_ratio    = input.float(15.0, title="Max Acceptable Cost/1R Ratio (%)", group=grp_cost)

grp_time = "Time Constraints"
max_hold_bars = input.int(3, title="Max Hold Time (Bars)", group=grp_time, tooltip="Kills trade if not at target by this bar count")

// ==============================================================================
// 2. MATH & METRICS (The "Simple Check")
// ==============================================================================
// A. Round-Trip Cost in Points
// syminfo.spread returns spread in ticks. We convert commission to points as well.
spread_pts = syminfo.spread
comm_pts   = (comm_per_lot) / point_value // Simplified conversion for standard lots
total_cost = spread_pts + comm_pts

// B. Cost as a fraction of 1R
cost_ratio = (total_cost / target_1R) * 100

// C. M1 Noise (Using ATR as a proxy for median minute-by-minute noise)
m1_noise = request.security(syminfo.tickerid, "1", ta.atr(14)) / syminfo.mintick

// ==============================================================================
// 3. EXECUTION LOGIC (Demonstration)
// ==============================================================================
// For demonstration, a simple momentum crossover entry
longCond = ta.crossover(ta.sma(close, 5), ta.sma(close, 20))

// Only enter if the cost ratio makes mathematical sense
valid_environment = cost_ratio <= max_ratio

if longCond and strategy.position_size == 0 and valid_environment
    strategy.entry("ScalpL", strategy.long)

// Track time in trade
var int entry_bar = na
if strategy.position_size != 0 and strategy.position_size[1] == 0
    entry_bar := bar_index

bars_held = strategy.position_size != 0 ? (bar_index - entry_bar) : 0
time_stop_hit = bars_held >= max_hold_bars

// Exit Logic
target_price = strategy.position_avg_price + (target_1R * syminfo.mintick)
stop_price   = strategy.position_avg_price - (target_1R * syminfo.mintick) // 1:1 risk

if strategy.position_size > 0
    if time_stop_hit
        strategy.close("ScalpL", comment="Time Stop (Hope Financed)")
    else
        strategy.exit("ExitL", "ScalpL", limit=target_price, stop=stop_price)

// ==============================================================================
// 4. DIAGNOSTIC DASHBOARD
// ==============================================================================
var table panel = table.new(position.top_right, 2, 4, bgcolor=color.new(color.black, 70), border_width=1, border_color=color.gray)

if barstate.islast
    // Headers
    table.cell(panel, 0, 0, "Scalp Viability Matrix", text_color=color.white, text_halign=text.align_left, text_size=size.small)
    
    // Total Cost
    table.cell(panel, 0, 1, "Total Cost (Spread+Comm):", text_color=color.silver, text_halign=text.align_left, text_size=size.small)
    table.cell(panel, 1, 1, str.tostring(total_cost, "#.##") + " pts", text_color=color.white, text_halign=text.align_right, text_size=size.small)
    
    // M1 Noise
    table.cell(panel, 0, 2, "M1 Noise (ATR 14):", text_color=color.silver, text_halign=text.align_left, text_size=size.small)
    table.cell(panel, 1, 2, str.tostring(m1_noise, "#.##") + " pts", text_color=color.white, text_halign=text.align_right, text_size=size.small)
    
    // Cost Ratio
    color ratio_col = cost_ratio > max_ratio ? color.red : color.green
    table.cell(panel, 0, 3, "Cost vs 1R Ratio:", text_color=color.silver, text_halign=text.align_left, text_size=size.small)
    table.cell(panel, 1, 3, str.tostring(cost_ratio, "#.##") + "%", text_color=ratio_col, text_halign=text.align_right, text_size=size.small)

Re: How commission per lot changes the optimal scalp hold time

Posted: Thu Sep 24, 2026 7:36 pm
by PTScalper
Why this approach works:

The Cost/1R Filter: If the current spread widens (common near rollover or high-impact news) and pushes the Cost vs 1R Ratio above your acceptable threshold (e.g., 15%), the script automatically invalidates the setup. You stop trading the pair at that size until costs normalize.

The Time Stop: The script actively counts the bars since the entry. If the setup doesn't pay out within your required window (e.g., 3 bars on an M1 chart), it ruthlessly cuts the trade at the market.

M1 Context: Pulling the M1 ATR dynamically via request.security keeps you grounded. If your 1R is 50 points, but M1 noise is only 5 points, a 3-minute time stop will practically never hit the target—allowing you to adjust your expectations or stay out.

Re: How commission per lot changes the optimal scalp hold time

Posted: Thu Sep 24, 2026 7:37 pm
by PTScalper
Here are the implementations for both MetaTrader 4 (MQL4) and MetaTrader 5 (MQL5).

Because indicators in MetaTrader cannot execute or close orders, both versions are built as trade-management Expert Advisors (EAs). They can run standalone to monitor manually opened tickets or be integrated directly into your existing execution scripts.

Each version:

1.) Calculates live round-trip cost (Spread + $/lot Commission converted into points via tick value).

2.) Measures M1 noise (ATR 14 in points).

3.) Evaluates the Cost vs. 1R ratio.

4.) Projects an on-chart diagnostic dashboard.

5.) Actively tracks open trades and executes a hard market close if elapsed time exceeds your hold threshold.

Re: How commission per lot changes the optimal scalp hold time

Posted: Thu Sep 24, 2026 7:37 pm
by PTScalper
1. MetaTrader 4 (MQL4)

Save this as an Expert Advisor (.mq4) in your MQL4/Experts directory:

Code: Select all

//+------------------------------------------------------------------+
//|                                     ScalpTimeCostManager.mq4     |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property link      ""
#property version   "1.00"
#property strict

//--- Inputs
input string   sep0                 = "=== Cost & Target Structure ===";
input double   InpCommissionPerLot  = 3.0;    // Commission per Lot Round-Trip ($)
input int      InpTarget1R          = 100;    // Typical 1R Target (Points)
input double   InpMaxCostRatio      = 15.0;   // Max Acceptable Cost/1R Ratio (%)

input string   sep1                 = "=== Time Constraints ===";
input int      InpMaxHoldSeconds    = 180;    // Max Hold Time (Seconds: 180 = 3m)
input bool     InpEnforceTimeStop   = true;   // Actively Close Orders on Time Stop
input int      InpMagicNumber       = 0;      // Magic Number to Manage (0 = Manual trades)
input int      InpSlippage          = 5;      // Max Slippage (Points)

//--- Global Variables
int g_atrHandle = -1;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Comment("");
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // 1. Math & Metrics
   double spreadPoints = (double)MarketInfo(_Symbol, MODE_SPREAD);
   
   // Convert $/lot commission into points
   double tickValue = MarketInfo(_Symbol, MODE_TICKVALUE);
   double tickSize  = MarketInfo(_Symbol, MODE_TICKSIZE);
   double pointVal  = (tickSize > 0) ? (tickValue * (_Point / tickSize)) : tickValue;
   
   double commPoints = 0.0;
   if(pointVal > 0)
      commPoints = InpCommissionPerLot / pointVal;
      
   double totalCostPoints = spreadPoints + commPoints;
   double costRatio       = (InpTarget1R > 0) ? (totalCostPoints / (double)InpTarget1R) * 100.0 : 0.0;
   
   // M1 Noise in points (14-period ATR)
   double m1Atr = iATR(_Symbol, PERIOD_M1, 14, 0);
   double m1NoisePoints = (_Point > 0) ? (m1Atr / _Point) : 0.0;

   // 2. On-Chart Diagnostic Dashboard
   string status = (costRatio <= InpMaxCostRatio) ? "VIABLE SETUP" : "COSTS TOO HIGH";
   string dashboard = StringFormat(
      "=== Scalp Viability Matrix [%s] ===\n" +
      "Live Spread:       %.1f pts\n" +
      "Est. Commission:   %.1f pts ($%.2f/lot)\n" +
      "Total Sunk Cost:   %.1f pts\n" +
      "M1 Noise (ATR 14): %.1f pts\n" +
      "Cost / 1R Ratio:   %.2f%% (Max: %.1f%%)\n" +
      "Status:            %s\n" +
      "Time Stop Limit:   %d sec",
      _Symbol, spreadPoints, commPoints, InpCommissionPerLot, 
      totalCostPoints, m1NoisePoints, costRatio, InpMaxCostRatio, 
      status, InpMaxHoldSeconds
   );
   Comment(dashboard);

   // 3. Time Stop Management
   if(InpEnforceTimeStop)
   {
      ManageTimeStops();
   }
}

//+------------------------------------------------------------------+
//| Close trades that exceed hold duration                           |
//+------------------------------------------------------------------+
void ManageTimeStops()
{
   datetime now = TimeCurrent();
   
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
         continue;

      if(OrderSymbol() != _Symbol)
         continue;

      if(InpMagicNumber != -1 && OrderMagicNumber() != InpMagicNumber)
         continue;

      int type = OrderType();
      if(type != OP_BUY && type != OP_SELL)
         continue;

      datetime openTime = OrderOpenTime();
      int elapsedSeconds = (int)(now - openTime);

      if(elapsedSeconds >= InpMaxHoldSeconds)
      {
         double closePrice = (type == OP_BUY) ? Bid : Ask;
         bool closed = OrderClose(OrderTicket(), OrderLots(), closePrice, InpSlippage, clrOrangeRed);
         if(closed)
         {
            PrintFormat("Order #%d closed: Time stop exceeded (%d sec >= %d sec)", 
                        OrderTicket(), elapsedSeconds, InpMaxHoldSeconds);
         }
         else
         {
            PrintFormat("Failed to close order #%d on time stop. Error: %d", 
                        OrderTicket(), GetLastError());
         }
      }
   }
}

Re: How commission per lot changes the optimal scalp hold time

Posted: Thu Sep 24, 2026 7:38 pm
by PTScalper
2. MetaTrader 5 (MQL5)
Save this as an Expert Advisor (.mq5) in your MQL5/Experts directory:

Code: Select all

//+------------------------------------------------------------------+
//|                                     ScalpTimeCostManager.mq5     |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property link      ""
#property version   "1.00"

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>

//--- Inputs
input group "=== Cost & Target Structure ==="
input double   InpCommissionPerLot  = 3.0;    // Commission per Lot Round-Trip ($)
input int      InpTarget1R          = 100;    // Typical 1R Target (Points)
input double   InpMaxCostRatio      = 15.0;   // Max Acceptable Cost/1R Ratio (%)

input group "=== Time Constraints ==="
input int      InpMaxHoldSeconds    = 180;    // Max Hold Time (Seconds: 180 = 3m)
input bool     InpEnforceTimeStop   = true;   // Actively Close Orders on Time Stop
input ulong    InpMagicNumber       = 0;      // Magic Number (0 = Manual trades)
input ulong    InpDeviation         = 5;      // Max Deviation/Slippage (Points)

//--- Global Objects
CTrade         m_trade;
CPositionInfo  m_position;
int            m_atrHandle = INVALID_HANDLE;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   m_trade.SetExpertMagicNumber(InpMagicNumber);
   m_trade.SetDeviationInPoints(InpDeviation);

   m_atrHandle = iATR(_Symbol, PERIOD_M1, 14);
   if(m_atrHandle == INVALID_HANDLE)
   {
      Print("Error initializing M1 ATR handle.");
      return(INIT_FAILED);
   }

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(m_atrHandle != INVALID_HANDLE)
      IndicatorRelease(m_atrHandle);
      
   Comment("");
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // 1. Math & Metrics
   long spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);

   // Point value conversion
   double pointVal = (tickSize > 0.0) ? (tickValue * (point / tickSize)) : tickValue;
   
   double commPoints = 0.0;
   if(pointVal > 0.0)
      commPoints = InpCommissionPerLot / pointVal;

   double totalCostPoints = (double)spread + commPoints;
   double costRatio       = (InpTarget1R > 0) ? (totalCostPoints / (double)InpTarget1R) * 100.0 : 0.0;

   // M1 Noise (ATR 14 in points)
   double atrBuf[];
   ArraySetAsSeries(atrBuf, true);
   double m1NoisePoints = 0.0;
   if(CopyBuffer(m_atrHandle, 0, 0, 1, atrBuf) > 0 && point > 0.0)
   {
      m1NoisePoints = atrBuf[0] / point;
   }

   // 2. On-Chart Diagnostic Dashboard
   string status = (costRatio <= InpMaxCostRatio) ? "VIABLE SETUP" : "COSTS TOO HIGH";
   string dashboard = StringFormat(
      "=== Scalp Viability Matrix [%s] ===\n" +
      "Live Spread:       %d pts\n" +
      "Est. Commission:   %.1f pts ($%.2f/lot)\n" +
      "Total Sunk Cost:   %.1f pts\n" +
      "M1 Noise (ATR 14): %.1f pts\n" +
      "Cost / 1R Ratio:   %.2f%% (Max: %.1f%%)\n" +
      "Status:            %s\n" +
      "Time Stop Limit:   %d sec",
      _Symbol, spread, commPoints, InpCommissionPerLot, 
      totalCostPoints, m1NoisePoints, costRatio, InpMaxCostRatio, 
      status, InpMaxHoldSeconds
   );
   Comment(dashboard);

   // 3. Time Stop Management
   if(InpEnforceTimeStop)
   {
      ManageTimeStops();
   }
}

//+------------------------------------------------------------------+
//| Close positions exceeding max duration                           |
//+------------------------------------------------------------------+
void ManageTimeStops()
{
   datetime now = TimeCurrent();

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      if(!m_position.SelectByIndex(i))
         continue;

      if(m_position.Symbol() != _Symbol)
         continue;

      if(InpMagicNumber != 0 && m_position.Magic() != InpMagicNumber)
         continue;

      datetime openTime = (datetime)m_position.Time();
      int elapsedSeconds = (int)(now - openTime);

      if(elapsedSeconds >= InpMaxHoldSeconds)
      {
         ulong ticket = m_position.Ticket();
         if(m_trade.PositionClose(ticket))
         {
            PrintFormat("Position #%d closed: Time stop exceeded (%d sec >= %d sec)", 
                        ticket, elapsedSeconds, InpMaxHoldSeconds);
         }
         else
         {
            PrintFormat("Failed to close position #%d on time stop. RetCode: %u", 
                        ticket, m_trade.ResultRetcode());
         }
      }
   }
}