#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Windows.Media;
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.Gui.Tools;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators;
using NinjaTrader.NinjaScript.Strategies;
#endregion
// Do not change namespace
namespace NinjaTrader.NinjaScript.Strategies
{
public class ChatGPT_LogTick_ATR : Strategy
{
private ATR atr;
private EMA ema;
// shading
private Brush timeShadeBrush;
private readonly Brush timeShadeClear = null;
// ===== per-entry container (one TradeID per entry fill) =====
private class ManagedTrade
{
public string TradeId; // UNIQUE: entry OrderId
public string EntryOrderId;
public Order StopLossOrder;
public Order ProfitTargetOrder;
public double EntryPrice;
public int EntryQty;
public string Side; // "LONG"/"SHORT"
public double AtrAtEntryPts;
public double StopAtrMultAtEntry;
public double TargetAtrMultAtEntry;
public bool IsOpen;
public ManagedTrade(string tradeId, string entryOrderId, double entryPrice, int qty, string side,
double atrPts, double stopK, double tgtK)
{
TradeId = tradeId;
EntryOrderId = entryOrderId;
EntryPrice = entryPrice;
EntryQty = qty;
Side = side;
AtrAtEntryPts = atrPts;
StopAtrMultAtEntry = stopK;
TargetAtrMultAtEntry = tgtK;
StopLossOrder = null;
ProfitTargetOrder = null;
IsOpen = true;
}
}
public enum EntryMode
{
BarExtremaLimit,
OneTickBreak,
FibonacciRetracement
}
private readonly List<ManagedTrade> openTrades = new List<ManagedTrade>();
private readonly List<Order> pendingEntryOrders = new List<Order>();
private int entrySeq = 0;
// one-bar-valid working entry (entry mechanics only)
private Order workingEntryOrder = null;
private int workingOrderBar = -1;
private int lastProcessedBar = -1;
// CSV: buffered logging
private StreamWriter logWriter = null;
private string sessionCsvPath = null;
private readonly List<string> logBuffer = new List<string>();
// flatten-at-time tracker
private DateTime lastFlattenSessionDate = DateTime.MinValue;
// ===== Ninja States =====
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
Description = "Unmanaged; stacked entries; logs per-tick Equity per ENTRY fill.";
Name = "ChatGPT_LogTick_ATR";
IsUnmanaged = true;
Calculate = Calculate.OnEachTick;
EntriesPerDirection = 100;
// Manual control only
IsExitOnSessionCloseStrategy = false;
ExitOnSessionCloseSeconds = 30;
IsFillLimitOnTouch = false;
MaximumBarsLookBack = MaximumBarsLookBack.Infinite;
OrderFillResolution = OrderFillResolution.Standard;
Slippage = 1;
StartBehavior = StartBehavior.WaitUntilFlat;
TimeInForce = TimeInForce.Gtc;
// Diagnostics ON
TraceOrders = true;
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
BarsRequiredToTrade = 20;
IsInstantiatedOnEachOptimizationIteration = true;
FourBarSetup = true;
FiveBarSetup = false;
SixBarSetup = false;
AtrPeriod = 21;
RiskPerTrade = 350;
MaxContracts = 15;
TargetMultiplier = 15.0; // xATR
StopLossMultiplier = 3.0; // xATR
EntryType = EntryMode.BarExtremaLimit;
FibRetracePct = 0.24;
UseEmaFilter = false;
EmaPeriod = 17;
TimeFilter = true;
WindowStart = "07:00:00";
WindowEnd = "08:45:00";
ShowTimeShade = true;
ShadeOpacity = 32;
FlattenAtTime = true;
FlattenTime = "14:30:00";
LogFolder = @"C:\NT_Exports";
CsvFilePrefix = "Gemini_EquityLog_ATR";
}
else if (State == State.Configure)
{
AddDataSeries(BarsPeriodType.Tick, 1);
}
else if (State == State.DataLoaded)
{
atr = ATR(AtrPeriod);
ema = EMA(EmaPeriod);
AddChartIndicator(ema);
byte a = (byte)Math.Max(0, Math.Min(255, ShadeOpacity));
var b = new SolidColorBrush(Color.FromArgb(a, 128, 128, 128)); b.Freeze();
timeShadeBrush = b;
try
{
if (!Directory.Exists(LogFolder)) Directory.CreateDirectory(LogFolder);
string inst = Instrument?.MasterInstrument?.Name ?? "Instrument";
string ts = DateTime.Now.ToString("yyyyMMdd_HHmmss");
sessionCsvPath = Path.Combine(LogFolder, $"{CsvFilePrefix}_{inst}_{ts}.csv");
logWriter = new StreamWriter(sessionCsvPath, append:false);
// header once
logWriter.WriteLine("TradeID,Time,LastPrice,RealizedPnL,UnrealizedPnL,Equity,EntryPrice,ATR_at_entry_pts,Side,EntryQty,Stop_ATR_mult,Target_ATR_mult");
logWriter.Flush();
Print($"[DIAG] Opened CSV at {sessionCsvPath}");
}
catch (Exception ex)
{
Print("[ChatGPT_LogTick_ATR] Could not open CSV: " + ex.Message);
}
}
else if (State == State.Terminated || State == State.Finalized)
{
try
{
FlushBufferAll(); // final write
}
catch { }
finally
{
try { if (logWriter != null) { logWriter.Close(); logWriter.Dispose(); } } catch { }
}
}
}
// ===== OnBarUpdate =====
protected override void OnBarUpdate()
{
// === TICK stream: manage stops/BE every tick + buffer logs ===
if (BarsInProgress == 1)
{
// DIAG: show that BIP=1 is alive even if we have no open trades (once per new bar)
if (openTrades.Count == 0 && IsFirstTickOfBar)
Print($"[DIAG] {Times[1][0]:O} BIP=1 tick path alive; openTrades=0 (no per-tick rows).");
// Flatten-at-time (once per day, at/after threshold)
if (FlattenAtTime)
{
TimeSpan ft = ParseTimeSpan(FlattenTime);
if (ft != TimeSpan.MinValue)
{
DateTime tNow = Times[1][0];
DateTime sessionDate = tNow.Date;
if (tNow.TimeOfDay >= ft && sessionDate != lastFlattenSessionDate)
{
Print($"[DIAG] FlattenAtTime triggered @ {tNow:O}");
for (int i = openTrades.Count - 1; i >= 0; i--)
{
var mt = openTrades[i];
if (!mt.IsOpen) continue;
try { if (mt.ProfitTargetOrder != null) CancelOrder(mt.ProfitTargetOrder); } catch {}
try { if (mt.StopLossOrder != null) CancelOrder(mt.StopLossOrder); } catch {}
string oco = "OCO-" + mt.EntryOrderId;
if (mt.Side == "LONG")
SubmitOrderUnmanaged(1, OrderAction.Sell, OrderType.Market, mt.EntryQty, 0, 0, oco, "FLAT");
else
SubmitOrderUnmanaged(1, OrderAction.BuyToCover, OrderType.Market, mt.EntryQty, 0, 0, oco, "FLAT");
}
lastFlattenSessionDate = sessionDate;
}
}
}
// buffer logs per open trade per tick
if (openTrades.Count > 0)
{
double lastPrice = Closes[1][0];
string tIso = Times[1][0].ToString("o");
foreach (var tr in openTrades)
{
if (!tr.IsOpen) continue;
double trade_unrealized = (tr.Side == "LONG")
? (lastPrice - tr.EntryPrice) * tr.EntryQty * Instrument.MasterInstrument.PointValue
: (tr.EntryPrice - lastPrice) * tr.EntryQty * Instrument.MasterInstrument.PointValue;
double trade_realized = 0;
double trade_equity = trade_realized + trade_unrealized;
logBuffer.Add(
$"{tr.TradeId},{tIso},{lastPrice:F2},{trade_realized:F2},{trade_unrealized:F2},{trade_equity:F2},{tr.EntryPrice:F2},{tr.AtrAtEntryPts:F4},{tr.Side},{tr.EntryQty},{tr.StopAtrMultAtEntry:F3},{tr.TargetAtrMultAtEntry:F3}"
);
}
// DIAG: periodic flush so you can see rows while running
FlushBufferIfNeeded();
}
return;
}
// === PRIMARY series: evaluate entries once per new bar ===
if (BarsInProgress != 0) return;
if (CurrentBar < Math.Max(BarsRequiredToTrade, 6)) return;
// cancel last bar's unfilled entry at new bar open (entry mechanics only)
if (IsFirstTickOfBar && workingEntryOrder != null && CurrentBar > workingOrderBar)
{
Print($"[DIAG] Cancelling unfilled workingEntryOrder {workingEntryOrder?.Name} from prior bar.");
try { CancelOrder(workingEntryOrder); } catch {}
workingEntryOrder = null;
workingOrderBar = -1;
}
// true once-per-bar
if (CurrentBar == lastProcessedBar) return;
lastProcessedBar = CurrentBar;
// time filter
if (TimeFilter)
{
DateTime barTime = Time[0];
TimeSpan startTs = ParseTimeSpan(WindowStart);
TimeSpan endTs = ParseTimeSpan(WindowEnd);
bool within = true;
if (startTs != TimeSpan.MinValue && endTs != TimeSpan.MinValue)
{
DateTime start = barTime.Date + startTs;
DateTime end = barTime.Date + endTs;
within = (barTime >= start && barTime <= end);
}
if (ShowTimeShade) BackBrushes[0] = within ? timeShadeBrush : timeShadeClear;
if (!within)
{
if (IsFirstTickOfBar) Print($"[DIAG] Skipping bar {barTime:O} outside time window {WindowStart}-{WindowEnd}");
return;
}
}
else if (ShowTimeShade) BackBrushes[0] = timeShadeClear;
// pattern logic (AS-IS — includes bar[0] just like your original)
bool isUpBar0=Close[0]>Open[0]; bool isDownBar0=Close[0]<Open[0];
bool isUpBar1=Close[1]>Open[1]; bool isDownBar1=Close[1]<Open[1];
bool isUpBar2=Close[2]>Open[2]; bool isDownBar2=Close[2]<Open[2];
bool isUpBar3=Close[3]>Open[3]; bool isDownBar3=Close[3]<Open[3];
bool isUpBar4=Close[4]>Open[4]; bool isDownBar4=Close[4]<Open[4];
bool isUpBar5=Close[5]>Open[5]; bool isDownBar5=Close[5]<Open[5];
bool long6Bar = isUpBar5 && isDownBar4 && isDownBar3 && isDownBar2 && isDownBar1 && isUpBar0;
bool short6Bar = isDownBar5 && isUpBar4 && isUpBar3 && isUpBar2 && isUpBar1 && isDownBar0;
bool long5Bar = isUpBar4 && isDownBar3 && isDownBar2 && isDownBar1 && isUpBar0;
bool short5Bar = isDownBar4 && isUpBar3 && isUpBar2 && isUpBar1 && isDownBar0;
bool long4Bar = isUpBar3 && isDownBar2 && isDownBar1 && isUpBar0;
bool short4Bar = isDownBar3 && isUpBar2 && isUpBar1 && isDownBar0;
bool anyEnabled = FourBarSetup || FiveBarSetup || SixBarSetup;
bool use4 = FourBarSetup || !anyEnabled;
bool use5 = FiveBarSetup;
bool use6 = SixBarSetup;
bool baseLong = (use6 && long6Bar) || (use5 && long5Bar) || (use4 && long4Bar);
bool baseShort = (use6 && short6Bar) || (use5 && short5Bar) || (use4 && short4Bar);
// EMA filter (AS-IS)
bool emaPassLong = !UseEmaFilter || (isUpBar0 && Close[0] > ema[0]);
bool emaPassShort = !UseEmaFilter || (isDownBar0 && Close[0] < ema[0]);
bool takeLong = baseLong && emaPassLong;
bool takeShort = baseShort && emaPassShort;
string tagPrefix =
(use6 && (long6Bar || short6Bar)) ? "4 BAR" :
(use5 && (long5Bar || short5Bar)) ? "3 BAR" :
"2 BAR";
// sizing by risk (AS-IS; ATR[0])
double stopPts = StopLossMultiplier * atr[0];
double stopDollars = stopPts * Instrument.MasterInstrument.PointValue;
if (stopDollars <= 0) return;
int qty = (int)Math.Floor(RiskPerTrade / stopDollars);
qty = Math.Min(qty, MaxContracts);
if (qty > 0)
{
if (takeLong)
{
OrderType ordType; double limitPrice = 0.0; double stopPrice = 0.0;
switch (EntryType)
{
case EntryMode.BarExtremaLimit:
ordType = OrderType.Limit;
limitPrice = Low[0];
break;
case EntryMode.OneTickBreak:
ordType = OrderType.StopMarket;
stopPrice = High[0] + TickSize;
break;
case EntryMode.FibonacciRetracement:
ordType = OrderType.Limit;
double rangeL = High[0] - Low[0];
limitPrice = High[0] - (FibRetracePct * rangeL);
break;
default:
ordType = OrderType.StopMarket;
stopPrice = High[0] + TickSize;
break;
}
string sig = tagPrefix + " LE_" + (++entrySeq);
Print($"[DIAG] SUBMIT {sig} qty={qty} type={ordType} lp={limitPrice} sp={stopPrice} at {Time[0]:O}");
var entryOrder = SubmitOrderUnmanaged(0, OrderAction.Buy, ordType, qty, limitPrice, stopPrice, string.Empty, sig);
if (entryOrder != null)
{
pendingEntryOrders.Add(entryOrder);
workingEntryOrder = entryOrder;
workingOrderBar = CurrentBar;
}
}
if (takeShort)
{
OrderType ordType; double limitPrice = 0.0; double stopPrice = 0.0;
switch (EntryType)
{
case EntryMode.BarExtremaLimit:
ordType = OrderType.Limit;
limitPrice = High[0];
break;
case EntryMode.OneTickBreak:
ordType = OrderType.StopMarket;
stopPrice = Low[0] - TickSize;
break;
case EntryMode.FibonacciRetracement:
ordType = OrderType.Limit;
double rangeS = High[0] - Low[0];
limitPrice = Low[0] + (FibRetracePct * rangeS);
break;
default:
ordType = OrderType.StopMarket;
stopPrice = Low[0] - TickSize;
break;
}
string sig = tagPrefix + " SE_" + (++entrySeq);
Print($"[DIAG] SUBMIT {sig} qty={qty} type={ordType} lp={limitPrice} sp={stopPrice} at {Time[0]:O}");
var entryOrder = SubmitOrderUnmanaged(0, OrderAction.SellShort, ordType, qty, limitPrice, stopPrice, string.Empty, sig);
if (entryOrder != null)
{
pendingEntryOrders.Add(entryOrder);
workingEntryOrder = entryOrder;
workingOrderBar = CurrentBar;
}
}
}
}
// ===== OnExecutionUpdate: open/close per-entry trades; submit brackets =====
protected override void OnExecutionUpdate(Execution execution, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time)
{
var o = execution?.Order;
// DIAG: always print executions seen
Print($"[DIAG][OnExecutionUpdate] {time:O} name={o?.Name} state={o?.OrderState} act={o?.OrderAction} avg={o?.AverageFillPrice} filled={o?.Filled} mp={marketPosition}");
if (o == null || o.OrderState != OrderState.Filled)
return;
// if entry filled, clear one-bar pointer so it won't be canceled next bar
if (workingEntryOrder != null && o.OrderId == workingEntryOrder.OrderId)
{
workingEntryOrder = null;
workingOrderBar = -1;
}
// ENTRY filled: open a new ManagedTrade and place its own SL/PT (on tick series)
if (o.Name.Contains(" LE_") || o.Name.Contains(" SE_"))
{
string side = (o.OrderAction == OrderAction.Buy) ? "LONG" : "SHORT";
string tradeId = o.OrderId;
double atrPts = atr[0];
var mt = new ManagedTrade(tradeId, o.OrderId, o.AverageFillPrice, o.Filled, side,
atrPts, StopLossMultiplier, TargetMultiplier);
string tagBase = (o.Name.StartsWith("4 ") ? "4 BAR" : (o.Name.StartsWith("3 ") ? "3 BAR" : "2 BAR"));
string oco = "OCO-" + o.OrderId;
double stopDist = StopLossMultiplier * atrPts;
double tgtDist = TargetMultiplier * atrPts;
if (side == "LONG")
{
double stopPrice = mt.EntryPrice - stopDist;
double targetPrice = mt.EntryPrice + tgtDist;
mt.ProfitTargetOrder = SubmitOrderUnmanaged(1, OrderAction.Sell, OrderType.Limit, mt.EntryQty, targetPrice, 0, oco, tagBase + " Target");
mt.StopLossOrder = SubmitOrderUnmanaged(1, OrderAction.Sell, OrderType.StopMarket, mt.EntryQty, 0, stopPrice, oco, tagBase + " Stop Loss");
}
else
{
double stopPrice = mt.EntryPrice + stopDist;
double targetPrice = mt.EntryPrice - tgtDist;
mt.ProfitTargetOrder = SubmitOrderUnmanaged(1, OrderAction.BuyToCover, OrderType.Limit, mt.EntryQty, targetPrice, 0, oco, tagBase + " Target");
mt.StopLossOrder = SubmitOrderUnmanaged(1, OrderAction.BuyToCover, OrderType.StopMarket, mt.EntryQty, 0, stopPrice, oco, tagBase + " Stop Loss");
}
openTrades.Add(mt);
return;
}
// EXIT filled: close the specific ManagedTrade (PT/SL/FLAT)
if (o.Name.EndsWith(" Stop Loss") || o.Name.EndsWith(" Target") || o.Name == "FLAT")
{
var mt = openTrades.FirstOrDefault(t => t.IsOpen && (t.StopLossOrder == o || t.ProfitTargetOrder == o));
if (mt == null)
{
string oco = o.Oco;
if (!string.IsNullOrEmpty(oco) && oco.StartsWith("OCO-"))
{
string entryId = oco.Substring(4);
mt = openTrades.FirstOrDefault(t => t.IsOpen && t.EntryOrderId == entryId);
}
}
if (mt != null)
{
double exitPrice = o.AverageFillPrice;
double tradePnL = (mt.Side == "LONG")
? (exitPrice - mt.EntryPrice) * mt.EntryQty * Instrument.MasterInstrument.PointValue
: (mt.EntryPrice - exitPrice) * mt.EntryQty * Instrument.MasterInstrument.PointValue;
string finalTiso = Time[0].ToString("o");
logBuffer.Add(
$"{mt.TradeId},{finalTiso},{exitPrice:F2},{tradePnL:F2},0.00,{tradePnL:F2},{mt.EntryPrice:F2},{mt.AtrAtEntryPts:F4},{mt.Side},{mt.EntryQty},{mt.StopAtrMultAtEntry:F3},{mt.TargetAtrMultAtEntry:F3}"
);
// DIAG: see exit row promptly
FlushBufferIfNeeded();
mt.IsOpen = false;
}
openTrades.RemoveAll(t => !t.IsOpen);
return;
}
// safety-net: if flat, clear any lingering IsOpen flags
if (marketPosition == MarketPosition.Flat && openTrades.Any(t => t.IsOpen))
{
foreach (var mt in openTrades) mt.IsOpen = false;
openTrades.RemoveAll(t => !t.IsOpen);
}
}
protected override void OnOrderUpdate(Order order, double limitPrice, double stopPrice, int quantity, int filled, double averageFillPrice, OrderState orderState, DateTime time, ErrorCode error, string nativeError)
{
if (order == null) return;
// DIAG: visibility into order lifecycle (no BarsInProgress usage)
if (order.OrderState == OrderState.Rejected || error != ErrorCode.NoError || !string.IsNullOrEmpty(nativeError))
{
Print($"[DIAG][OnOrderUpdate] {time:O} REJECTED/ERR name={order.Name} act={order.OrderAction} type={order.OrderType} " +
$"qty={order.Quantity} state={order.OrderState} err={error} native='{nativeError}' limit={limitPrice} stop={stopPrice}");
}
else if (order.OrderState == OrderState.Cancelled)
{
Print($"[DIAG][OnOrderUpdate] {time:O} CANCELLED name={order.Name} act={order.OrderAction} type={order.OrderType} " +
$"qty={order.Quantity} oco='{order.Oco}' limit={limitPrice} stop={stopPrice}");
}
else if (order.OrderState == OrderState.Working && IsFirstTickOfBar)
{
Print($"[DIAG][OnOrderUpdate] {time:O} WORKING name={order.Name} limit={limitPrice} stop={stopPrice}");
}
// original maintenance: drop pending list on cancel/reject
if (order.Name.Contains(" LE_") || order.Name.Contains(" SE_"))
{
if (order.OrderState == OrderState.Cancelled || order.OrderState == OrderState.Rejected)
pendingEntryOrders.RemoveAll(o => o.OrderId == order.OrderId);
}
}
private TimeSpan ParseTimeSpan(string hhmmss)
{
if (string.IsNullOrWhiteSpace(hhmmss)) return TimeSpan.MinValue;
TimeSpan ts; return TimeSpan.TryParse(hhmmss, out ts) ? ts : TimeSpan.MinValue;
}
// ===== CSV flush helpers =====
private const int FlushEveryNLines = 500;
private void FlushBufferIfNeeded()
{
if (logWriter == null) return;
if (logBuffer.Count < FlushEveryNLines) return;
try
{
foreach (var line in logBuffer) logWriter.WriteLine(line);
logWriter.Flush();
logBuffer.Clear();
}
catch (Exception ex)
{
Print("[DIAG] Flush error: " + ex.Message);
}
}
private void FlushBufferAll()
{
if (logWriter == null) return;
if (logBuffer.Count == 0) return;
try
{
foreach (var line in logBuffer) logWriter.WriteLine(line);
logWriter.Flush();
logBuffer.Clear();
Print("[DIAG] Final CSV flush complete.");
}
catch (Exception ex)
{
Print("[DIAG] Final flush error: " + ex.Message);
}
}
#region Properties
[NinjaScriptProperty, Display(Name="2 Bar", GroupName="Pattern Setups", Order=0)]
public bool FourBarSetup { get; set; }
[NinjaScriptProperty, Display(Name="3 Bar", GroupName="Pattern Setups", Order=1)]
public bool FiveBarSetup { get; set; }
[NinjaScriptProperty, Display(Name="4 Bar", GroupName="Pattern Setups", Order=2)]
public bool SixBarSetup { get; set; }
[NinjaScriptProperty, Range(1, int.MaxValue), Display(Name="ATR Period", GroupName="Risk Sizing", Order=0)]
public int AtrPeriod { get; set; }
[NinjaScriptProperty, Range(1, int.MaxValue), Display(Name="Risk Per Trade ($)", GroupName="Risk Sizing", Order=1)]
public double RiskPerTrade { get; set; }
[NinjaScriptProperty, Range(1, int.MaxValue), Display(Name="Max Contracts", GroupName="Risk Sizing", Order=2)]
public int MaxContracts { get; set; }
[NinjaScriptProperty, Range(0.1, double.MaxValue), Display(Name="Target (xATR)", GroupName="Exit Multipliers", Order=0)]
public double TargetMultiplier { get; set; }
[NinjaScriptProperty, Range(0.1, double.MaxValue), Display(Name="Stop (xATR)", GroupName="Exit Multipliers", Order=1)]
public double StopLossMultiplier { get; set; }
[NinjaScriptProperty, Display(Name="Entry Mode", GroupName="Entry Mode", Order=0)]
public EntryMode EntryType { get; set; }
[NinjaScriptProperty, Range(0.0, 1.0), Display(Name="Fib Retracement %", GroupName="Entry Mode", Order=1)]
public double FibRetracePct { get; set; }
[NinjaScriptProperty, Display(Name="Use EMA Filter", GroupName="EMA Filter", Order=0)]
public bool UseEmaFilter { get; set; }
[NinjaScriptProperty, Range(1, int.MaxValue), Display(Name="EMA Period", GroupName="EMA Filter", Order=1)]
public int EmaPeriod { get; set; }
[NinjaScriptProperty, Display(Name="Time Filter", GroupName="Time Filter", Order=0)]
public bool TimeFilter { get; set; }
[NinjaScriptProperty, Display(Name="Window Start (hh:mm:ss)", GroupName="Time Filter", Order=1)]
public string WindowStart { get; set; }
[NinjaScriptProperty, Display(Name="Window End (hh:mm:ss)", GroupName="Time Filter", Order=2)]
public string WindowEnd { get; set; }
[NinjaScriptProperty, Display(Name="Flatten At Time", GroupName="Flatten At Time", Order=0)]
public bool FlattenAtTime { get; set; }
[NinjaScriptProperty, Display(Name="Flatten Time (hh:mm:ss)", GroupName="Flatten At Time", Order=1)]
public string FlattenTime { get; set; }
[NinjaScriptProperty, Display(Name="Show Time Shade", GroupName="Visuals", Order=0)]
public bool ShowTimeShade { get; set; }
[NinjaScriptProperty, Range(0,255), Display(Name="Shade Opacity (0-255)", GroupName="Visuals", Order=1)]
public int ShadeOpacity { get; set; }
[NinjaScriptProperty, Display(Name="Log Folder", GroupName="Logging", Order=0)]
public string LogFolder { get; set; }
[NinjaScriptProperty, Display(Name="CSV File Prefix", GroupName="Logging", Order=1)]
public string CsvFilePrefix { get; set; }
#endregion
}
}