gsTradingEngine v2.0

The gsTradingEngine is the C# backtesting engine built into gStockly. Write algorithmic trading strategies directly in the browser — they are compiled at runtime via Roslyn and executed against real historical OHLCV data.

Looking for practical strategies? Open the companion example library: gsTradingSystemExamples.html

💻 C# Compiled Engine

Full C# syntax, LINQ, and all .NET 8 features. Compiled at runtime — no restrictions.

📈 60+ Indicators

SMA, EMA, RSI, MACD, Bollinger, ATR, Stochastic, Ichimoku, SuperTrend and more via Skender.

📊 Real Market Data

Daily, weekly and monthly OHLCV quotes. Auto benchmark vs S&P 500 and Nasdaq-100.

🌐 Dynamic Universes

Resolve ticker lists dynamically from metadata such as index membership, country, exchange, sector and market cap.

🏆 Ranked Portfolios

Score every ticker, rebalance on daily, weekly or monthly bars and automatically buy the top-ranked names.

🔧 Deployment Check

The v2 health endpoint exposes engine name, version and build date so you can verify the active backend in seconds.

How It Works

Every strategy inherits from TradingSystemBase and implements exactly two methods:

  • ConfigureSystem() — called once at startup to set tickers, dates, capital and parameters.
  • ExecuteStrategy() — called for every trading day × every ticker. This is where your buy/sell logic lives.
public override void ConfigureSystem()
{
    Config.Tickers  = new[] { "AAPL", "MSFT" };
    Config.FromDate = new DateTime(2020, 1, 1);
    Config.ToDate   = DateTime.Today;
    Config.StartEquity        = 10_000;
    Config.InvestmentPerTrade = 10_000;
}

public override void ExecuteStrategy(string ticker = null)
{
    // CurrentTicker and CurrentDate are set automatically by the engine
    double sma20 = GetSMA(20);
    double sma50 = GetSMA(50);
    bool inPos = OpenPositions.ContainsKey(CurrentTicker);

    if (sma20 > sma50 && !inPos) Buy("SMA20 crossed above SMA50");
    if (sma20 < sma50 &&  inPos) Sell("SMA20 crossed below SMA50");
    SetStopLoss(5.0);
}
v2 addition: You can now define Config.Universe for dynamic ticker resolution and Config.RankedUniverse for top-N ranking strategies with automatic portfolio rotation.
Execution order: In TickerFirst mode (default), one ticker is fully processed before moving to the next. In DateFirst mode all tickers are evaluated on the same date before advancing — use this for cross-asset logic or portfolio rotation.

ConfigureSystem()

Called once before the backtest begins. Set your tickers, date range, capital, commissions and execution parameters here. In v2 you can also define a dynamic universe and ranked portfolio rules. Parameters set via the UI always override values set in code.

TradingSystemConfig Properties

PropertyTypeDefaultDescription
Tickersstring[]Ticker symbols to backtest, e.g. new[]{"AAPL","MSFT"}
FromDateDateTimeBacktest start date
ToDateDateTimeBacktest end date
StartEquitydouble10 000Initial portfolio capital in USD
InvestmentPerTradedouble10 000Fixed USD amount per trade (capped at available equity)
InvestmentPerTradePercTotalEquitydouble0If > 0, invest this % of current equity per trade instead of a fixed amount. Enables compounding.
OpenCommissionValuedouble0Fixed commission per open trade (USD)
OpenCommissionPercdouble0Open commission as % of trade value
CloseCommissionValuedouble0Fixed commission per close trade (USD)
CloseCommissionPercdouble0Close commission as % of trade value
SpreadPricePercdouble0.01Bid-ask spread % applied to execution price on every order
PercTaxdouble26Capital gains tax % applied to profitable trades on close
DefaultExecutionTypeExecutionTypeClosePrice used for order execution — see table below
DefaultTimeframestring"d"Default timeframe: "d" daily · "w" weekly · "m" monthly
ModeBacktestModeTickerFirstIteration order: TickerFirst or DateFirst
UnifiedPortfolioboolfalseSingle shared cash account across all tickers. Required for ranked top-N strategies.
UniverseUniverseFilter?nullOptional dynamic universe definition resolved from the tickers table.
RankedUniverseRankedUniverseOptionsdisabledOptional top-N ranking mode with rebalance frequency and minimum score filters.

ExecutionType

Controls which price is used when an order is executed on a given day.

ValueBuy priceSell priceUse case
CloseCloseCloseDefault. Signal detected on close, execute at close.
NextOpenNext day OpenNext day OpenMore realistic: signal on close, execute next morning.
Average(High+Low)/2(High+Low)/2Mid-day average price assumption.
PessimisticHighLowWorst case: buy at session high, sell at session low.
OptimisticLowHighBest case: buy at session low, sell at session high.

BacktestMode

ValueIteration orderUse case
TickerFirstFor each ticker → iterate all datesDefault. Independent single-asset strategies.
DateFirstFor each date → iterate all tickersMulti-asset logic: pair trading, portfolio rotation, cross-asset signals.

Ranked Universe v2

v2 adds a new cross-sectional workflow. Instead of relying only on a manually typed ticker list, you can resolve a dynamic universe from metadata and assign a score to each ticker inside ExecuteStrategy(). On rebalance bars the engine automatically buys the top-ranked names.

UniverseFilter

PropertyTypeDescription
Namestring?Optional label used in logs and debugging.
PartOfstring?Index or group membership such as "S&P 500" or "Nasdaq-100".
Country, Area, Exchangestring?Geographic and market filters from the tickers table.
Sector, Industry, Categorystring?Business classification filters.
BenchmarkTicker, Currencystring?Additional metadata filters.
EnabledOnlyboolDefaults to true. Excludes disabled tickers.
MinMarketCapdouble?Optional minimum market cap filter.
Limitint?Optional cap on the resolved universe size.
IncludeTickers, ExcludeTickersstring?Comma-separated include and exclude lists applied after the dynamic query.

RankedUniverseOptions

PropertyTypeDescription
EnabledboolTurns ranked top-N portfolio mode on.
MaxPositionsintMaximum number of simultaneous positions selected from the highest scores.
MinScoredouble?Optional minimum score required for eligibility.
RebalanceFrequencystringDaily, Weekly or Monthly.
ClosePositionsOnRotateboolIf true, positions that fall out of the top-N set are closed automatically at rebalance.
Important: ranked top-N mode is designed to be used with Config.UnifiedPortfolio = true. The engine uses a single shared cash account and forces date-first execution so all tickers are compared on the same bar.

Health & Versioning v2

The v2 backend exposes explicit deployment metadata from /health so you can verify which engine is active on the server.

{
  "status": "ok",
  "engine": "gsTradingSystemEngine_v2",
  "version": "2.0.0",
  "built": "2026-06-27",
  "ts": "2026-06-27T10:00:00Z"
}

ExecuteStrategy()

Called for every active trading day for every ticker. The engine sets the following properties automatically before your code runs:

PropertyTypeDescription
CurrentTickerstringThe ticker currently being evaluated (e.g. "AAPL")
CurrentDateDateTimeThe date currently being evaluated
CurrentCandleCandle?Full OHLCV candle for the current day. Null if no quote exists.
NextCandleCandle?Next available candle (used internally for NextOpen execution)
OpenPositionsDictionary<string, TradePosition>Currently open trades, keyed by ticker symbol
v2 ranked mode: you still implement only ExecuteStrategy(), but in ranked universe strategies you usually compute a score with SetScore(...) instead of calling Buy() on every candidate ticker. The engine then buys the top-ranked names on rebalance bars.

Score Helpers v2

HelperReturnsDescription
SetScore(score, note?, ticker?)voidAssigns a cross-sectional score to the current ticker or to an explicitly provided ticker.
ClearScore(ticker?)voidRemoves a previously assigned score so the ticker is ignored by the top-N selector.
GetPendingScore(ticker?)double?Returns the score already assigned in the current bar, if any.
HasPendingScore(ticker?)boolTrue if a score has already been registered for the ticker in the current bar.
IsRankedUniverseMode()boolTrue when the engine is running in ranked top-N mode.
UniverseTickersstring[]The final resolved universe after dynamic filters and include/exclude adjustments.
double score = 0;
if (Close() > GetSMA(200)) score += 10;
score += GetChangePercent(60);

if (score >= 20)
    SetScore(score, $"score={score:F1}");
else
    ClearScore();

Orders

Long Entry Buy(string? reason = null, ExecutionType? executionType = null)

Opens a long position on CurrentTicker. Ignored if a position is already open for that ticker. Calculates shares from InvestmentPerTrade (or % of equity if configured). Applies spread and commission at entry, tax on profitable close.

ParameterTypeDescription
reasonstring?Label saved in the trade record (e.g. "SMA20 > SMA50"). Optional.
executionTypeExecutionType?Overrides the default execution type for this single order. Optional.
Long Exit Sell(string? reason = null, ExecutionType? executionType = null)

Closes an open long position on CurrentTicker. Calculates profit, applies tax, saves the closed trade to the database. No-op if no long position is open.

Short Entry Short(string? reason = null, ExecutionType? executionType = null)

Opens a short position on CurrentTicker. Shares are stored as a negative value. Profit is calculated as open value minus close value (inverse of a long).

Short Exit Cover(string? reason = null, ExecutionType? executionType = null)

Closes an open short position on CurrentTicker.

Stop Loss bool SetStopLoss(double pctLoss, ExecutionType? executionType = null)

Checks if the current execution price has crossed the stop-loss threshold. If triggered, automatically calls Sell() or Cover() and returns true. Returns false if no stop was hit.

// Exit if price drops 5% below entry (long position)
SetStopLoss(5.0);

// Stop with early return — skip remaining logic if stopped out
if (SetStopLoss(7.0)) return;
Close All void CloseAllPositions(ExecutionType? executionType = null)

Closes all open positions across all tickers. Called automatically by the engine at the end of the backtest. Can also be called manually mid-strategy.

Take Profit bool SetTakeProfit(double pctProfit, ExecutionType? executionType = null)

Closes the position if the unrealised gain % reaches pctProfit. Works for both long and short positions. Returns true if triggered.

ParameterTypeDescription
pctProfitdoubleTarget profit % from entry price
executionTypeExecutionType?Override execution price. Optional.
// Exit at +15% profit
if (SetTakeProfit(15.0)) return;

// Combine with stop loss: take profit at 15%, stop at 5%
if (SetTakeProfit(15.0)) return;
SetStopLoss(5.0);
Trailing Stop bool SetTrailingStop(double pctTrail, ExecutionType? executionType = null)

Dynamic stop loss that follows the price. Tracks the highest price reached since entry (long) or lowest (short) and exits if price reverses by pctTrail% from the peak. Returns true if triggered.

The trailing high/low is initialised at the entry price and updated automatically every time SetTrailingStop is called. No extra state needed in the strategy code.
ParameterDescription
pctTrailDistance % from peak price that triggers the exit. E.g. 8.0 = exit if price drops 8% from its highest point since entry.
// Trail 8% from peak (long)
if (SetTrailingStop(8.0)) return;

// Combine: tight initial stop, then trail once in profit
if (currentGainPercent() > 5.0)
    SetTrailingStop(6.0); // switch to trailing once +5%
else
    SetStopLoss(3.0);   // tight fixed stop while not yet in profit
ATR Stop bool SetATRStop(double multiplier = 2.0, int atrPeriod = 14, ExecutionType? executionType = null)

Volatility-adaptive stop loss. Exits if price moves multiplier × ATR(atrPeriod) against the entry price. Automatically widens in volatile markets and tightens in quiet ones. Returns true if triggered.

ParameterDefaultDescription
multiplier2.0ATR multiplier. 1.5 = tight, 2.0 = standard, 3.0 = wide
atrPeriod14Period for ATR calculation
// Standard: 2× ATR(14) stop
SetATRStop();

// Wide stop for swing trading: 3× ATR(14)
SetATRStop(3.0);

// Tight stop for mean reversion: 1.5× ATR(10)
SetATRStop(1.5, 10);

OpenPositions

OpenPositions is a Dictionary<string, TradePosition> where the key is the ticker symbol. It always reflects the currently open trades.

// Check if a position is open for the current ticker
bool inPos = OpenPositions.ContainsKey(CurrentTicker);

// Filter by side using PositionSide
bool hasLong  = OpenPositions.Any(p => p.Value.PositionSide == 1);
bool hasShort = OpenPositions.Any(p => p.Value.PositionSide == -1);

// How many positions are open?
int count = OpenPositions.Count;

TradePosition Properties

PropertyTypeDescription
OpenDateDateTimeDate the position was opened
OpenPricedoubleActual execution price at entry (after spread applied)
SharesintNumber of shares. Positive = long, negative = short.
OpenPositiondoubleTotal invested amount (Shares × OpenPrice)
OpenCommissiondoubleCommission paid at entry
OpenPositionNotestringThe reason string passed to Buy() or Short()
PositionSideint1 = Long, -1 = Short (derived from Shares sign)

Equity & Gain Helpers

Equity double GetCurrentCumulativeProfit()

Returns the running equity for CurrentTicker from the in-memory cache (updated after every closed trade). Returns StartEquity if no trades have been closed yet. Use this to size positions dynamically.

Performance: This method is now backed by an in-memory dictionary — zero DB queries per candle, regardless of backtest length or ticker count.
Unrealised P&L double currentGainPercent()

Unrealised gain % of the current open position on CurrentTicker, accounting for spread and close commission. Returns 0 if no position is open.

Unrealised P&L double currentGainValue()

Same as currentGainPercent() but returns the absolute USD unrealised gain/loss.

📄 Example — Take Profit + Dynamic Sizing

// Take profit at +15%
if (currentGainPercent() > 15.0)
    Sell("Take profit +15%");

// Invest 10% of current equity per trade (compounds over time)
Config.InvestmentPerTradePercTotalEquity = 10;

Log()

Logging void Log(string message)

Writes a message to the Log tab in the interface during a live backtest run. The message is automatically prefixed with the current ticker and date.

Log($"RSI={rsi:F1} SMA20={sma20:F2} — entering long");
// → [AAPL 2023-06-15] RSI=28.4 SMA20=182.34 — entering long

Log($"Stop hit at {Close():F2}, entry was {pos.OpenPrice:F2}");

Candle Access

All candle functions default to CurrentTicker and DefaultTimeframe. Offset 0 = current day, -1 = one period back, -2 = two periods back, etc.

MethodReturnsDescription
Close(ticker?)doubleClose price of the current day
Open(ticker?)doubleOpen price of the current day
High(ticker?)doubleSession high of the current day
Low(ticker?)doubleSession low of the current day
Volume(ticker?)doubleVolume of the current day
GetCandle(offset, tf?, ticker?)Candle?Full OHLCV candle N periods back (0 = current, -1 = previous)
GetClose(offset, tf?, ticker?)doubleClose N periods back
GetOpen(offset, tf?, ticker?)doubleOpen N periods back
GetHigh(offset, tf?, ticker?)doubleHigh N periods back
GetLow(offset, tf?, ticker?)doubleLow N periods back
GetVolume(offset, tf?, ticker?)doubleVolume N periods back
// Compare yesterday's close to today's
double today     = GetClose(0);
double yesterday = GetClose(-1);
bool   upDay     = today > yesterday;

// Access another ticker (e.g. S&P 500 as filter)
double spxClose = GetClose(0, "d", "^GSPC");
if (spxClose < GetClose(-1, "d", "^GSPC")) return; // skip if market is down

Date Helpers

MethodReturns
GetCurrentDayOfWeek()DayOfWeek — Monday, Tuesday…
GetCurrentDay()int — day of month (1–31)
GetCurrentMonth()int — month (1–12)
GetCurrentYear()int — year

GetChangePercent()

Price Change double GetChangePercent(int bars, string? tf = null, string? ticker = null)

Returns the % price change of the current close vs the close N bars ago. Supports all timeframes including multi-timeframe lookups.

// 20-day momentum
double mom20 = GetChangePercent(20);

// Weekly performance over last 4 weeks
double w4 = GetChangePercent(4, "w");

// Only buy if 3-month momentum is positive
if (GetChangePercent(63) > 0 && !inPos) Buy("Positive 3-month momentum");

GetSlopePercent()

Trend Slope double GetSlopePercent(int period, string? tf = null, string? ticker = null)

Linear regression slope of closing prices over the last N bars, expressed as % per bar. More robust than a single-bar comparison because it fits all N points. Positive = uptrend, negative = downtrend.

// Confirm uptrend: slope > 0.05% per day
if (GetSlopePercent(30) > 0.05) Buy("Rising slope");

// Weekly slope > 0 = structural uptrend
bool weeklyUp = GetSlopePercent(26, "w") > 0;

GetDaysInPosition()

Position Age int GetDaysInPosition(string? ticker = null)

Returns the number of calendar days since the current position was opened. Returns 0 if no position is open. Useful for time-based exits.

// Exit if held more than 20 days without reaching target
if (GetDaysInPosition() > 20)
    Sell("Time exit: 20 days elapsed");

// Tighten stop after 10 days in position
double stopPct = GetDaysInPosition() > 10 ? 3.0 : 6.0;
SetStopLoss(stopPct);

IsFirstBarOfWeek() / IsFirstBarOfMonth()

Time Filter bool IsFirstBarOfWeek()  /  bool IsFirstBarOfMonth()

Returns true if the current bar is the first trading day of the week or month. Based on actual trading calendar (not simply Monday/first day of month), so it works correctly on holiday weeks.

// Only enter positions at start of week
if (!IsFirstBarOfWeek()) return;

// Monthly rebalancing
if (IsFirstBarOfMonth())
{
    CloseAllPositions();
    Log("Monthly rebalance triggered");
}

Moving Averages

All MA functions now support the tf parameter for multi-timeframe indicators. If tf differs from DefaultTimeframe, the engine automatically preloads that timeframe's data on first use. Powered by Skender.Stock.Indicators.

MethodReturnsDescription
GetSMA(period, tf?, pt?, t?)doubleSimple Moving Average
GetEMA(period, tf?, pt?, t?)doubleExponential Moving Average
GetWMA(period, t?)doubleWeighted Moving Average
GetDEMA(period, t?)doubleDouble EMA
GetTEMA(period, t?)doubleTriple EMA
GetHMA(period, t?)doubleHull Moving Average
GetVWMA(period, t?)doubleVolume-Weighted Moving Average
GetVWAP(t?)doubleVolume-Weighted Average Price

Oscillators

MethodReturnsNotes
GetRSI(period=14, tf?, t?)double0–100. <30 oversold, >70 overbought
GetMACD(fast=12, slow=26, sig=9, tf?, t?)(macd, signal, histogram)Tuple — use .macd, .signal, .histogram
GetBollingerBands(period=20, std=2.0, tf?, t?)(upper, middle, lower)Upper band, SMA, lower band
GetATR(period=14, tf?, t?)doubleAverage True Range in price units
GetStochastic(k=14, d=3, sk=3, tf?, t?)(k, d)%K and %D lines, 0–100
GetStochRSI(rp=14, sp=14, sig=3, smk=1, t?)(k, d)Stochastic RSI, 0–100
GetWilliamsR(period=14, tf?, t?)double–100 to 0
GetCCI(period=20, tf?, t?)doubleCommodity Channel Index
GetROC(period=10, t?)doubleRate of Change %
GetMFI(period=14, t?)doubleMoney Flow Index, 0–100
GetCMF(period=20, t?)doubleChaikin Money Flow, –1 to 1
GetMomentum(period=10, t?)doublePrice difference (Close[0] – Close[N])
double rsi = GetRSI(14);
var (macd, signal, hist) = GetMACD();
var (upper, mid, lower)  = GetBollingerBands(20, 2.0);
var (k, d) = GetStochastic();

if (rsi < 40 && hist > 0 && !OpenPositions.ContainsKey(CurrentTicker))
    Buy("RSI oversold + MACD positive histogram");

Trend & Channels

MethodReturnsNotes
GetADX(period=14, t?)(adx, pdi, mdi)ADX, +DI, –DI. ADX > 25 = strong trend.
GetAroon(period=25, t?)(aroonUp, aroonDown, osc)0–100 each
GetParabolicSAR(accel=0.02, max=0.2, t?)(sar, isReversal)isReversal=true when SAR just flipped sides
GetSuperTrend(period=10, mult=3.0, t?)(superTrend, isUpperBand)isUpperBand=true = bearish regime
GetIchimoku(tp=9, kp=26, sp=52, t?)(tenkan, kijun, senkouA, senkouB, chikou)Full Ichimoku cloud components
GetDonchian(period=20, t?)(upper, lower, center)Donchian channel bands
GetKeltner(ema=20, mult=2.0, atr=10, t?)(upper, center, lower)Keltner channel bands

Volume

MethodReturnsDescription
GetOBV(t?)doubleOn-Balance Volume — cumulative
GetAvgVolume(period=20, t?)doubleSimple average volume over N periods

Other Indicators

MethodReturnsDescription
GetHighest(period, t?)doubleHighest High over the last N bars
GetLowest(period, t?)doubleLowest Low over the last N bars
GetPivotPoints(t?)(pp, r1, s1, r2, s2)Classic pivot point levels from previous bar
GetPriceHistory(priceType, from, to, tf?, t?)double[]Raw price array for a date range. priceType: "close", "open", "high", "low", "volume"
GetVolumeHistory(from, to, tf?, t?)double[]Volume array for a date range

Example: SMA Crossover

The classic golden/death cross — long when SMA20 > SMA50, exit when it crosses back. Uses NextOpen execution for realism.

public override void ConfigureSystem()
{
    Config.Tickers            = new[] { "AAPL" };
    Config.FromDate           = new DateTime(2018, 1, 1);
    Config.ToDate             = DateTime.Today;
    Config.StartEquity        = 10_000;
    Config.InvestmentPerTrade = 10_000;
    Config.SpreadPricePerc    = 0.01;
    Config.PercTax            = 26;
    Config.DefaultExecutionType = ExecutionType.NextOpen;
}

public override void ExecuteStrategy(string ticker = null)
{
    double sma20 = GetSMA(20);
    double sma50 = GetSMA(50);
    bool   inPos = OpenPositions.ContainsKey(CurrentTicker);

    if (sma20 > sma50 && !inPos) Buy("SMA20 > SMA50");
    if (sma20 < sma50 &&  inPos) Sell("SMA20 < SMA50");
    SetStopLoss(5.0);
}

Example: Trend Following Long/Short

Long when all three SMAs are aligned up, short when aligned down. Uses PositionSide to distinguish position direction.

public override void ExecuteStrategy(string ticker = null)
{
    double sma20  = GetSMA(20);
    double sma50  = GetSMA(50);
    double sma200 = GetSMA(200);

    bool longEntry  = sma20 > sma50 && sma50 > sma200;
    bool shortEntry = sma20 < sma50 && sma50 < sma200;
    bool hasLong  = OpenPositions.Any(p => p.Value.PositionSide == 1);
    bool hasShort = OpenPositions.Any(p => p.Value.PositionSide == -1);

    // Exits first
    if (!longEntry  && hasLong)  Sell("Trend reversal exit");
    if (!shortEntry && hasShort) Cover("Trend reversal cover");

    // Entries
    if (longEntry  && !hasLong  && !hasShort) Buy("Bullish alignment");
    if (shortEntry && !hasShort && !hasLong)  Short("Bearish alignment");

    SetStopLoss(5.0);
}

Example: RSI Mean Reversion + Bollinger Bands

public override void ExecuteStrategy(string ticker = null)
{
    double rsi = GetRSI(14);
    var (upper, mid, lower) = GetBollingerBands(20, 2.0);
    double close = Close();
    bool   inPos = OpenPositions.ContainsKey(CurrentTicker);

    // Entry: RSI oversold AND price near lower Bollinger band
    if (rsi < 30 && close < lower * 1.01 && !inPos)
        Buy("RSI oversold + BB lower");

    // Exit: RSI overbought OR price touches upper band
    if (inPos && (rsi > 70 || close > upper * 0.99))
        Sell("RSI overbought or BB upper");

    SetStopLoss(8.0);
}

Example: Multi-Ticker Portfolio

Use DateFirst mode to process all tickers on the same date. Invest a fixed % of equity per trade so positions scale with portfolio growth.

public override void ConfigureSystem()
{
    Config.Tickers = new[] { "AAPL", "MSFT", "GOOGL", "AMZN", "NVDA" };
    Config.FromDate           = new DateTime(2020, 1, 1);
    Config.ToDate             = DateTime.Today;
    Config.StartEquity        = 50_000;
    Config.InvestmentPerTradePercTotalEquity = 20; // 20% of equity per position
    Config.Mode               = BacktestMode.DateFirst;
}

public override void ExecuteStrategy(string ticker = null)
{
    double rsi    = GetRSI(14);
    double sma50  = GetSMA(50);
    double sma200 = GetSMA(200);
    bool   inPos  = OpenPositions.ContainsKey(CurrentTicker);

    if (Close() > sma50 && sma50 > sma200 && rsi > 50 && rsi < 75 && !inPos)
        Buy($"Momentum entry {CurrentTicker}");

    if (inPos && (Close() < sma50 || rsi < 40))
        Sell("Exit: below SMA50 or RSI weak");

    SetStopLoss(6.0);
}
Tip — InvestmentPerTradePercTotalEquity: When set, each trade invests a percentage of current equity instead of a fixed amount. This automatically compounds gains — positions grow as the portfolio grows, and shrink after drawdowns.

Example: Ranked S&P 500 Universe v2

This is a simple v2 validation strategy. It resolves the S&P 500 dynamically, scores every ticker and on weekly rebalance bars buys the 5 highest scores using a unified portfolio.

public override void ConfigureSystem()
{
    Config.Universe = new UniverseFilter
    {
        PartOf = "S&P 500",
        Category = "Stock",
        EnabledOnly = true
    };

    Config.RankedUniverse = new RankedUniverseOptions
    {
        Enabled = true,
        MaxPositions = 5,
        RebalanceFrequency = "Weekly",
        MinScore = 20
    };

    Config.UnifiedPortfolio = true;
    Config.FromDate = new DateTime(2022, 1, 1);
    Config.ToDate = new DateTime(2024, 12, 31);
    Config.StartEquity = 100000;
    Config.InvestmentPerTradePercTotalEquity = 20;
    Config.DefaultExecutionType = ExecutionType.Close;
    Config.DefaultTimeframe = "d";
}

public override void ExecuteStrategy(string ticker = null)
{
    double sma50 = GetSMA(50);
    double sma200 = GetSMA(200);
    double rsi = GetRSI(14);
    double momentum60 = GetChangePercent(60);

    if (sma50 <= 0 || sma200 <= 0)
    {
        ClearScore();
        return;
    }

    double score = 0;
    if (Close() > sma200) score += 10;
    if (sma50 > sma200) score += 10;
    if (rsi >= 50 && rsi <= 70) score += 10;
    score += momentum60;

    if (score >= 20)
        SetScore(score, $"score={score:F1}");
    else
        ClearScore();
}

Example: Trailing Stop + Take Profit

Combines a fixed initial stop loss with a trailing stop that activates once the trade is profitable, plus a take profit target.

public override void ExecuteStrategy(string ticker = null)
{
    double rsi  = GetRSI(14);
    double sma50 = GetSMA(50);
    bool   inPos = OpenPositions.ContainsKey(CurrentTicker);

    // Entry: RSI recovering from oversold, above SMA50
    if (!inPos && rsi > 35 && rsi < 55 && Close() > sma50)
    {
        Buy("RSI recovery above SMA50");
        Log($"Entry RSI={rsi:F1} close={Close():F2}");
        return;
    }

    if (!inPos) return;

    // Take profit at +20%
    if (SetTakeProfit(20.0)) return;

    // Once +5% in profit: switch to 8% trailing stop
    if (currentGainPercent() > 5.0)
    {
        if (SetTrailingStop(8.0)) return;
    }
    else
    {
        // Fixed stop while not yet in profit
        SetStopLoss(4.0);
    }

    // Time exit: close after 30 days regardless
    if (GetDaysInPosition() > 30)
        Sell("Time exit: 30 days");
}

Example: Multi-Timeframe Strategy

Filter entries using weekly trend context, execute on daily signals. The engine loads the weekly data automatically on demand.

public override void ExecuteStrategy(string ticker = null)
{
    // Weekly context: only trade in the direction of the weekly trend
    double weeklySMA20 = GetSMA(20, "w");
    double weeklySMA50 = GetSMA(50, "w");
    double weeklySlope = GetSlopePercent(26, "w");
    bool   weeklyUp    = weeklySMA20 > weeklySMA50 && weeklySlope > 0;

    // Daily entry signal
    double dailyRSI  = GetRSI(14);
    double dailySMA20 = GetSMA(20);
    bool   inPos      = OpenPositions.ContainsKey(CurrentTicker);

    if (!inPos && weeklyUp && dailyRSI < 45 && Close() > dailySMA20)
    {
        Buy("Daily pullback in weekly uptrend");
        return;
    }

    if (inPos)
    {
        SetATRStop(2.0);       // volatility-adaptive stop
        SetTakeProfit(15.0);  // fixed target
        // Exit if weekly trend turns
        if (!weeklyUp) Sell("Weekly trend broken");
    }
}
Multi-timeframe note: The first time GetSMA(20, "w") is called for a ticker, the engine fetches and caches the full weekly history automatically. Subsequent calls in the same backtest are served from memory.
gsTradingEngine C# Strategy Reference v2.0 — gStockly.com © 2026  |  Built on Skender.Stock.Indicators · .NET 8 · Roslyn Compiler  |  Back to gstockly.com  |  Back to top ↑