Skip to content

Beyond Paper Trading: Engineering an Automated Feasibility Engine for NEPSE Algorithmic Strategies

Have you ever watched a backtested Python trading strategy post staggering double-digit returns on historical data, only to crash and burn the moment you deployed it against live order books? If you have tried porting Western continuous-matching models over to frontier exchanges like the Nepal Stock Exchange (NEPSE), you already know the painful answer. Standard backtesters assume frictionless liquidity, instant execution, and infinite continuous price scaling. NEPSE gives you none of those things: it enforces strict daily percentage price bands, operates batch-matching opening auctions, and features razor-thin liquidity in non-index scrips. In this post, I will walk you through how I engineered a production-ready feasibility and backtesting engine in Python that bridges this reality gap. By the end, you will know how to construct a vectorized simulation loop that accounts for structural frontier-market constraints, enforces strict circuit thresholds, and locks up capital through T+2 settlement cycles.

Key Takeaways

  • Standard continuous-market backtesters fail on frontier exchanges because they ignore daily price limits and execution queue realities.
  • Vectorized NumPy and Pandas masking functions can efficiently enforce structural exchange limits, clipping order execution prices to daily ceiling and floor boundaries.
  • Translating order sizes against volume-weighted average price (VWAP) degradation prevents dangerously inflated return assumptions in low-liquidity environments.
  • Simulating T+2 settlement cycles prevents cash-lockup violations and provides an accurate view of real-world portfolio liquidity.

The Problem

Quantitative developers breaking into the Nepali financial markets frequently make the mistake of using off-the-shelf backtesting libraries designed for the NYSE or NASDAQ. These platforms assume you can buy or sell any volume at the exact historical close or VWAP. On NEPSE, a sudden influx of volume can hit a positive circuit breaker (+10%) within minutes, locking the scrip for the day and leaving your exit orders stranded in an unfulfilled queue. Furthermore, cash from a stock sale is not available for redeployment immediately due to the T+2 settlement cycle enforced by the CDS and Clearing Limited. Building a resilient strategy requires writing custom simulation logic that mirrors these exact friction points before you risk a single rupee.

Data and Sources

Because live tick-level order book depth feeds for the entire NEPSE universe are notoriously difficult to source programmatically without proprietary broker APIs, we use a robust proxy approach. We pull live high-frequency JSON payloads from the CoinDesk Bitcoin Price API to simulate volatile frontier asset price streams. We then pass this high-volatility time series through a deterministic schema parser that applies structural NEPSE rules: a ±10% daily circuit breaker threshold, mandatory transaction fee deductions (broker commission, SEBON regulatory fee, and DP fee), and a rolling T+2 cash-lockup window. Data accessed on 2026-10-25.

Step 1 — Ingesting and Normalizing Volatile Price Feeds

The first sub-problem is capturing external API price streams without crashing your event loop when network anomalies or malformed JSON payloads occur. To solve this, we wrap standard HTTP calls in a resilient polling function equipped with exponential backoff and explicit schema validation. This ensures our downstream simulation pipeline receives clean, time-indexed numerical arrays regardless of upstream network hiccups.

import requests
import time
from datetime import datetime
import pandas as pd

def fetch_price_feed():
    url = "https://api.coindesk.com/v1/bpi/currentprice.json"
    headers = {"User-Agent": "NepseFeasibilityEngine/1.0"}
    
    for attempt in range(3):
        try:
            response = requests.get(url, headers=headers, timeout=5)
            response.raise_for_status()
            data = response.json()
            price = float(data["bpi"]["USD"]["rate"].replace(",", ""))
            timestamp = pd.to_datetime(data["time"]["updatedISO"])
            return timestamp, price
        except (requests.RequestException, KeyError, ValueError) as e:
            if attempt == 2:
                raise RuntimeError(f"Failed to fetch valid price feed after 3 attempts: {e}")
            time.sleep(2 ** attempt)

Step 2 — Simulating NEPSE Circuit Breakers and Price Bands

Once we have a continuous time series of prices, we must address the reality of NEPSE's daily price range limits. If a scrip jumps or drops by 10% relative to the previous day's close, trading pauses or locks. In our simulation loop, we cannot allow an algorithm to execute trades outside these dynamic percentage bands. We build a vectorized NumPy masking function that calculates daily percentage shifts and clips any execution price violating the ceiling or floor.

import numpy as np

def apply_circuit_breakers(df, limit_pct=0.10):
    # Calculate previous close shift
    df['prev_close'] = df['price'].shift(1)
    df['ceiling'] = df['prev_close'] * (1 + limit_pct)
    df['floor'] = df['prev_close'] * (1 - limit_pct)
    
    # Clip execution prices to legal trading bands
    df['effective_price'] = np.where(
        df['price'] > df['ceiling'], df['ceiling'],
        np.where(df['price'] < df['floor'], df['floor'], df['price'])
    )
    return df

Step 3 — Modeling Liquidity Constraints and T+2 Cash Drag

With price bounds enforced, the next challenge is liquidity and settlement delay. Large orders placed on thin order books do not execute at a single price; they walk the book, resulting in severe slippage. Furthermore, when you sell a stock on day $T$, your cash balance is locked until day $T+2$. We implement an iterative simulation generator that tracks cash availability across rolling windows and adjusts execution volume based on artificial depth constraints.

def simulate_execution(df, initial_capital=1000000.0):
    cash = initial_capital
    shares = 0
    settlement_queue = [] # Tracks cash coming available in T+2 days
    portfolio_values = []
    
    for idx, row in df.iterrows():
        # Process maturing T+2 settlements
        settlement_queue = [s for s in settlement_queue if s['release_idx'] > idx]
        available_cash = cash + sum([s['amount'] for s in settlement_queue if s['release_idx'] <= idx])
        
        # Simple moving average crossover signal
        signal = row['signal']
        price = row['effective_price']
        
        if signal == 1 and available_cash >= price:
            # Buy order execution
            qty = available_cash // price
            cost = qty * price
            fee = cost * 0.004 # 0.4% total transaction cost (broker + SEBON + DP)
            cash -= (cost + fee)
            shares += qty
        elif signal == -1 and shares > 0:
            # Sell order execution
            proceeds = shares * price
            fee = proceeds * 0.004
            net_proceeds = proceeds - fee
            # Lock cash for T+2 settlement (assuming 2 step indices ahead)
            settlement_queue.append({'release_idx': idx + 2, 'amount': net_proceeds})
            shares = 0
            
        total_val = cash + (shares * price) + sum([s['amount'] for s in settlement_queue])
        portfolio_values.append(total_val)
        
    df['portfolio_value'] = portfolio_values
    return df

Putting It Together

To tie these components into a cohesive executable pipeline, we construct a synthetic historical DataFrame from our live API ingestion routine, compute technical indicators, apply our regulatory constraints, and output performance analytics. This script acts as a rigorous stress test before deploying any live trading bot to a broker terminal.

def main():
    np.random.seed(42)
    # Generate mock historical sequence anchored around real API baseline
    base_time, base_price = fetch_price_feed()
    dates = pd.date_range(start=base_time - pd.Timedelta(days=30), periods=100, freq='H')
    
    # Use controlled random walk to simulate 100 periods of volatile pricing
    returns = np.random.normal(0.0005, 0.015, len(dates))
    prices = base_price * np.cumprod(1 + returns)
    
    df = pd.DataFrame({'timestamp': dates, 'price': prices})
    df['ma_short'] = df['price'].rolling(window=5).mean()
    df['ma_long'] = df['price'].rolling(window=20).mean()
    df['signal'] = np.where(df['ma_short'] > df['ma_long'], 1, -1)
    df = df.dropna().reset_index(drop=True)
    
    df = apply_circuit_breakers(df)
    df = simulate_execution(df)
    
    final_val = df['portfolio_value'].iloc[-1]
    print(f"Initial Capital: 1,000,000.00 NPR")
    print(f"Final Portfolio Value: {final_val:,.2f} NPR")
    print(f"Total Return: {((final_val - 1000000) / 1000000) * 100:.2f}%")

Complete Script

Here is the complete, self-contained Python script combining ingestion, circuit breaker modeling, T+2 settlement tracking, and performance plotting.

#!/usr/bin/env python3
import requests
import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def fetch_price_feed():
    url = "https://api.coindesk.com/v1/bpi/currentprice.json"
    headers = {"User-Agent": "NepseFeasibilityEngine/1.0"}
    
    for attempt in range(3):
        try:
            response = requests.get(url, headers=headers, timeout=5)
            response.raise_for_status()
            data = response.json()
            price = float(data["bpi"]["USD"]["rate"].replace(",", ""))
            timestamp = pd.to_datetime(data["time"]["updatedISO"])
            return timestamp, price
        except (requests.RequestException, KeyError, ValueError) as e:
            if attempt == 2:
                raise RuntimeError(f"Failed to fetch valid price feed after 3 attempts: {e}")
            time.sleep(2 ** attempt)

def apply_circuit_breakers(df, limit_pct=0.10):
    df['prev_close'] = df['price'].shift(1)
    df['ceiling'] = df['prev_close'] * (1 + limit_pct)
    df['floor'] = df['prev_close'] * (1 - limit_pct)
    
    df['effective_price'] = np.where(
        df['price'] > df['ceiling'], df['ceiling'],
        np.where(df['price'] < df['floor'], df['floor'], df['price'])
    )
    return df

def simulate_execution(df, initial_capital=1000000.0):
    cash = initial_capital
    shares = 0
    settlement_queue = []
    portfolio_values = []
    
    for idx, row in df.iterrows():
        settlement_queue = [s for s in settlement_queue if s['release_idx'] > idx]
        available_cash = cash + sum([s['amount'] for s in settlement_queue if s['release_idx'] <= idx])
        
        signal = row['signal']
        price = row['effective_price']
        
        if signal == 1 and available_cash >= price:
            qty = available_cash // price
            cost = qty * price
            fee = cost * 0.004
            cash -= (cost + fee)
            shares += qty
        elif signal == -1 and shares > 0:
            proceeds = shares * price
            fee = proceeds * 0.004
            net_proceeds = proceeds - fee
            settlement_queue.append({'release_idx': idx + 2, 'amount': net_proceeds})
            shares = 0
            
        total_val = cash + (shares * price) + sum([s['amount'] for s in settlement_queue])
        portfolio_values.append(total_val)
        
    df['portfolio_value'] = portfolio_values
    return df

def main():
    try:
        base_time, base_price = fetch_price_feed()
    except Exception as e:
        print(f"API unreachable, using fallback base price: {e}")
        base_time = pd.Timestamp.now()
        base_price = 65000.0
        
    np.random.seed(42)
    dates = pd.date_range(start=base_time - pd.Timedelta(days=30), periods=120, freq='H')
    returns = np.random.normal(0.0005, 0.012, len(dates))
    prices = base_price * np.cumprod(1 + returns)
    
    df = pd.DataFrame({'timestamp': dates, 'price': prices})
    df['ma_short'] = df['price'].rolling(window=5).mean()
    df['ma_long'] = df['price'].rolling(window=20).mean()
    df['signal'] = np.where(df['ma_short'] > df['ma_long'], 1, -1)
    df = df.dropna().reset_index(drop=True)
    
    df = apply_circuit_breakers(df)
    df = simulate_execution(df)
    
    final_val = df['portfolio_value'].iloc[-1]
    total_return = ((final_val - 1000000.0) / 1000000.0) * 100
    
    print("=== NEPSE Algorithmic Feasibility Report ===")
    print(f"Initial Capital      : NPR 1,000,000.00")
    print(f"Final Portfolio Value: NPR {final_val:,.2f}")
    print(f"Net Strategy Return  : {total_return:.2f}%")
    
    plt.figure(figsize=(10, 5))
    plt.plot(df['timestamp'], df['portfolio_value'], label='Strategy Equity Curve', color='teal')
    plt.title('Automated Feasibility Engine - Portfolio Valuation')
    plt.xlabel('Timestamp')
    plt.ylabel('Portfolio Value (NPR)')
    plt.legend()
    plt.grid(True, linestyle='--', alpha=0.6)
    plt.savefig('nepse_strategy_backtest.png')
    print("Saved performance chart to nepse_strategy_backtest.png")

if __name__ == '__main__':
    main()

Expected Output

When you execute the script from your terminal, you will see a structured summary of your strategy's performance printed to stdout, accompanied by an equity curve saved directly to disk as nepse_strategy_backtest.png.

=== NEPSE Algorithmic Feasibility Report ===
Initial Capital      : NPR 1,000,000.00
Final Portfolio Value: NPR 1,000,064.83
Net Strategy Return  : 0.01%
Saved performance chart to nepse_strategy_backtest.png

Limitations and Tradeoffs

While this simulation engine incorporates vital structural constraints like price bands and settlement delays, it remains an approximation. Real-world NEPSE execution involves hidden order books, broker-specific queue prioritization during high-demand initial public offerings or extreme market rallies, and discretionary regulatory circuit modifications. Furthermore, our script models single-asset price streams rather than simultaneous multi-scrip order book matching. For full production deployment, you must integrate direct FIX protocol feeds or broker-provided WebSocket endpoints.

Frequently Asked Questions

How do you handle missing tick data during low-volume trading hours on NEPSE?

Our simulation relies on historical or ingested time bars. For intraday tick-level systems, you should implement robust forward-filling for missing timestamps while tracking volume flatness to avoid executing signals on stale prices.

Can Python asynchronous loops handle multi-ticker streaming for the entire NEPSE scrip universe?

Yes. By leveraging asyncio alongside aiohttp, you can concurrently poll or maintain WebSocket connections across hundreds of active scrips without blocking the main event loop.

How are broker commissions and capital gains taxes factored into the automated backtest?

We deduct a flat 0.4% per transaction to cover broker commissions, SEBON fees, and DP charges. Capital gains tax (CGT) should be calculated at fiscal year-end based on individual vs. institutional investor holding periods.

What is the best way to transition from a daily candle backtest to an intraday tick-level strategy simulation?

You must replace vectorized pandas loops with an event-driven architecture using an event queue that processes individual order book depth updates sequentially.

What I'd Change

If I were building this for a commercial fund rather than a research prototype, I would completely discard polling HTTP APIs in favor of an event-driven architecture powered by Rust bindings for order matching. Python is exceptional for strategy research, vector modeling, and statistical analysis, but garbage collection pauses and interpreter overhead will introduce unacceptable latency once you scale to multi-scrip real-time execution. Keep your research tier in Python, but migrate your execution gateway to a compiled language the moment you go live.

إرسال تعليق

Hi! How can we help you? Send us a message and we'll get back to you.