예제 #1
0
        public static void AddPnLHoldTime(this Plotter plotter, SimulatorCore sim)
        {
            var tradeLog = LogAnalysis
                           .GroupPositions(sim.Log, true)
                           .OrderBy(i => i.Entry.BarOfExecution.Time);

            plotter.SelectChart(Plotter.SheetNames.PNL_HOLD_TIME, "Days Held");
            foreach (var trade in tradeLog)
            {
                var pnl   = (trade.Quantity > 0 ? 100.0 : -100.0) * (trade.Exit.FillPrice / trade.Entry.FillPrice - 1.0);
                var label = pnl > 0.0 ? "Profit" : "Loss";
                plotter.SetX((trade.Exit.BarOfExecution.Time - trade.Entry.BarOfExecution.Time).TotalDays);
                plotter.Plot(label, pnl);
            }
        }
예제 #2
0
        public static void AddMfeMae(this Plotter plotter, SimulatorCore sim)
        {
            var tradeLog = LogAnalysis
                           .GroupPositions(sim.Log, true)
                           .OrderBy(i => i.Entry.BarOfExecution.Time);

            plotter.SelectChart(Plotter.SheetNames.PNL_MFE_MAE, "Max Excursion");
            foreach (var trade in tradeLog)
            {
                var pnl   = 100.0 * (trade.Exit.FillPrice / trade.Entry.FillPrice - 1.0);
                var label = pnl > 0.0 ? "Profit" : "Loss";

                plotter.SetX(100.0 * (trade.HighestHigh / trade.Entry.FillPrice - 1.0));
                plotter.Plot(label, pnl);

                plotter.SetX(100.0 * (trade.LowestLow / trade.Entry.FillPrice - 1.0));
                plotter.Plot(label, pnl);
            }
        }
예제 #3
0
        public static void AddPositionLog(this Plotter plotter, SimulatorCore sim)
        {
            var tradeLog = LogAnalysis
                           .GroupPositions(sim.Log, true)
                           .OrderBy(i => i.Entry.BarOfExecution.Time);

            plotter.SelectChart("Position Log", "entry date");
            foreach (var trade in tradeLog)
            {
                plotter.SetX(string.Format("{0:MM/dd/yyyy}", trade.Entry.BarOfExecution.Time));
                plotter.Plot("exit date", string.Format("{0:MM/dd/yyyy}", trade.Exit.BarOfExecution.Time));
                plotter.Plot("days held", (trade.Exit.BarOfExecution.Time - trade.Entry.BarOfExecution.Time).TotalDays);
                plotter.Plot("Symbol", trade.Symbol);
                plotter.Plot("Quantity", trade.Quantity);
                plotter.Plot("% Profit", Math.Round((trade.Quantity > 0 ? 100.0 : -100.0) * (trade.Exit.FillPrice / trade.Entry.FillPrice - 1.0), 2));
                plotter.Plot("Exit", trade.Exit.OrderTicket.Comment ?? "");
                //plotter.Plot("$ Profit", trade.Quantity * (trade.Exit.FillPrice - trade.Entry.FillPrice));
            }
        }
예제 #4
0
        public override void Run()
        {
            //========== initialization ==========

            StartTime = DateTime.Parse("01/01/2008");
            EndTime   = DateTime.Now.Date - TimeSpan.FromDays(5);

            foreach (var n in UNIVERSE)
            {
                AddDataSource(n);
            }
            AddDataSource(BENCHMARK);

            Deposit(INITIAL_CAPITAL);
            CommissionPerShare = 0.015;

            //========== simulation loop ==========

            foreach (var s in SimTimes)
            {
                //----- find instruments

                _benchmark = _benchmark ?? FindInstrument(BENCHMARK);
                var universe = Instruments
                               .Where(i => i != _benchmark)
                               .ToList();

                //----- calculate indicators

                // make sure to calculate indicators for the
                // full universe on every single bar
                var indicators = universe
                                 .ToDictionary(
                    i => i,
                    i => new
                {
                    rsi = i.Close.RSI(3),
                    roc = i.Close.Momentum(200),
                });

                var smaBand = _benchmark.Close.SMA(200).Multiply(0.98); // 2% below 200-day SMA

                // filter universe to potential candidates
                var filtered = universe
                               .Where(i => _benchmark.Close[0] > smaBand[0] &&
                                      indicators[i].rsi[0] < MAX_RSI)
                               .ToList();

                if (NextSimTime.DayOfWeek < SimTime[0].DayOfWeek) // open positions on Monday
                {
                    // sort candidates by ROC to find entries
                    var entries = filtered
                                  .OrderByDescending(i => indicators[i].roc[0])
                                  .Take(MAX_ENTRIES)
                                  .ToList();

                    foreach (var i in universe)
                    {
                        int targetShares = entries.Contains(i)
                            ? (int)Math.Floor(NetAssetValue[0] / MAX_ENTRIES / i.Close[0])
                            : 0;

                        i.Trade(targetShares - i.Position);
                    }
                }

                //----- output

                if (!IsOptimizing)
                {
                    // plot to chart
                    _plotter.SelectChart(Name + ": " + OptimizerParamsAsString, "date");
                    _plotter.SetX(SimTime[0]);
                    _plotter.Plot("nav", NetAssetValue[0]);
                    _plotter.Plot(_benchmark.Symbol, _benchmark.Close[0]);

                    // placeholder, to make sure positions land on sheet 2
                    _plotter.SelectChart(Name + " positions", "entry date");

                    // additional indicators
                    _plotter.SelectChart(Name + " extra", "date");
                    _plotter.SetX(SimTime[0]);
                    _plotter.Plot("leverage", Instruments.Sum(i => i.Position * i.Close[0]) / NetAssetValue[0]);
                }
            }

            //========== post processing ==========

            //----- print position log, grouped as LIFO

            if (!IsOptimizing)
            {
                var tradeLog = LogAnalysis
                               .GroupPositions(Log, true)
                               .OrderBy(i => i.Entry.BarOfExecution.Time);

                _plotter.SelectChart(Name + " positions", "entry date");
                foreach (var trade in tradeLog)
                {
                    _plotter.SetX(trade.Entry.BarOfExecution.Time);
                    _plotter.Plot("exit date", trade.Exit.BarOfExecution.Time);
                    _plotter.Plot("Symbol", trade.Symbol);
                    _plotter.Plot("Quantity", trade.Quantity);
                    _plotter.Plot("% Profit", (trade.Quantity > 0 ? 1.0 : -1.0) * (trade.Exit.FillPrice / trade.Entry.FillPrice - 1.0));
                    _plotter.Plot("Exit", trade.Exit.OrderTicket.Comment ?? "");
                    //_plotter.Plot("$ Profit", trade.Quantity * (trade.Exit.FillPrice - trade.Entry.FillPrice));
                }
            }

            //----- optimization objective

            double cagr = Math.Exp(252.0 / Math.Max(1, TradingDays) * Math.Log(NetAssetValue[0] / INITIAL_CAPITAL)) - 1.0;

            FitnessValue = cagr / Math.Max(1e-10, Math.Max(0.01, NetAssetValueMaxDrawdown));

            if (!IsOptimizing)
            {
                Output.WriteLine("CAGR = {0:P2}, DD = {1:P2}, Fitness = {2:F4}", cagr, NetAssetValueMaxDrawdown, FitnessValue);
            }
        }
예제 #5
0
        public override void Run()
        {
            //========== initialization ==========

            StartTime = DateTime.Parse("01/01/2008");
            EndTime   = DateTime.Now.Date - TimeSpan.FromDays(5);

            foreach (var n in UNIVERSE)
            {
                AddDataSource(n);
            }
            AddDataSource(BENCHMARK);

            Deposit(INITIAL_CAPITAL);
            CommissionPerShare = 0.015;

            var entryParameters = Enumerable.Empty <Instrument>()
                                  .ToDictionary(
                i => i,
                i => new
            {
                entryDate    = default(DateTime),
                entryPrice   = default(double),
                stopLoss     = default(double),
                profitTarget = default(double),
            });

            //========== simulation loop ==========

            foreach (var s in SimTimes)
            {
                //----- find instruments

                _benchmark = _benchmark ?? FindInstrument(BENCHMARK);
                var universe = Instruments
                               .Where(i => i != _benchmark)
                               .ToList();

                //----- calculate indicators

                // make sure to calculate indicators for the
                // full universe on every single bar
                var indicators = universe
                                 .ToDictionary(
                    i => i,
                    i => new
                {
                    sma150 = i.Close.SMA(SMA_DAYS),
                    adx7   = i.ADX(7),
                    atr10  = i.TrueRange().Divide(i.Close).SMA(10),
                    rsi3   = i.Close.RSI(3),
                });

                // filter universe to potential candidates
                var filtered = universe
                               .Where(i =>
                                      (ENTRY_DIR > 0
                            ? i.Close[0] > indicators[i].sma150[0]                // long: above sma
                            : i.Close[0] > i.Close[1] && i.Close[1] > i.Close[2]) && // short: 2 up-days
                                      indicators[i].adx7[0] > MIN_ADX &&
                                      indicators[i].atr10[0] > MIN_ATR / 10000.0 &&
                                      (ENTRY_DIR > 0
                            ? indicators[i].rsi3[0] < MINMAX_RSI   // long: maximum
                            : indicators[i].rsi3[0] > MINMAX_RSI)) // short: minimum
                               .ToList();

                //----- manage existing positions

                int numOpenPositions = Positions.Keys.Count();
                foreach (var pos in Positions.Keys)
                {
                    // time-based exit
                    if (entryParameters[pos].entryDate <= SimTime[MAX_HOLD_DAYS - 1])
                    {
                        pos.Trade(-pos.Position, OrderType.closeThisBar).Comment = "time exit";
                        numOpenPositions--;
                    }
                    else if (ENTRY_DIR > 0
                        ? pos.Close[0] >= entryParameters[pos].profitTarget  // long
                        : pos.Close[0] <= entryParameters[pos].profitTarget) // short
                    {
                        pos.Trade(-pos.Position,
                                  OrderType.openNextBar)
                        .Comment = "profit target";
                        numOpenPositions--;
                    }
                    else
                    {
                        pos.Trade(-pos.Position,
                                  OrderType.stopNextBar,
                                  entryParameters[pos].stopLoss)
                        .Comment = "stop loss";
                    }
                }

                //----- open new positions

                if (NextSimTime.DayOfWeek < SimTime[0].DayOfWeek) // open positions on Monday
                {
                    // sort candidates by RSI to find entries
                    var entries = ENTRY_DIR > 0
                        ? filtered // long
                                  .Where(i => i.Position == 0)
                                  .OrderBy(i => indicators[i].rsi3[0])
                                  .Take(MAX_ENTRIES - numOpenPositions)
                                  .ToList()
                        : filtered // short
                                  .Where(i => i.Position == 0)
                                  .OrderByDescending(i => indicators[i].rsi3[0])
                                  .Take(MAX_ENTRIES - numOpenPositions)
                                  .ToList();

                    foreach (var i in entries)
                    {
                        // save our entry parameters, so that we may access
                        // them later to manage exits
                        double entryPrice = ENTRY_DIR > 0
                                ? i.Close[0] * (1.0 - MIN_ATR / 10000.0) // long
                                : i.Close[0];                            // short

                        double stopLoss = ENTRY_DIR > 0
                                ? entryPrice * (1.0 - STOP_LOSS / 100.0 * indicators[i].atr10[0])  // long
                                : entryPrice * (1.0 + STOP_LOSS / 100.0 * indicators[i].atr10[0]); // short

                        double profitTarget = ENTRY_DIR > 0
                            ? entryPrice * (1.0 + PROFIT_TARGET / 10000.0)  // long
                            : entryPrice * (1.0 - PROFIT_TARGET / 10000.0); // short

                        entryParameters[i] = new
                        {
                            entryDate = NextSimTime,
                            entryPrice,
                            stopLoss,
                            profitTarget,
                        };

                        // calculate target shares in two ways:
                        // * fixed-fractional risk (with entry - stop-loss = "risk"), and
                        // * fixed percentage of total equity
                        double riskPerShare = ENTRY_DIR > 0
                            ? Math.Max(0.10, entryPrice - stopLoss)  // long
                            : Math.Max(0.10, stopLoss - entryPrice); // short

                        int sharesRiskLimited = (int)Math.Floor(MAX_RISK / 100.0 / MAX_ENTRIES * NetAssetValue[0] / riskPerShare);
                        int sharesCapLimited  = (int)Math.Floor(MAX_CAP / 100.0 / MAX_ENTRIES * NetAssetValue[0] / entryParameters[i].entryPrice);
                        int targetShares      = (ENTRY_DIR > 0 ? 1 : -1) * Math.Min(sharesRiskLimited, sharesCapLimited);

                        // enter positions with limit order
                        i.Trade(targetShares,
                                OrderType.limitNextBar,
                                entryParameters[i].entryPrice);
                    }
                }

                //----- output

                if (!IsOptimizing)
                {
                    // plot to chart
                    _plotter.SelectChart(Name + ": " + OptimizerParamsAsString, "date");
                    _plotter.SetX(SimTime[0]);
                    _plotter.Plot("nav", NetAssetValue[0]);
                    _plotter.Plot(_benchmark.Symbol, _benchmark.Close[0]);

                    // placeholder, to make sure positions land on sheet 2
                    _plotter.SelectChart(Name + " positions", "entry date");

                    // additional indicators
                    _plotter.SelectChart(Name + " extra", "date");
                    _plotter.SetX(SimTime[0]);
                    _plotter.Plot("leverage", Instruments.Sum(i => i.Position * i.Close[0]) / NetAssetValue[0]);
                }
            }

            //========== post processing ==========

            //----- print position log, grouped as LIFO

            if (!IsOptimizing)
            {
                var tradeLog = LogAnalysis
                               .GroupPositions(Log, true)
                               .OrderBy(i => i.Entry.BarOfExecution.Time);

                _plotter.SelectChart(Name + " positions", "entry date");
                foreach (var trade in tradeLog)
                {
                    _plotter.SetX(trade.Entry.BarOfExecution.Time);
                    _plotter.Plot("exit date", trade.Exit.BarOfExecution.Time);
                    _plotter.Plot("Symbol", trade.Symbol);
                    _plotter.Plot("Quantity", trade.Quantity);
                    _plotter.Plot("% Profit", (ENTRY_DIR > 0 ? 1.0 : -1.0) * (trade.Exit.FillPrice / trade.Entry.FillPrice - 1.0));
                    _plotter.Plot("Exit", trade.Exit.OrderTicket.Comment ?? "");
                    //_plotter.Plot("$ Profit", trade.Quantity * (trade.Exit.FillPrice - trade.Entry.FillPrice));
                }
            }

            //----- optimization objective

            double cagr = Math.Exp(252.0 / Math.Max(1, TradingDays) * Math.Log(NetAssetValue[0] / INITIAL_CAPITAL)) - 1.0;

            FitnessValue = cagr / Math.Max(1e-10, Math.Max(0.01, NetAssetValueMaxDrawdown));

            if (!IsOptimizing)
            {
                Output.WriteLine("CAGR = {0:P2}, DD = {1:P2}, Fitness = {2:F4}", cagr, NetAssetValueMaxDrawdown, FitnessValue);
            }
        }
        public override void Run()
        {
            //---------- initialization

            // set simulation time frame
            WarmupStartTime = DateTime.Parse("01/01/2005", CultureInfo.InvariantCulture);
            StartTime       = DateTime.Parse("01/01/2008", CultureInfo.InvariantCulture);
            EndTime         = DateTime.Now.Date - TimeSpan.FromDays(5);

            // set account value
            Deposit(INITIAL_FUNDS);
            CommissionPerShare = 0.015;

            // add instruments
            AddDataSource(BENCHMARK);
            foreach (string nickname in UNIVERSE)
            {
                AddDataSource(nickname);
            }

            //---------- simulation

            // loop through all bars
            foreach (DateTime simTime in SimTimes)
            {
                // determine active instruments
                var activeInstruments = Instruments
                                        .Where(i => UNIVERSE.Contains(i.Nickname) &&
                                               i.Time[0] == simTime)
                                        .ToList();

                // calculate indicators
                // store to list, to make sure indicators are evaluated
                // exactly once per bar
                var instrumentEvaluation = activeInstruments
                                           .Select(i => new
                {
                    instrument = i,
                    regression = i.Close.LogRegression(MOM_PERIOD),
                    maxMove    = i.Close.LogReturn().AbsValue().Highest(MOM_PERIOD),
                    avg100     = i.Close.EMA(INSTR_FLT),
                    atr20      = i.AverageTrueRange(ATR_PERIOD),
                })
                                           .ToList();

                // 1) rank instruments by volatility-adjusted momentum
                // 2) determine position size with risk equal to 10-basis points
                //    of NAV per instrument, based on 20-day ATR
                // 3) disqualify instruments, when
                //    - trading below 100-day moving average
                //    - negative momentum
                //    - maximum move > 15%
                var instrumentRanking = instrumentEvaluation
                                        .OrderByDescending(e => e.regression.Slope[0] * e.regression.R2[0])
                                        .Select(e => new
                {
                    instrument   = e.instrument,
                    positionSize = (e.maxMove[0] < Math.Log(MAX_MOVE / 100.0) &&
                                    e.instrument.Close[0] > e.avg100[0] &&
                                    e.regression.Slope[0] > 0.0)
                            ? 0.001 / e.atr20[0] * e.instrument.Close[0]
                            : 0.0,
                })
                                        .ToList();

                // assign equity, until we run out of cash,
                // or we are no longer in the top-ranking 20%
                var instrumentEquity = Enumerable.Range(0, instrumentRanking.Count)
                                       .ToDictionary(
                    i => instrumentRanking[i].instrument,
                    i => i <= TOP_PCNT / 100.0 * instrumentRanking.Count
                            ? Math.Min(
                        instrumentRanking[i].positionSize,
                        Math.Max(0.0, 1.0 - instrumentRanking.Take(i).Sum(r => r.positionSize)))
                            : 0.0);

                // index filter: only buy any shares, while S&P-500 is trading above its 200-day moving average
                var indexFilter = FindInstrument(BENCHMARK).Close
                                  .Divide(FindInstrument(BENCHMARK).Close.EMA(INDEX_FLT));

                // trade once per week
                // this is a slight simplification from Clenow's suggestion to change positions
                // every week, and adjust position sizes only every other week
                if (SimTime[0].DayOfWeek >= DayOfWeek.Wednesday && SimTime[1].DayOfWeek < DayOfWeek.Wednesday)
                {
                    foreach (Instrument instrument in instrumentEquity.Keys)
                    {
                        int currentShares = instrument.Position;
                        int targetShares  = (int)Math.Round(NetAssetValue[0] * instrumentEquity[instrument] / instrument.Close[0]);

                        if (indexFilter[0] < 0.0)
                        {
                            targetShares = Math.Min(targetShares, currentShares);
                        }

                        instrument.Trade(targetShares - currentShares);
                    }

                    string message = instrumentEquity
                                     .Where(i => i.Value != 0.0)
                                     .Aggregate(string.Format("{0:MM/dd/yyyy}: ", SimTime[0]),
                                                (prev, next) => prev + string.Format("{0}={1:P2} ", next.Key.Symbol, next.Value));
                    if (!IsOptimizing)
                    {
                        Output.WriteLine(message);
                    }
                }

                // create plots on Sheet 1
                if (TradingDays > 0)
                {
                    _plotter.SelectChart(Name, "date");
                    _plotter.SetX(SimTime[0]);
                    _plotter.Plot("NAV", NetAssetValue[0]);
                    _plotter.Plot(BENCHMARK, FindInstrument(BENCHMARK).Close[0]);
                }
            }

            //----- post processing

            // print position log, grouped as LIFO
            var tradeLog = LogAnalysis
                           .GroupPositions(Log, true)
                           .OrderBy(i => i.Entry.BarOfExecution.Time);

            _plotter.SelectChart(Name + " positions", "entry date");
            foreach (var trade in tradeLog)
            {
                _plotter.SetX(trade.Entry.BarOfExecution.Time.Date);
                _plotter.Plot("exit date", trade.Exit.BarOfExecution.Time.Date);
                _plotter.Plot("Symbol", trade.Symbol);
                _plotter.Plot("Quantity", trade.Quantity);
                _plotter.Plot("% Profit", trade.Exit.FillPrice / trade.Entry.FillPrice - 1.0);
                _plotter.Plot("Exit", trade.Exit.OrderTicket.Comment ?? "");
                //_plotter.Plot("$ Profit", trade.Quantity * (trade.Exit.FillPrice - trade.Entry.FillPrice));
            }
        }
        public override void Run()
        {
            //========== initialization ==========

            StartTime = DateTime.Parse("01/01/1990");
            EndTime   = DateTime.Now.Date - TimeSpan.FromDays(5);

            AddDataSource(MARKET);

            Deposit(INITIAL_CAPITAL);
            CommissionPerShare = 0.015;

            //========== simulation loop ==========

            var entryPrices = new Dictionary <Instrument, double>();

            foreach (var s in SimTimes)
            {
                _market = _market ?? FindInstrument(MARKET);

                double percentToBuySell = Rules(_market);

                //----- entries

                if (_market.Position == 0 && percentToBuySell != 0.0 ||
                    _market.Position > 0 && percentToBuySell > 0 ||
                    _market.Position < 0 && percentToBuySell < 0)
                {
                    int sharesToBuySell = (int)(Math.Sign(percentToBuySell) * Math.Floor(
                                                    Math.Abs(percentToBuySell) * NetAssetValue[0] / _market.Close[0]));

                    _market.Trade(sharesToBuySell, OrderType.closeThisBar);
                }

                //----- exits

                if (_market.Position > 0 && percentToBuySell < 0 ||
                    _market.Position < 0 && percentToBuySell > 0)
                {
                    // none of the algorithms attempt to gradually
                    // exit positions, so this is good enough
                    _market.Trade(-_market.Position, OrderType.closeThisBar);
                }

                //----- output

                if (!IsOptimizing)
                {
                    // plot to chart
                    _plotter.SelectChart(Name, "date");
                    _plotter.SetX(SimTime[0]);
                    _plotter.Plot("nav", NetAssetValue[0]);
                    _plotter.Plot(_market.Symbol, _market.Close[0]);

                    // placeholder for 2nd sheet
                    _plotter.SelectChart(Name + " positions", "entry date");

                    _plotter.SelectChart(Name + " % invested", "entry date");
                    _plotter.SetX(SimTime[0]);
                    _plotter.Plot("% long", Positions.Keys.Sum(i => Math.Max(0, i.Position) * i.Close[0]) / NetAssetValue[0]);
                    _plotter.Plot("% short", Positions.Keys.Sum(i => - Math.Min(0, i.Position) * i.Close[0]) / NetAssetValue[0]);
                    _plotter.Plot("% total", Positions.Keys.Sum(i => Math.Abs(i.Position) * i.Close[0]) / NetAssetValue[0]);
                }
            }

            //========== post processing ==========

            //----- print position log, grouped as LIFO

            if (!IsOptimizing)
            {
                var tradeLog = LogAnalysis
                               .GroupPositions(Log, true)
                               .OrderBy(i => i.Entry.BarOfExecution.Time);

                _plotter.SelectChart(Name + " positions", "entry date");
                foreach (var trade in tradeLog)
                {
                    _plotter.SetX(trade.Entry.BarOfExecution.Time);
                    _plotter.Plot("exit date", trade.Exit.BarOfExecution.Time);
                    _plotter.Plot("Symbol", trade.Symbol);
                    _plotter.Plot("Quantity", trade.Quantity);
                    _plotter.Plot("% Profit", (trade.Quantity > 0 ? 1.0 : -1.0) * (trade.Exit.FillPrice / trade.Entry.FillPrice - 1.0));
                    _plotter.Plot("Exit", trade.Exit.OrderTicket.Comment ?? "");
                    //_plotter.Plot("$ Profit", trade.Quantity * (trade.Exit.FillPrice - trade.Entry.FillPrice));
                }
            }

            //----- optimization objective

            double cagr = Math.Exp(252.0 / Math.Max(1, TradingDays) * Math.Log(NetAssetValue[0] / INITIAL_CAPITAL)) - 1.0;

            FitnessValue = cagr / Math.Max(1e-10, Math.Max(0.01, NetAssetValueMaxDrawdown));

            if (!IsOptimizing)
            {
                Output.WriteLine("CAGR = {0:P2}, DD = {1:P2}, Fitness = {2:F4}", cagr, NetAssetValueMaxDrawdown, FitnessValue);
            }
        }
예제 #8
0
        public override void Run()
        {
            //========== initialization ==========

            StartTime = DateTime.Parse("01/01/1990");
            EndTime   = DateTime.Now.Date - TimeSpan.FromDays(5);

            AddDataSource(MARKET);
            AddDataSource(VOLATILITY);
#if INCLUDE_TRIN_STRATEGY
            AddDataSource(TRIN);
#endif

            Deposit(INITIAL_CAPITAL);
            CommissionPerShare = 0.015;

            //========== simulation loop ==========

            foreach (var s in SimTimes)
            {
                _market     = _market ?? FindInstrument(MARKET);
                _volatility = _volatility ?? FindInstrument(VOLATILITY);
#if INCLUDE_TRIN_STRATEGY
                _trin = _trin ?? FindInstrument(TRIN);
#endif

                int buySell = Rules();

                //----- enter positions

                if (_market.Position == 0 && buySell != 0)
                {
                    int numShares = buySell * (int)Math.Floor(NetAssetValue[0] / _market.Close[0]);
                    _market.Trade(numShares, OrderType.closeThisBar);
                }

                //----- exit positions

                else if (_market.Position != 0 && buySell != 0)
                {
                    _market.Trade(-_market.Position, OrderType.closeThisBar);
                }

                //----- output

                if (!IsOptimizing)
                {
                    // plot to chart
                    _plotter.SelectChart(Name + ": " + OptimizerParamsAsString, "date");
                    _plotter.SetX(SimTime[0]);
                    _plotter.Plot("nav", NetAssetValue[0]);
                    _plotter.Plot(_market.Symbol, _market.Close[0]);
                }
            }

            //========== post processing ==========

            //----- print position log, grouped as LIFO

            if (!IsOptimizing)
            {
                var tradeLog = LogAnalysis
                               .GroupPositions(Log, true)
                               .OrderBy(i => i.Entry.BarOfExecution.Time);

                _plotter.SelectChart(Name + " positions", "entry date");
                foreach (var trade in tradeLog)
                {
                    _plotter.SetX(trade.Entry.BarOfExecution.Time);
                    _plotter.Plot("exit date", trade.Exit.BarOfExecution.Time);
                    _plotter.Plot("Symbol", trade.Symbol);
                    _plotter.Plot("Quantity", trade.Quantity);
                    _plotter.Plot("% Profit", (trade.Quantity > 0 ? 1.0 : -1.0) * (trade.Exit.FillPrice / trade.Entry.FillPrice - 1.0));
                    _plotter.Plot("Exit", trade.Exit.OrderTicket.Comment ?? "");
                    //_plotter.Plot("$ Profit", trade.Quantity * (trade.Exit.FillPrice - trade.Entry.FillPrice));
                }
            }

            //----- optimization objective

            double cagr = Math.Exp(252.0 / Math.Max(1, TradingDays) * Math.Log(NetAssetValue[0] / INITIAL_CAPITAL)) - 1.0;
            FitnessValue = cagr / Math.Max(1e-10, Math.Max(0.01, NetAssetValueMaxDrawdown));

            if (!IsOptimizing)
            {
                Output.WriteLine("CAGR = {0:P2}, DD = {1:P2}, Fitness = {2:F4}", cagr, NetAssetValueMaxDrawdown, FitnessValue);
            }
        }