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.
💻 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);
}
Config.Universe for dynamic ticker resolution and Config.RankedUniverse for top-N ranking strategies with automatic portfolio rotation.
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
| Property | Type | Default | Description |
|---|---|---|---|
Tickers | string[] | — | Ticker symbols to backtest, e.g. new[]{"AAPL","MSFT"} |
FromDate | DateTime | — | Backtest start date |
ToDate | DateTime | — | Backtest end date |
StartEquity | double | 10 000 | Initial portfolio capital in USD |
InvestmentPerTrade | double | 10 000 | Fixed USD amount per trade (capped at available equity) |
InvestmentPerTradePercTotalEquity | double | 0 | If > 0, invest this % of current equity per trade instead of a fixed amount. Enables compounding. |
OpenCommissionValue | double | 0 | Fixed commission per open trade (USD) |
OpenCommissionPerc | double | 0 | Open commission as % of trade value |
CloseCommissionValue | double | 0 | Fixed commission per close trade (USD) |
CloseCommissionPerc | double | 0 | Close commission as % of trade value |
SpreadPricePerc | double | 0.01 | Bid-ask spread % applied to execution price on every order |
PercTax | double | 26 | Capital gains tax % applied to profitable trades on close |
DefaultExecutionType | ExecutionType | Close | Price used for order execution — see table below |
DefaultTimeframe | string | "d" | Default timeframe: "d" daily · "w" weekly · "m" monthly |
Mode | BacktestMode | TickerFirst | Iteration order: TickerFirst or DateFirst |
UnifiedPortfolio | bool | false | Single shared cash account across all tickers. Required for ranked top-N strategies. |
Universe | UniverseFilter? | null | Optional dynamic universe definition resolved from the tickers table. |
RankedUniverse | RankedUniverseOptions | disabled | Optional 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.
| Value | Buy price | Sell price | Use case |
|---|---|---|---|
Close | Close | Close | Default. Signal detected on close, execute at close. |
NextOpen | Next day Open | Next day Open | More realistic: signal on close, execute next morning. |
Average | (High+Low)/2 | (High+Low)/2 | Mid-day average price assumption. |
Pessimistic | High | Low | Worst case: buy at session high, sell at session low. |
Optimistic | Low | High | Best case: buy at session low, sell at session high. |
BacktestMode
| Value | Iteration order | Use case |
|---|---|---|
TickerFirst | For each ticker → iterate all dates | Default. Independent single-asset strategies. |
DateFirst | For each date → iterate all tickers | Multi-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
| Property | Type | Description |
|---|---|---|
Name | string? | Optional label used in logs and debugging. |
PartOf | string? | Index or group membership such as "S&P 500" or "Nasdaq-100". |
Country, Area, Exchange | string? | Geographic and market filters from the tickers table. |
Sector, Industry, Category | string? | Business classification filters. |
BenchmarkTicker, Currency | string? | Additional metadata filters. |
EnabledOnly | bool | Defaults to true. Excludes disabled tickers. |
MinMarketCap | double? | Optional minimum market cap filter. |
Limit | int? | Optional cap on the resolved universe size. |
IncludeTickers, ExcludeTickers | string? | Comma-separated include and exclude lists applied after the dynamic query. |
RankedUniverseOptions
| Property | Type | Description |
|---|---|---|
Enabled | bool | Turns ranked top-N portfolio mode on. |
MaxPositions | int | Maximum number of simultaneous positions selected from the highest scores. |
MinScore | double? | Optional minimum score required for eligibility. |
RebalanceFrequency | string | Daily, Weekly or Monthly. |
ClosePositionsOnRotate | bool | If true, positions that fall out of the top-N set are closed automatically at rebalance. |
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:
| Property | Type | Description |
|---|---|---|
CurrentTicker | string | The ticker currently being evaluated (e.g. "AAPL") |
CurrentDate | DateTime | The date currently being evaluated |
CurrentCandle | Candle? | Full OHLCV candle for the current day. Null if no quote exists. |
NextCandle | Candle? | Next available candle (used internally for NextOpen execution) |
OpenPositions | Dictionary<string, TradePosition> | Currently open trades, keyed by ticker symbol |
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
| Helper | Returns | Description |
|---|---|---|
SetScore(score, note?, ticker?) | void | Assigns a cross-sectional score to the current ticker or to an explicitly provided ticker. |
ClearScore(ticker?) | void | Removes 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?) | bool | True if a score has already been registered for the ticker in the current bar. |
IsRankedUniverseMode() | bool | True when the engine is running in ranked top-N mode. |
UniverseTickers | string[] | 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
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.
| Parameter | Type | Description |
|---|---|---|
reason | string? | Label saved in the trade record (e.g. "SMA20 > SMA50"). Optional. |
executionType | ExecutionType? | Overrides the default execution type for this single order. Optional. |
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(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).
Cover(string? reason = null, ExecutionType? executionType = null)
Closes an open short position on CurrentTicker.
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;
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.
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.
| Parameter | Type | Description |
|---|---|---|
pctProfit | double | Target profit % from entry price |
executionType | ExecutionType? | 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);
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.
SetTrailingStop is called. No extra state needed in the strategy code.| Parameter | Description |
|---|---|
pctTrail | Distance % 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
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.
| Parameter | Default | Description |
|---|---|---|
multiplier | 2.0 | ATR multiplier. 1.5 = tight, 2.0 = standard, 3.0 = wide |
atrPeriod | 14 | Period 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
| Property | Type | Description |
|---|---|---|
OpenDate | DateTime | Date the position was opened |
OpenPrice | double | Actual execution price at entry (after spread applied) |
Shares | int | Number of shares. Positive = long, negative = short. |
OpenPosition | double | Total invested amount (Shares × OpenPrice) |
OpenCommission | double | Commission paid at entry |
OpenPositionNote | string | The reason string passed to Buy() or Short() |
PositionSide | int | 1 = Long, -1 = Short (derived from Shares sign) |
Equity & Gain Helpers
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.
double currentGainPercent()
Unrealised gain % of the current open position on CurrentTicker, accounting for spread and close commission. Returns 0 if no position is open.
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()
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.
| Method | Returns | Description |
|---|---|---|
Close(ticker?) | double | Close price of the current day |
Open(ticker?) | double | Open price of the current day |
High(ticker?) | double | Session high of the current day |
Low(ticker?) | double | Session low of the current day |
Volume(ticker?) | double | Volume of the current day |
GetCandle(offset, tf?, ticker?) | Candle? | Full OHLCV candle N periods back (0 = current, -1 = previous) |
GetClose(offset, tf?, ticker?) | double | Close N periods back |
GetOpen(offset, tf?, ticker?) | double | Open N periods back |
GetHigh(offset, tf?, ticker?) | double | High N periods back |
GetLow(offset, tf?, ticker?) | double | Low N periods back |
GetVolume(offset, tf?, ticker?) | double | Volume 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
| Method | Returns |
|---|---|
GetCurrentDayOfWeek() | DayOfWeek — Monday, Tuesday… |
GetCurrentDay() | int — day of month (1–31) |
GetCurrentMonth() | int — month (1–12) |
GetCurrentYear() | int — year |
GetChangePercent()
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()
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()
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()
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.
| Method | Returns | Description |
|---|---|---|
GetSMA(period, tf?, pt?, t?) | double | Simple Moving Average |
GetEMA(period, tf?, pt?, t?) | double | Exponential Moving Average |
GetWMA(period, t?) | double | Weighted Moving Average |
GetDEMA(period, t?) | double | Double EMA |
GetTEMA(period, t?) | double | Triple EMA |
GetHMA(period, t?) | double | Hull Moving Average |
GetVWMA(period, t?) | double | Volume-Weighted Moving Average |
GetVWAP(t?) | double | Volume-Weighted Average Price |
Oscillators
| Method | Returns | Notes |
|---|---|---|
GetRSI(period=14, tf?, t?) | double | 0–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?) | double | Average 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?) | double | Commodity Channel Index |
GetROC(period=10, t?) | double | Rate of Change % |
GetMFI(period=14, t?) | double | Money Flow Index, 0–100 |
GetCMF(period=20, t?) | double | Chaikin Money Flow, –1 to 1 |
GetMomentum(period=10, t?) | double | Price 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
| Method | Returns | Notes |
|---|---|---|
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
| Method | Returns | Description |
|---|---|---|
GetOBV(t?) | double | On-Balance Volume — cumulative |
GetAvgVolume(period=20, t?) | double | Simple average volume over N periods |
Other Indicators
| Method | Returns | Description |
|---|---|---|
GetHighest(period, t?) | double | Highest High over the last N bars |
GetLowest(period, t?) | double | Lowest 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);
}
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");
}
}
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.