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.
UpdateQuotesbooltruePaper/Live only: choose the current-candle source for the initial snapshot. true polls every deployment ticker and preloads history; false reads the latest persisted candle in one DB snapshot, then loads prior bars/indicators only for screened candidates and allows selective realtime refresh with RequestRealtimeQuotes().
UniverseUniverseFilter?nullOptional dynamic universe definition resolved from the tickers table.
RankedUniverseRankedUniverseOptionsdisabledOptional top-N ranking mode with rebalance frequency and minimum score filters.

Selective realtime quote refresh

Paper and Live deployments have two initial-snapshot modes. The default, Config.UpdateQuotes = true, polls the realtime endpoint once for every ticker in the deployment and preloads the configured historical range. Set Config.UpdateQuotes = false for a broad universe: the runtime reads only the latest persisted candle for all tickers with a batched database query. A prior-bar helper such as GetClose(-1) reads just the needed persisted bar; EMA, RSI and other indicators lazily load the historical series only after the strategy has retained that ticker as a candidate. During ExecuteStrategy(), call RequestRealtimeQuotes(ticker, ...) only after the inexpensive cached screening has retained a candidate. The call waits for the selected quote, replaces that ticker's current candle, invalidates/recalculates its indicators and then continues in the same strategy call. Scanner signals are also fetched in batches for the current snapshot. Use IsRealtimeQuote(ticker) to verify the refresh. Backtests never access realtime data.

public override void ConfigureSystem()
{
    Config.UpdateQuotes = false;
}

public override void ExecuteStrategy(string ticker = null)
{
    if (!CachedCandidateCondition()) return;
    RequestRealtimeQuotes(CurrentTicker);
    if (!IsRealtimeQuote(CurrentTicker)) return;
    if (RealtimeConditionIsValid()) Buy("Realtime confirmation");
}
Initial quote policyBest useNetwork calls per cycleStrategy action
Config.UpdateQuotes = trueSmall universe where every ticker must be current.One realtime request per deployment ticker.Use the initial current candles directly.
Config.UpdateQuotes = falseLarge universe (hundreds or thousands of tickers) with an initial DB-based screen.Zero initial quote requests; only the tickers explicitly passed to RequestRealtimeQuotes().Screen on the cached current candle and batched scanner signals; history is loaded lazily for surviving candidates, then refresh candidates and every open position before acting.
Safety: realtime requests are selective and subject to the deployment cycle timeout. In cache-first mode, refresh open positions before applying stops, targets or any exit that depends on the current price. A cached candle is not a replacement for a realtime execution check.

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.

Operational observability

The Trading Console exposes more than the deployment status. Select a deployment and use the dedicated tabs to inspect the strategy’s actual runtime behavior:

  • Decisions: one record per ticker evaluation, including no-action decisions, reference price, side, quantity and strategy reason.
  • Trades: Paper fills with ticker, side, quantity, fill price and realized P&L. Selecting a trade loads the OHLC chart and overlays entry/exit markers.
  • Performance: current equity, realized and unrealized P&L, return, win rate, profit factor, maximum drawdown and the Paper equity curve.

The corresponding endpoints are /api/ts/deployments/{id}/decisions, /trades and /performance. Decision rows are stored in ts_deployment_decision; Paper equity snapshots are stored in ts_paper_equity and survive service restarts.

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();

Execution Runtimes: Backtest, Paper and Live

The engine separates strategy logic from the runtime that applies it. The same strategy concepts can therefore be reused across historical testing, simulated execution and — once a broker adapter is configured — real-time trading.

Backtest

Runs the strategy against historical OHLCV data using the virtual portfolio and performance metrics.

Paper

Uses the live runtime with simulated order execution. No broker order is sent.

Live

Uses a dedicated market-data feed and broker gateway. This mode is not enabled by the backtest endpoint.

Important: Paper and Live are not alternate backtest settings. The backtest endpoint accepts only Backtest and explicitly rejects the other modes, so a live configuration cannot accidentally be executed as a historical simulation.

Runtime pipeline

Market-data snapshot
        ↓
ILiveStrategyEvaluator
        ↓
TradingOrderIntent
        ↓
LiveTradingCoordinator
        ↓
Safety checks + idempotency
        ↓
Paper simulator or broker gateway

The live cycle is implemented by LiveTradingSession. Each cycle is serial and follows the same order:

  1. Read a coherent market and portfolio snapshot.
  2. Evaluate the strategy through ILiveStrategyEvaluator.
  3. Produce one or more TradingOrderIntent objects.
  4. Validate each intent through the safety guard.
  5. Submit it to the configured paper simulator or broker gateway.
  6. Persist the result so the same intent cannot be submitted twice.

Live contracts

ContractResponsibility
ILiveMarketDataFeedReturns the current timestamp, candles, positions and available cash.
ILiveStrategyEvaluatorEvaluates the strategy against one snapshot and returns order intentions.
ILiveOrderGatewayReads broker state and submits orders. A broker-specific implementation is required.
ILiveTradingStateStoreStores submitted intents and broker results for restart recovery and deduplication.
LiveTradingSafetyGuardChecks quantity, order value, gross exposure, maximum positions and short-selling permission.
LiveTradingSessionRuns one cycle with RunOnceAsync() or a continuous loop with RunAsync().
Live trading is not yet broker-enabled. The current implementation provides the runtime boundary and safety contracts, but it does not send orders until an explicit broker gateway, realtime data feed and persistent state store are configured. Do not connect a broker directly to TradingRepository: that repository is designed for buffered backtest results.

Current broker and feed status

ComponentCurrent statusLive implication
DEGIRO local broker bridgePortfolio polling, local-agent snapshots, conditional rules and sell_position are available.Exit execution is available through the existing bridge path; generic Buy, Short and Cover gateway support is not yet exposed.
Interactive BrokersThe current connector checks local TWS/IB Gateway reachability and detects the official API installation.It is still a foundation connector: portfolio reads and order submission are not enabled.
Realtime quote feedUse the existing polling endpoint /api/quotes.php?action=realtime&ticker=....The response supplies the current price, quote timestamp and current candle. Historical data remains necessary for indicator calculations.
Capability-aware execution: before a live order is submitted, the coordinator checks the gateway capabilities for the requested side. Unsupported operations are rejected and logged instead of being silently simulated or sent to the wrong broker path. Live mode also requires the explicit AllowLiveOrders=true confirmation and an active kill switch policy.

Recommended rollout

StagePurpose
1 BacktestValidate the strategy on historical data and inspect trades, drawdown and exposure.
2 PaperRun the same decision flow on current data without sending broker orders.
3 Live with limitsUse small exposure, restricted tickers, short selling disabled and a kill switch.
4 Scale graduallyIncrease exposure only after reconciliation, restart and order-failure scenarios are verified.

Live Trading

Live Trading applies the strategy to current market data and can submit orders to a configured broker. It is a separate operational runtime from historical backtesting and paper trading.

ModeDataPortfolio and executionBroker orderPurpose
Backtest Historical OHLCV candles between FromDate and ToDate. Virtual portfolio, simulated prices, spread, commissions and tax. Never Measure historical behaviour, risk and performance.
Paper Trading Current data from either the realtime polling feed or the persisted market-data snapshot, according to Config.UpdateQuotes. Paper portfolio and simulated order fills using the live decision cycle. Never Verify that current signals, sizing and operational timing behave correctly.
Live Trading Current data from either the realtime polling feed or the persisted market-data snapshot, according to Config.UpdateQuotes. Actual broker positions, cash, fills and reconciliation. Possible, only through an enabled broker gateway Operate the strategy on the real portfolio.
Live Trading is not a faster backtest. It has a different lifecycle: the engine must read the actual portfolio, detect current market data, generate a decision, validate risk limits, submit or reject the order, and reconcile the broker result. A strategy signal is not the same thing as a filled order.

Live Trading cycle

1. Poll current quotes and candles
2. Read broker positions and available cash
3. Evaluate the strategy on the current snapshot
4. Create a Buy / Sell / Short / Cover intent
5. Check broker capabilities and safety limits
6. Submit the order only in Live mode
7. Persist the intent and broker result
8. Reconcile the next snapshot

Mode selection rules

RuleBacktestPaperLive
Uses historical date rangeYesNoNo
Can use realtime pollingNoYes — every ticker or selected tickersYes — every ticker or selected tickers
Uses real broker positionsNoOptional/read-onlyRequired
Can submit a real orderNoNoOnly with an enabled gateway and explicit confirmation
Requires reconciliation after executionNoRecommendedRequired
Recommended rollout: run the strategy in Backtest first, then Paper Trading with the same ticker list and sizing rules, and only then enable Live Trading with restricted exposure, short selling disabled initially and an active kill switch.
Current integration status: the existing DEGIRO bridge supports portfolio snapshots and sell-side close commands. The current Interactive Brokers connector is a connectivity foundation and does not yet submit orders. Unsupported live order sides are rejected explicitly.

Trading Console: multiple deployments

The Trading Console is the operational surface for running more than one configured instance of a Trading System. A deployment pins a saved strategy version to its execution mode, ticker universe, account, capital, schedule and refresh interval.

One strategy, many deployments: the same saved Trading System can run separately on different markets or universes, for example a Paper deployment for COMEX:SI1! and COMEX:GC1!, and another deployment for European equities.

Deployment definitions are persisted in the master database. After an engine restart or VPS reboot, the Trading Console loads the saved deployments again; deployments whose Enabled flag is active are restored and started automatically. Recent cycle, signal and order events are persisted as well.

Paper accounts are isolated per deployment and persisted in the master database. The Portfolio tab reads the virtual cash balance, open positions, market values, unrealized P&L and recent Paper fills from /api/ts/deployments/{id}/portfolio. A restart therefore restores the Paper portfolio instead of starting with an empty account.

The polling feed is tolerant of partial failures: if one ticker returns an HTTP or payload error, that ticker is skipped and the valid tickers continue through the cycle. The problem is recorded as a warning event. The deployment enters Error only when no valid ticker remains.

Deployment settingMeaning
Trading SystemThe saved strategy code and version used by this runtime instance.
Execution modePaper simulates fills; Live requires a verified broker gateway and explicit activation.
Ticker universe overrideOptional. If empty, the deployment uses Config.Tickers from the saved Trading System. Use an override only when reusing the strategy on another universe.
Account IDInternal account identifier. For the current isolated Paper Gateway, use 0. It is not a broker password or login.
Refresh intervalHow often the worker reads a new market snapshot and evaluates the strategy.

Refresh interval

The console offers controlled values from 15 seconds to 60 minutes: 15 seconds, 30 seconds, 1 minute, 5 minutes, 15 minutes, 30 minutes and 60 minutes. The interval is configured independently for each deployment and can be changed directly from the deployment grid.

Choose the interval together with the timeframe. Polling a daily strategy every 15 seconds may repeatedly evaluate the same daily candle. This is useful only when the current candle is being updated intraday; it does not create a new historical bar. Short intervals also increase API, CPU and broker-gateway load when many deployments run at once.

Deployment lifecycle

1. Select a saved Trading System and version
2. Choose Paper or Live mode
3. Keep the ticker override empty to use Config.Tickers, or provide an override
4. Configure account, capital, schedule and refresh interval
5. Create the deployment
6. Start or stop it from the grid
7. Inspect cycles, signals and order results in Recent events

Stopping a deployment stops new evaluation cycles and disables automatic restart. It does not close existing positions. Closing positions is a separate operational action.

Deployment API

MethodEndpointPurpose
GET/api/ts/deploymentsList deployments and status.
POST/api/ts/deploymentsCreate a deployment.
PUT/api/ts/deployments/{id}Edit a deployment and persist the updated configuration.
POST/api/ts/deployments/{id}/startStart one worker.
POST/api/ts/deployments/{id}/stopStop one worker.
PATCH/api/ts/deployments/{id}/refreshUpdate the refresh interval.
GET/api/ts/deployments/{id}/eventsRead recent cycles and order results.
GET/api/ts/deployments/{id}/portfolioRead the persisted Paper cash balance, positions and fills.
DELETE/api/ts/deployments/{id}Delete the deployment and its persisted events.

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

Trading Signals

The engine can read the existing trading_signals table while evaluating the current ticker and date. The signal model exposes the table metadata, including StrategyName, StrategyCode, Timeframe, ClosePrice and the raw ExtraData JSON payload.

Scope: GetTradingSignals() reads signals for CurrentTicker and CurrentDate. The result is cached for the current engine run.
MethodReturnsDescription
GetTradingSignalTickers()string[]Distinct, ordered tickers currently present in trading_signals. Useful for dynamic universes.
GetTradingSignalTickers(strategyName, ...)string[]Restricts the dynamic universe to one or more exact strategy_name values. Use this for a focused scanner-driven live deployment.
GetTradingSignals()List<TradingSignalRecord>All signals for the current ticker and date.
GetTradingSignals(strategyName, strategyCode?)List<TradingSignalRecord>Filters the current signals case-insensitively by strategy_name and optionally strategy_code.
GetTradingSignalExtraData(signal)JsonElement?Safely parses the signal's extra_data JSON object.
GetTradingSignalExtraValue<T>(signal, key)T?Reads a typed, case-insensitive value from extra_data. Returns null when missing or not convertible.
public override void ConfigureSystem()
{
    // Restrict the live universe to the setup this system trades
    Config.Tickers = GetTradingSignalTickers(
        "Near EMA 50 daily",
        "Near EMA 50 weekly");
}

public override void ExecuteStrategy(string ticker = null)
{
    var signals = GetTradingSignals(
        "Pre-Breakout Bollinger Squeeze - Balanced",
        "pre_breakout_squeeze_balanced");

    var signal = signals.FirstOrDefault();
    if (signal is null) return;

    var rsi = GetTradingSignalExtraValue<double>(signal, "RSI");
    var bollWidth = GetTradingSignalExtraValue<double>(signal, "BollWidth");

    if (rsi is double rsiValue && bollWidth is double width && rsiValue < 55 && width < 5)
        Buy("Signal confirmed from trading_signals");
}

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 ↑