QuanTAlib is platform-agnostic. Any .NET environment that can reference a DLL can use it. The complexity lies not in the library but in understanding each platform's quirks.
Quantower accepts custom indicators written in C#. Integration follows a wrapper pattern.
- Build QuanTAlib or grab the NuGet package
- Add reference to
QuanTAlib.dllin the Quantower indicator project - Create wrapper class inheriting from
Indicator
using Quantower.API.Indicators;
using QuanTAlib;
public class MySmaIndicator : Indicator
{
private Sma _sma;
[InputParameter("Period", 10, 1000, 1, 0)]
public int Period = 14;
public override void OnInit()
{
_sma = new Sma(Period);
AddLineSeries("SMA", Color.Yellow, LineStyle.Solid, 2);
}
public override void OnUpdate(UpdateArgs args)
{
double price = ClosePrice;
// Quantower handles bar lifecycle; check UpdateReason
bool isNew = args.Reason == UpdateReason.NewBar;
var result = _sma.Update(new TValue(DateTime.UtcNow, price), isNew);
SetValue(result.Value);
}
}Pre-built adapters exist for common indicators:
| Bundle | Indicators | Notes |
|---|---|---|
| Trends (IIR) | HemaIndicator, ZlemaIndicator, EmaIndicator, etc. | Exponential family |
| Trends (FIR) | SmaIndicator, WmaIndicator, HmaIndicator, etc. | Finite response family |
| Volatility | AtrIndicator, AdrIndicator | Range-based volatility |
| Dynamics | AdxIndicator, SuperTrendIndicator | Trend strength |
UpdateReason matters. Quantower calls OnUpdate for both new bars and intra-bar ticks. The args.Reason check determines isNew flag behavior. Getting this wrong causes state corruption that manifests as mysteriously wrong indicator values.
Historical data loads first. Quantower calls OnUpdate repeatedly during historical load before live data arrives. The indicator warms up during this phase.
NinjaTrader 8 runs on .NET Framework 4.8. QuanTAlib targets .NET Standard, enabling interop.
- Copy
QuanTAlib.dlltoDocuments\NinjaTrader 8\bin\Custom - In NinjaScript Editor: right-click ’ References ’ Add
QuanTAlib.dll
private QuanTAlib.Sma _sma;
[Range(1, int.MaxValue)]
[NinjaScriptProperty]
public int Period { get; set; } = 14;
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
Name = "QuanTAlib SMA";
Calculate = Calculate.OnBarClose;
}
else if (State == State.DataLoaded)
{
_sma = new QuanTAlib.Sma(Period);
}
}
protected override void OnBarUpdate()
{
// isNew depends on Calculate mode
// OnBarClose: every call is a new bar
// OnEachTick: use IsFirstTickOfBar
bool isNew = Calculate == Calculate.OnBarClose || IsFirstTickOfBar;
var result = _sma.Update(new TValue(Time[0], Close[0]), isNew);
Value[0] = result.Value;
}Calculate mode affects isNew logic. With Calculate.OnBarClose, every OnBarUpdate call represents a completed bar. With Calculate.OnEachTick, only the first tick of each bar should use isNew = true. Mixing these concepts produces indicators that work in backtest but fail live.
Historical vs real-time. NinjaTrader processes historical bars differently from real-time bars. The State property indicates the current phase. Indicator warmup should complete during historical processing.
LEAN supports custom libraries through NuGet integration.
- Add
QuanTAlibto project dependencies - Instantiate indicators in
Initialize() - Update in
OnData()
public class MyAlgorithm : QCAlgorithm
{
private Sma _mySma;
private Symbol _symbol;
public override void Initialize()
{
SetStartDate(2020, 1, 1);
SetEndDate(2023, 12, 31);
SetCash(100000);
_symbol = AddEquity("SPY", Resolution.Daily).Symbol;
_mySma = new Sma(14);
}
public override void OnData(Slice data)
{
if (!data.Bars.ContainsKey(_symbol))
return;
var bar = data.Bars[_symbol];
var result = _mySma.Update(new TValue(bar.EndTime, (double)bar.Close));
if (_mySma.IsHot)
{
Plot("Indicators", "SMA", result.Value);
// Trading logic here
if (!Portfolio[_symbol].Invested && result.Value < (double)bar.Close)
{
SetHoldings(_symbol, 0.5);
}
}
}
}Decimal to double conversion. LEAN uses decimal for prices; QuanTAlib uses double. Cast on input, cast back on output if needed. The precision difference rarely matters for indicator calculations.
Resolution affects bar timing. Daily bars have different EndTime semantics than minute bars. UTC timestamps prevent timezone confusion.
For proprietary trading engines, Streaming Mode fits most use cases.
| Consideration | Requirement | Consequence of Ignoring |
|---|---|---|
| Time handling | UTC timestamps | Timezone bugs in historical analysis |
| Numeric precision | double input/output |
Cast from/to decimal if platform uses it |
| State persistence | One instance per symbol | Recreating indicators loses warmup state |
| Thread safety | Separate instances per thread | Concurrent access corrupts internal state |
| Bar correction | Proper isNew flag usage |
State accumulation errors |
public class MyTradingEngine
{
// One indicator instance per symbol, persisted for session lifetime
private readonly Dictionary<string, Sma> _indicators = new();
public void OnSymbolAdded(string symbol, int smaPeriod)
{
_indicators[symbol] = new Sma(smaPeriod);
}
public void OnTick(string symbol, DateTime time, double price, bool isNewBar)
{
if (!_indicators.TryGetValue(symbol, out var sma))
return;
var result = sma.Update(new TValue(time, price), isNewBar);
if (sma.IsHot)
{
// Use result.Value for trading logic
ProcessSignal(symbol, result.Value, price);
}
}
public void OnSymbolRemoved(string symbol)
{
_indicators.Remove(symbol);
}
}The isNew flag determines whether Update() advances to the next bar or corrects the current one. Correct implementation depends on data source semantics:
| Data Source Type | isNew = true When | isNew = false When |
|---|---|---|
| Bar-based feed | New bar arrives | Never (each bar final) |
| Tick-based, bar aggregation | First tick after bar close | Subsequent ticks within bar |
| Streaming with corrections | Timestamp advances | Same timestamp, updated price |
Testing approach: Feed identical data through the indicator in streaming mode (tick by tick with correct isNew flags) and batch mode (complete series at once). Compare final values. Mismatch indicates isNew flag logic error.