SL order not place

@dhan @Tradehull_Imran

Please help , my code buy order placed successfully but SL order / Exit order not place what is missing/mistake , in coding

Thanks in advance

below code i run

import pandas as pd
from Dhan_Tradehull import Tradehull
import time
import json
from typing import List, Dict
import pandas as pd
import math
from datetime import datetime, timedelta, time as dt_time
import os
import pdb
import talib
import numpy as np
import pandas_ta as ta
import time

try:
    with open('config.json', 'r') as f:
        config = json.load(f)
    client_id = config['client_id']
    access_token = config['access_token']
except FileNotFoundError:
    print("Error: config.json not found.client_id and access_token.")
    exit()
tsl = Tradehull(client_id, access_token)


try:
    with open('stocks.json', 'r') as f:
        stocks_data = json.load(f)
    stocks = stocks_data['stocks']
    print(f"Successfully loaded {len(stocks)} stocks from stocks.json")
except FileNotFoundError:
    print("Error: stocks.json not found. Please create it with a list of stock symbols.")
    stocks = []

TIMEFRAME_MINUTES = "15"
TIMEFRAME_DHAN_API = str(TIMEFRAME_MINUTES)
CAPITAL_ALLOCATION_PERCENT = 0.50
RISK_REWARD_RATIO = 2.0
ATR_PERIOD = 14
ATR_MULTIPLIER = 1.5
MAX_DAILY_LOSS = -2000
MAX_DAILY_TRADES = 6
#RISK_PER_TRADE_PERCENT = 0.05
MAX_RISK_PER_TRADE_RUPEES = 500
MAX_CONCURRENT_TRADES = 4

def wait_for_next_candle(timeframe_minutes: int):
    """Calculates and waits for the start of the next market candle."""
    now = datetime.now()
    next_candle_minute = (now.minute // timeframe_minutes + 1) * timeframe_minutes
    if next_candle_minute >= 60:
        next_candle_time = now.replace(minute=0, second=5, microsecond=0) + timedelta(hours=1)
    else:
        next_candle_time = now.replace(minute=next_candle_minute, second=5, microsecond=0)

    wait_seconds = (next_candle_time - now).total_seconds()
    print(f"--- Syncing with market... Waiting {int(wait_seconds)} seconds for the next {timeframe_minutes}-min candle at {next_candle_time.strftime('%H:%M:%S')} ---")
    time.sleep(max(0, wait_seconds)) # Ensure wait_seconds is not negative

def get_price_filtered_watchlist(full_watchlist):
    print("--- [DEBUG] Entering get_price_filtered_watchlist ---")
    print("--- Pre-filtering watchlist by price (₹25 - ₹5000) ---")
    filtered_stocks = []
    try:
        print(f"--- [API-SEND] get_ltp_data for {len(full_watchlist)} stocks ---")
        ltp_data = tsl.get_ltp_data(full_watchlist)
        print(f"--- [API-RECV] get_ltp_data received: {ltp_data} ---")
        if not ltp_data:
            print("Could not fetch LTP data for watchlist. Using full list.")
            
            return full_watchlist

        for stock, price in ltp_data.items():
            if 25 < price < 5000:
                filtered_stocks.append(stock)
        print(f"Price filter reduced watchlist from {len(full_watchlist)} to {len(filtered_stocks)} stocks.")
        print("--- [DEBUG] Exiting get_price_filtered_watchlist (success) ---")
        return filtered_stocks
    except Exception as e:
        print(f"--- [DEBUG] ERROR in get_price_filtered_watchlist: {e}. Using full list. ---")
        return full_watchlist


def run_macd_strategy():
    print("--- [DEBUG] Entering run_macd_strategy ---")
    print("--- Starting MACD Crossover Strategy ---")

    print("\n--- Performing Initial Daily Scan ---")
    eligible_stocks = get_price_filtered_watchlist(stocks)
    print("--- Initial Daily Scan Complete. Starting trading loop. ---")
    if not eligible_stocks:
        print("No eligible stocks found after price filtering. The script will wait.")

    kill_switch_activated = False
    trades_today_count = 0
    current_day = datetime.today().date()

    try:
        print("--- [DEBUG] Attempting to fetch initial positions and order book. ---")
        print("--- [API-SEND] get_positions ---")
        positions_df = tsl.get_positions()
        print(f"--- [API-RECV] get_positions received:\n{positions_df.to_string()} ---")
        open_positions = {}
        if positions_df is not None and not positions_df.empty:
            # Filter for open MIS positions, which is what this bot trades.
            mis_positions = positions_df[(positions_df['netQty'] != 0) & (positions_df['productType'] == 'MIS')]
            for index, row in mis_positions.iterrows():
                symbol = row['tradingSymbol']
                qty = int(row['netQty'])
                if symbol in open_positions: continue # Already processed

                print(f"--- [DEBUG] Found existing position for {symbol}. Placing protective SL order. ---")
                try:
                    # Fetch data to calculate SL
                    hist_data_sl = tsl.get_historical_data(tradingsymbol=symbol, exchange='NSE', timeframe=TIMEFRAME_DHAN_API)
                    if hist_data_sl is None or len(hist_data_sl) < 5:
                        print(f"WARNING: Not enough data for {symbol} to set SL. Position is unprotected.")
                        continue
                    
                    # Calculate SL price (same logic as new trades)
                    stop_loss_price = hist_data_sl['low'].tail(5).min() - 0.05

                    # Place the SL order
                    sl_order_params = {
                        'tradingsymbol': symbol, 'exchange': 'NSE', 'quantity': abs(qty),
                        'order_type': 'STOP_LOSS', 'transaction_type': 'SELL', 'trade_type': 'MIS',
                        'price': 0, 'trigger_price': float(stop_loss_price)
                    }
                    print(f"--- [API-SEND] order_placement (SL) for existing position {symbol} with params:\n{sl_order_params} ---")
                    sl_order_id = tsl.order_placement(**sl_order_params)
                    print(f"--- [API-RECV] order_placement (SL) received order ID: {sl_order_id} ---")

                    if sl_order_id:
                        print(f"SUCCESS: Placed protective SL for existing position {symbol} with ID: {sl_order_id}")
                        open_positions[symbol] = {
                            'qty': abs(qty), 'buy_order_id': None, 'sl_order_id': sl_order_id,
                            'sl_price': stop_loss_price, 'direction': 'BUY' if qty > 0 else 'SELL'
                        }
                    else:
                        print(f"CRITICAL WARNING: Failed to place protective SL for {symbol}. Position is unprotected.")

                except Exception as e_sl:
                    print(f"--- [DEBUG] ERROR placing protective SL for {symbol}: {e_sl} ---")

            if open_positions:
                print(f"Found existing open positions: {list(open_positions.keys())}")
        print("--- [DEBUG] Successfully processed initial positions. ---")
    except Exception as e:
        print(f"--- [DEBUG] ERROR fetching initial positions: {e} ---")
        open_positions = {}

    while True:
        print("\n--- [DEBUG] Top of the main trading loop. ---")
        try:
            current_time = datetime.now().time()
            print(f"--- [DEBUG] Time check. Current time: {current_time.strftime('%H:%M:%S')} ---")
            if current_time > dt_time(15, 30):
                print("--- [DEBUG] Time is after 15:30. Shutting down. ---")
                print("Market is closed (after 15:30). Stopping strategy for the day.")
                break
            if current_time > dt_time(15, 10):
                print("--- [DEBUG] Time is after 15:10. Cancelling all orders and stopping for the day. ---")
                print("--- [API-SEND] cancel_all_orders ---")
                tsl.cancel_all_orders() 
                print("--- [API-RECV] cancel_all_orders sent. ---")
                tsl.get_live_pnl()
                print (live_pnl)
                break

            if not (dt_time(9, 40) <= current_time <= dt_time(15, 0)):
                print(f"--- [DEBUG] Time {current_time.strftime('%H:%M:%S')} is outside the trading window (09:40 - 15:00). Waiting... ---")
                time.sleep(60)
                continue

            if datetime.today().date() != current_day:
                print("--- [DEBUG] New day detected. ---")
                print("New day detected. Resetting daily limits and kill switch.")
                current_day = datetime.today().date()
                kill_switch_activated = False
                trades_today_count = 0

                print("\n--- Performing Initial Daily Scan for new day---")
                eligible_stocks = get_price_filtered_watchlist(stocks)
                print("--- Initial Daily Scan Complete. Starting trading loop. ---")

            if kill_switch_activated:
                print("--- [DEBUG] Kill switch is active, skipping trade logic. ---")
                print(f"Kill switch is active. No new trades will be placed today. Waiting for next day.")
                time.sleep(60 * 5)
                continue

            print("--- [DEBUG] Checking balance and P&L. ---")
            print("--- [API-SEND] get_balance ---")
            available_balance = tsl.get_balance()
            print(f"--- [API-RECV] get_balance received: {available_balance} ---")
            if not available_balance or available_balance == 0:
                print("Could not fetch balance or balance is zero. Waiting...")
                time.sleep(60)
                continue
            
            risk_from_capital = available_balance
            risk_amount = min(risk_from_capital, MAX_RISK_PER_TRADE_RUPEES)
            
            print("--- [API-SEND] get_live_pnl ---")
            live_pnl = tsl.get_live_pnl()
            print(f"--- [API-RECV] get_live_pnl received: {live_pnl} ---")

            print(f"Current Day P&L: ₹{live_pnl:.2f}")
            if live_pnl is not None and live_pnl < MAX_DAILY_LOSS:
                print("--- [DEBUG] Max daily loss reached. Activating kill switch. ---")
                print(f"!!! MAX DAILY LOSS of ₹{MAX_DAILY_LOSS} REACHED. ACTIVATING KILL SWITCH. !!!")
                print("--- Closing all open positions. ---")
                for symbol in list(open_positions.keys()):
                    print(f"--- Closing position for {symbol} due to kill switch. ---")
                    # First, cancel the pending SL order to avoid it being orphaned
                    trade_info = open_positions.get(symbol)
                    if trade_info and trade_info.get('sl_order_id'):
                        sl_order_id_to_cancel = trade_info['sl_order_id']
                        print(f"--- [API-SEND] cancel_order for SL ID: {sl_order_id_to_cancel} ---")
                        tsl.cancel_order(trade_info['sl_order_id'])
                        print(f"--- [API-RECV] cancel_order sent for {sl_order_id_to_cancel} ---")
                    print(f"--- [API-SEND] exit_position for {symbol} ---")
                    tsl.exit_position(tradingsymbol=symbol, exchange='NSE', trade_type='MIS')
                    print(f"--- [API-RECV] exit_position sent for {symbol} ---")
                    del open_positions[symbol]
                kill_switch_activated = True
                continue

            if open_positions:
                print("--- [DEBUG] Entering 'Manage Open Positions' block. ---")
                print("--- Managing open positions ---")
                for symbol, trade_info in list(open_positions.items()):
                    print(f"--- [API-SEND] get_historical_data for {symbol} (managing position) ---")
                    hist_data_15min = tsl.get_historical_data(tradingsymbol=symbol, exchange='NSE', timeframe=TIMEFRAME_DHAN_API)
                    print(f"--- [API-RECV] get_historical_data for {symbol} received {len(hist_data_15min) if hist_data_15min is not None else 0} rows ---")

                    if hist_data_15min is None or len(hist_data_15min) < 30: continue

                    macd, macdsignal, _ = talib.MACD(hist_data_15min['close'], fastperiod=12, slowperiod=26, signalperiod=9)
                    #atr = talib.ATR(hist_data_15min['high'], hist_data_15min['low'], hist_data_15min['close'], timeperiod=ATR_PERIOD)
                    previous_macd = macd.iloc[-2]
                    latest_macd = macd.iloc[-1]
                    previous_signal = macdsignal.iloc[-2]
                    latest_signal = macdsignal.iloc[-1]

                    # Exit condition: MACD bearish crossover (was bullish, now bearish)
                    if trade_info['direction'] == 'BUY' and previous_macd > previous_signal and latest_macd < latest_signal:
                        print(f"EXIT (MACD Crossover): Closing BUY on {symbol} at {hist_data_15min['close'].iloc[-1]}")
                        # First, cancel the pending SL order to avoid double execution
                        if trade_info.get('sl_order_id'):
                            sl_order_id_to_cancel = trade_info['sl_order_id']
                            print(f"--- [API-SEND] cancel_order for SL ID: {sl_order_id_to_cancel} ---")
                            tsl.cancel_order(sl_order_id_to_cancel)
                            print(f"--- [API-RECV] cancel_order sent for {sl_order_id_to_cancel} ---")
                        
                        # Then, exit the position with a market order
                        print(f"--- [API-SEND] exit_position for {symbol} ---")
                        tsl.exit_position(
                            tradingsymbol=symbol, exchange='NSE', trade_type='MIS'
                        )
                        print(f"--- [API-RECV] exit_position sent for {symbol} ---")
                        del open_positions[symbol]
                        continue
                    
                    # --- TRAILING STOP-LOSS LOGIC ---
                    # if not atr.empty:
                    #     latest_close = hist_data_15min['close'].iloc[-1]
                    #     latest_atr = atr.iloc[-1]
                        
                    #     potential_new_sl = latest_close - (latest_atr * ATR_MULTIPLIER)
                    #     current_sl = trade_info['sl']

                    #     # If new SL is higher than current SL, trail it up
                    #     if potential_new_sl > current_sl:
                    #         new_sl_price = round_to_tick(potential_new_sl)
                    #         print(f"TRAILING SL for {symbol}: From {current_sl:.2f} to {new_sl_price:.2f}")
                    #         tsl.modify_order(order_id=trade_info['orderId'], trigger_price=new_sl_price)
                    #         open_positions[symbol]['sl'] = new_sl_price # Update local SL

            if available_balance <= 0:
                print("Balance is zero or less. Skipping scan for new opportunities.")
                time.sleep(300)
                continue

            print("--- [DEBUG] Entering 'Scan for New Trades' block. ---")
            print(f"\nScanning for new trades at {datetime.now().strftime('%H:%M:%S')}...")

            for stock_symbol in eligible_stocks:
                if trades_today_count >= MAX_DAILY_TRADES:
                    print("Max daily trades limit reached. No more trades today.")
                    print("--- [DEBUG] Max daily trades reached, breaking scan loop. ---")
                    break
                if stock_symbol in open_positions.keys():
                    continue
                
                # Check for concurrent trade limit
                if len(open_positions) >= MAX_CONCURRENT_TRADES:
                    print(f"Max concurrent trades limit of {MAX_CONCURRENT_TRADES} reached. Will not open new positions now.")
                    break

                try:
                    
                    print(f"--- [API-SEND] get_historical_data for {stock_symbol} (scanning) ---")
                    hist_data = tsl.get_historical_data(tradingsymbol=stock_symbol, exchange='NSE', timeframe=TIMEFRAME_DHAN_API)
                    print(f"--- [API-RECV] get_historical_data for {stock_symbol} received {len(hist_data) if hist_data is not None else 0} rows ---")
                    if hist_data is None or len(hist_data) < 30:
                        print(f"Limited DATA  {stock_symbol}")
                        continue

                    macd, macdsignal, macdhist = talib.MACD(hist_data['close'], fastperiod=12, slowperiod=26, signalperiod=9)
                    atr = talib.ATR(hist_data['high'], hist_data['low'], hist_data['close'], timeperiod=ATR_PERIOD)
                    rsi = talib.RSI(hist_data['close'], timeperiod=14)
                    hist_data.ta.supertrend(length=ATR_PERIOD, multiplier=3.0, append=True)
                    
                    latest_close = hist_data['close'].iloc[-1]
                    latest_open = hist_data['open'].iloc[-1]
                    previous_close = hist_data['close'].iloc[-2]
                    previous_high = hist_data['high'].iloc[-2]
                    latest_atr = atr.iloc[-1]
                    latest_rsi = rsi.iloc[-1]
                    if pd.isna(latest_atr):
                        continue
                    if pd.isna(latest_rsi):
                        continue

                    latest_macd = macd.iloc[-1]
                    previous_macd = macd.iloc[-2]
                    latest_signal = macdsignal.iloc[-1]
                    previous_signal = macdsignal.iloc[-2]

                    st_col = f'SUPERT_{ATR_PERIOD}_3.0'
                    latest_supertrend = hist_data[st_col].iloc[-1]
                    previous_supertrend = hist_data[st_col].iloc[-2]
                    two_ago_supertrend = hist_data[st_col].iloc[-3]
                    two_ago_close = hist_data['close'].iloc[-3]

                    # Ensure all required indicator values are not NaN before proceeding
                    if pd.isna(latest_supertrend) or pd.isna(previous_supertrend) or pd.isna(two_ago_supertrend):
                        print(f"Skipping {stock_symbol} due to NaN in Supertrend indicator values.")
                        continue

                    trade_placed = False

                    
                    # Entry condition: MACD Crossover confirmed by RSI, without the high breakout.

                    if previous_macd < previous_signal and latest_macd > latest_signal and latest_rsi >= 56 and latest_close > latest_open:
                        print(f"Signal Found: {stock_symbol} | MACD: {latest_macd:.2f} | RSI: {latest_rsi:.2f} | Close: {latest_close:.2f} | Signal: (Crossover + RSI + Bullish Candle)")
                        trade_placed = True

                    if trade_placed:
                        last_five_lows = hist_data['low'].tail(5)
                        # Set SL just below the lowest low of the last 5 candles for safety
                        stop_loss_price = last_five_lows.min() - 0.05 
                        risk_per_share = latest_close - stop_loss_price
                        if risk_per_share <= 0: continue
                        print(f"--- [DEBUG] Risk Calc for {stock_symbol}: Risk/Share=₹{risk_per_share:.2f} (Entry:{latest_close:.2f} - SL:{stop_loss_price:.2f})")
                        
                        # Calculate quantity based on risk
                        quantity = int(risk_amount / risk_per_share)
                        
                        if quantity == 0:
                            print(f"Skipping {stock_symbol}, not enough capital for 1 share.")
                            continue
                        
                        # Check if available margin is sufficient for the trade
                        required_margin = quantity * latest_close
                        if required_margin > available_balance:
                            print(f"Skipping {stock_symbol} due to low margin. Required: ~₹{required_margin:.2f}, Available: ₹{available_balance:.2f}")
                            continue

                        # Prepare and print the parameters for debugging
                        order_params = {         
                            'tradingsymbol': stock_symbol,
                            'exchange': 'NSE',
                            'quantity': quantity,
                            'price': 0,
                            'trigger_price': 0,
                            'order_type': 'MARKET',
                            'transaction_type': 'BUY',
                            'trade_type': 'MIS'
                        }
                        # Use pandas to print the parameters in a clean table format
                        print("DEBUG: Attempting to place order with parameters:")
                        print(pd.DataFrame.from_dict(order_params, orient='index', columns=['Value']).to_string())

                        print(f"--- [API-SEND] order_placement (BUY) for {stock_symbol} ---")
                        order_id = tsl.order_placement(**order_params)
                        print(f"--- [API-RECV] order_placement (BUY) received order ID: {order_id} ---")

                        if order_id:
                            # --- Verify Order Status ---
                            print(f"--- [DEBUG] Verifying execution status for order ID: {order_id} ---")
                            
                            order_confirmed = False
                            final_order_status = "UNKNOWN"
                            # Retry loop to confirm trade execution (e.g., 5 attempts over 10 seconds)
                            for i in range(5):
                                time.sleep(2)
                                status = tsl.get_order_status(order_id)
                                print(f"--- [API-RECV] Verification attempt {i+1}/5 for order {order_id}. Status: {status} ---")
                                if status and status.lower() == 'traded':
                                    order_confirmed = True
                                    final_order_status = status
                                    break
                                if status and status.lower() in ['rejected', 'cancelled', 'expired']:
                                    final_order_status = status
                                    break

                            if order_confirmed:
                                print(f"SUCCESS: BUY Order for {stock_symbol} confirmed as TRADED.")
                                
                                # --- Place Live Stop-Loss Order ---
                                print(f"--- [DEBUG] Placing SL order for {stock_symbol} at trigger price {stop_loss_price:.2f} ---")
                                sl_order_params = {
                                    'tradingsymbol': stock_symbol, 'exchange': 'NSE', 'quantity': quantity,
                                    'order_type': 'STOP_LOSS', 'transaction_type': 'SELL', 'trade_type': 'MIS',
                                    'price': 0, # 0 for SL-Market order
                                    'trigger_price': stop_loss_price
                                }
                                print(f"--- [API-SEND] order_placement (SL) for {stock_symbol} with params:\n{sl_order_params} ---") # Corrected print statement
                                sl_order_id = tsl.order_placement(**sl_order_params)
                                print(f"--- [API-RECV] order_placement (SL) received order ID: {sl_order_id} ---")

                                if sl_order_id:
                                    print(f"SUCCESS: Placed SL order for {stock_symbol} with ID: {sl_order_id}")
                                    trades_today_count += 1
                                    print(f"Trades taken today: {trades_today_count}/{MAX_DAILY_TRADES}")
                                    open_positions[stock_symbol] = {
                                        'qty': quantity, 
                                        'buy_order_id': order_id, 
                                        'sl_order_id': sl_order_id, 
                                        'sl_price': stop_loss_price, 
                                        'direction': 'BUY'
                                    }
                                else:
                                    print(f"CRITICAL WARNING: Failed to place SL order for {stock_symbol}. Exiting position immediately for safety.")
                                    print(f"--- [API-SEND] exit_position for {stock_symbol} (safety exit) ---")
                                    tsl.exit_position(tradingsymbol=stock_symbol, exchange='NSE', trade_type='MIS')
                                    print(f"--- [API-RECV] exit_position sent for {stock_symbol} (safety exit) ---")

                            else:
                                print(f"WARNING: Order for {stock_symbol} (ID: {order_id}) was not confirmed as TRADED. Final status: {final_order_status.upper()}. Will not place SL.")

                except Exception as e:
                    print(f"--- [DEBUG] ERROR processing stock {stock_symbol}: {e} ---")

            print("--- [DEBUG] Finished scanning all stocks. ---")
            print(f"Current available balance after scan: ₹{available_balance:.2f}")
            # Wait intelligently for the next 15-minute candle to form
            wait_for_next_candle(int(TIMEFRAME_MINUTES))


        except KeyboardInterrupt:
            print("--- [DEBUG] KeyboardInterrupt detected. Exiting... ---")
            print("Strategy stopped by user.")
            break
        except Exception as e:
            print(f"--- [DEBUG] An unexpected error occurred in the main loop: {e} ---")
            time.sleep(60)

if __name__ == "__main__":
    run_macd_strategy() 

Hey @DKS ,

Thanks for bringing this up @Tradehull_Imran might be able to assist here.

1 Like

Hello @DKS,

Your BUY order is correct, but your SL order is never getting placed because the order_type you used is invalid for Dhan.

You used:

'order_type': 'STOP_LOSS'

But Dhan does NOT support STOP_LOSS.
The only valid values are:

'MARKET', 'LIMIT', 'STOPLIMIT', 'STOPMARKET'

Because of this, your SL order is rejected internally and never goes through.


:white_check_mark: Correct SL order placement code (Dhan format)

Use STOPMARKET for SL-M:

sl_order_params = {
    'tradingsymbol': stock_symbol,
    'exchange': 'NSE',
    'quantity': quantity,
    'order_type': 'STOPMARKET',   # ✔ valid DHAN type
    'transaction_type': 'SELL',
    'trade_type': 'MIS',
    'price': 0,                   # For SL-M
    'trigger_price': float(stop_loss_price)
}
sl_order_id = tsl.order_placement(**sl_order_params)


:magnifying_glass_tilted_left: Why SL was not getting placed?

  • You used an unsupported order_typeSTOP_LOSS

  • Dhan silently rejects it → so no SL / Exit ever happens


:white_check_mark: Final Note

Just changing the SL order type to STOPMARKET or STOPLIMIT will fix your issue.

1 Like