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. |
UpdateQuotes | bool | true | Paper/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(). |
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. |
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 policy | Best use | Network calls per cycle | Strategy action |
|---|---|---|---|
Config.UpdateQuotes = true | Small universe where every ticker must be current. | One realtime request per deployment ticker. | Use the initial current candles directly. |
Config.UpdateQuotes = false | Large 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. |
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.
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:
| 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();
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.
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:
- Read a coherent market and portfolio snapshot.
- Evaluate the strategy through
ILiveStrategyEvaluator. - Produce one or more
TradingOrderIntentobjects. - Validate each intent through the safety guard.
- Submit it to the configured paper simulator or broker gateway.
- Persist the result so the same intent cannot be submitted twice.
Live contracts
| Contract | Responsibility |
|---|---|
ILiveMarketDataFeed | Returns the current timestamp, candles, positions and available cash. |
ILiveStrategyEvaluator | Evaluates the strategy against one snapshot and returns order intentions. |
ILiveOrderGateway | Reads broker state and submits orders. A broker-specific implementation is required. |
ILiveTradingStateStore | Stores submitted intents and broker results for restart recovery and deduplication. |
LiveTradingSafetyGuard | Checks quantity, order value, gross exposure, maximum positions and short-selling permission. |
LiveTradingSession | Runs one cycle with RunOnceAsync() or a continuous loop with RunAsync(). |
TradingRepository: that repository is designed for buffered backtest results.
Current broker and feed status
| Component | Current status | Live implication |
|---|---|---|
| DEGIRO local broker bridge | Portfolio 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 Brokers | The 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 feed | Use 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. |
AllowLiveOrders=true confirmation and an active kill switch policy.
Recommended rollout
| Stage | Purpose |
|---|---|
| 1 Backtest | Validate the strategy on historical data and inspect trades, drawdown and exposure. |
| 2 Paper | Run the same decision flow on current data without sending broker orders. |
| 3 Live with limits | Use small exposure, restricted tickers, short selling disabled and a kill switch. |
| 4 Scale gradually | Increase 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.
| Mode | Data | Portfolio and execution | Broker order | Purpose |
|---|---|---|---|---|
| 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 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
| Rule | Backtest | Paper | Live |
|---|---|---|---|
| Uses historical date range | Yes | No | No |
| Can use realtime polling | No | Yes — every ticker or selected tickers | Yes — every ticker or selected tickers |
| Uses real broker positions | No | Optional/read-only | Required |
| Can submit a real order | No | No | Only with an enabled gateway and explicit confirmation |
| Requires reconciliation after execution | No | Recommended | Required |
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.
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 setting | Meaning |
|---|---|
Trading System | The saved strategy code and version used by this runtime instance. |
Execution mode | Paper simulates fills; Live requires a verified broker gateway and explicit activation. |
Ticker universe override | Optional. If empty, the deployment uses Config.Tickers from the saved Trading System. Use an override only when reusing the strategy on another universe. |
Account ID | Internal account identifier. For the current isolated Paper Gateway, use 0. It is not a broker password or login. |
Refresh interval | How 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.
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
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/ts/deployments | List deployments and status. |
POST | /api/ts/deployments | Create a deployment. |
PUT | /api/ts/deployments/{id} | Edit a deployment and persist the updated configuration. |
POST | /api/ts/deployments/{id}/start | Start one worker. |
POST | /api/ts/deployments/{id}/stop | Stop one worker. |
PATCH | /api/ts/deployments/{id}/refresh | Update the refresh interval. |
GET | /api/ts/deployments/{id}/events | Read recent cycles and order results. |
GET | /api/ts/deployments/{id}/portfolio | Read the persisted Paper cash balance, positions and fills. |
DELETE | /api/ts/deployments/{id} | Delete the deployment and its persisted events. |
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 |
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.
GetTradingSignals() reads signals for CurrentTicker and CurrentDate. The result is cached for the current engine run.| Method | Returns | Description |
|---|---|---|
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()
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.