コード例 #1
0
        /// <summary>
        /// Метод под флаг TemplateTypes.INTERACTIVESPLINE
        /// </summary>
        public InteractiveSeries Execute(InteractiveSeries smile, IOptionSeries optSer, double scaleMult, int barNum)
        {
            if ((smile == null) || (optSer == null))
            {
                return(Constants.EmptySeries);
            }

            int barsCount = m_context.BarsCount;

            if (!m_context.IsLastBarUsed)
            {
                barsCount--;
            }
            if (barNum < barsCount - 1)
            {
                return(Constants.EmptySeries);
            }

            SmileInfo oldInfo = smile.GetTag <SmileInfo>();

            if ((oldInfo == null) || (oldInfo.ContinuousFunction == null))
            {
                return(Constants.EmptySeries);
            }

            double futPx = oldInfo.F;
            double dT    = oldInfo.dT;

            if (m_executeCommand)
            {
                string msg = String.Format("[{0}.StartButton] Strike: {1}", GetType().Name, m_strike);
                m_context.Log(msg, MessageType.Info, false);
            }

            #region 1. Список страйков
            HashSet <string> serList = StrikeList;
            serList.Clear();

            IOptionStrikePair[] pairs;
            if (Double.IsNaN(m_strikeStep) || (m_strikeStep <= Double.Epsilon))
            {
                pairs = optSer.GetStrikePairs().ToArray();
            }
            else
            {
                // Выделяем страйки, которые нацело делятся на StrikeStep
                pairs = (from p in optSer.GetStrikePairs()
                         let test = m_strikeStep * Math.Round(p.Strike / m_strikeStep)
                                    where DoubleUtil.AreClose(p.Strike, test)
                                    select p).ToArray();

                // [2015-12-24] Если шаг страйков по ошибке задан совершенно неправильно,
                // то в коллекцию ставим все имеющиеся страйки.
                // Пользователь потом разберется
                if (pairs.Length <= 0)
                {
                    pairs = optSer.GetStrikePairs().ToArray();
                }
            }
            //if (pairs.Length < 2)
            //    return Constants.EmptyListDouble;

            foreach (IOptionStrikePair pair in pairs)
            {
                double k = pair.Strike;
                serList.Add(k.ToString(StrikeFormat, CultureInfo.InvariantCulture));
            }
            #endregion 1. Список страйков

            InteractiveSeries        res           = Constants.EmptySeries;
            List <InteractiveObject> controlPoints = new List <InteractiveObject>();

            #region 2. Формируем улыбку просто для отображения текущего положения потенциальной котировки
            // При нулевом рабочем объёме не утруждаемся рисованием лишних линий
            if (!DoubleUtil.IsZero(m_qty))
            {
                for (int j = 0; j < pairs.Length; j++)
                {
                    var    pair  = pairs[j];
                    double sigma = oldInfo.ContinuousFunction.Value(pair.Strike) + m_shiftIv;
                    if (!DoubleUtil.IsPositive(sigma))
                    {
                        //string msg = String.Format("[DEBUG:{0}] Invalid sigma:{1} for strike:{2}", GetType().Name, sigma, nodeInfo.Strike);
                        //m_context.Log(msg, MessageType.Warning, true);
                        continue;
                    }

                    //bool isCall = (futPx <= pair.Strike);
                    bool isCall;
                    if (m_optionType == StrikeType.Call)
                    {
                        isCall = true;
                    }
                    else if (m_optionType == StrikeType.Put)
                    {
                        isCall = false;
                    }
                    else
                    {
                        isCall = (futPx <= pair.Strike);
                    }

                    StrikeType optionType = isCall ? StrikeType.Call : StrikeType.Put;
                    Contract.Assert(pair.Tick < 1, $"#1 На тестовом контуре Дерибит присылает неправильный шаг цены! Tick:{pair.Tick}; Decimals:{pair.Put.Security.Decimals}");
                    double theorOptPxDollars = FinMath.GetOptionPrice(futPx, pair.Strike, dT, sigma, oldInfo.RiskFreeRate, isCall);
                    // Сразу(!!!) переводим котировку из баксов в битки
                    double theorOptPxBitcoins = theorOptPxDollars / scaleMult;

                    // Сдвигаем цену в долларах (с учетом ш.ц. в баксах)
                    theorOptPxDollars += m_shiftPriceStep * pair.Tick * scaleMult;
                    theorOptPxDollars  = Math.Round(theorOptPxDollars / (pair.Tick * scaleMult)) * (pair.Tick * scaleMult);

                    // Сдвигаем цену в биткойнах (с учетом ш.ц. в битках)
                    theorOptPxBitcoins += m_shiftPriceStep * pair.Tick;
                    theorOptPxBitcoins  = Math.Round(theorOptPxBitcoins / pair.Tick) * pair.Tick;
                    if ((!DoubleUtil.IsPositive(theorOptPxBitcoins)) || (!DoubleUtil.IsPositive(theorOptPxDollars)))
                    {
                        //string msg = String.Format("[DEBUG:{0}] Invalid theorOptPx:{1} for strike:{2}", GetType().Name, theorOptPx, nodeInfo.Strike);
                        //m_context.Log(msg, MessageType.Warning, true);
                        continue;
                    }

                    // Пересчитываем сигму обратно, ЕСЛИ мы применили сдвиг цены в абсолютном выражении
                    if (m_shiftPriceStep != 0)
                    {
                        // Обратный пересчет в волатильность
                        sigma = FinMath.GetOptionSigma(futPx, pair.Strike, dT, theorOptPxDollars, oldInfo.RiskFreeRate, isCall);
                        if (!DoubleUtil.IsPositive(sigma))
                        {
                            //string msg = String.Format("[DEBUG:{0}] Invalid sigma:{1} for strike:{2}", GetType().Name, sigma, nodeInfo.Strike);
                            //m_context.Log(msg, MessageType.Warning, true);
                            continue;
                        }
                    }

                    // ReSharper disable once UseObjectOrCollectionInitializer
                    SmileNodeInfo nodeInfo = new SmileNodeInfo();
                    var           secDesc  = isCall ? pair.CallFinInfo.Security : pair.PutFinInfo.Security;
                    nodeInfo.F            = oldInfo.F;
                    nodeInfo.dT           = oldInfo.dT;
                    nodeInfo.RiskFreeRate = oldInfo.RiskFreeRate;
                    nodeInfo.Strike       = pair.Strike;
                    nodeInfo.Sigma        = sigma;
                    nodeInfo.OptPx        = theorOptPxBitcoins;
                    nodeInfo.OptionType   = isCall ? StrikeType.Call : StrikeType.Put;
                    nodeInfo.Pair         = pair;

                    nodeInfo.Symbol   = secDesc.Name;
                    nodeInfo.DSName   = secDesc.DSName;
                    nodeInfo.Expired  = secDesc.Expired;
                    nodeInfo.FullName = secDesc.FullName;

                    // ReSharper disable once UseObjectOrCollectionInitializer
                    InteractivePointActive tmp = new InteractivePointActive();

                    tmp.IsActive     = true;
                    tmp.ValueX       = pair.Strike;
                    tmp.ValueY       = sigma;
                    tmp.DragableMode = DragableMode.Yonly;
                    tmp.Tooltip      = String.Format(CultureInfo.InvariantCulture,
                                                     " F: {0}\r\n K: {1}; IV: {2:P2}\r\n {3} px {4}",
                                                     futPx, pair.Strike, sigma, optionType, theorOptPxBitcoins);

                    tmp.Tag = nodeInfo;

                    //tmp.Color = Colors.White;
                    if (m_qty > 0)
                    {
                        tmp.Geometry = Geometries.Triangle;
                    }
                    else if (m_qty < 0)
                    {
                        tmp.Geometry = Geometries.TriangleDown;
                    }
                    else
                    {
                        tmp.Geometry = Geometries.None;
                    }

                    InteractiveObject obj = new InteractiveObject();
                    obj.Anchor = tmp;

                    controlPoints.Add(obj);
                }

                // ReSharper disable once UseObjectOrCollectionInitializer
                res = new InteractiveSeries(); // Здесь так надо -- мы делаем новую улыбку
                res.ControlPoints = new ReadOnlyCollection <InteractiveObject>(controlPoints);

                // ReSharper disable once UseObjectOrCollectionInitializer
                SmileInfo sInfo = new SmileInfo();
                sInfo.F            = futPx;
                sInfo.dT           = dT;
                sInfo.Expiry       = oldInfo.Expiry;
                sInfo.ScriptTime   = oldInfo.ScriptTime;
                sInfo.RiskFreeRate = oldInfo.RiskFreeRate;
                sInfo.BaseTicker   = oldInfo.BaseTicker;

                res.Tag = sInfo;

                if (controlPoints.Count > 0)
                {
                    res.ClickEvent -= InteractiveSplineOnQuoteIvEvent;
                    res.ClickEvent += InteractiveSplineOnQuoteIvEvent;

                    m_clickableSeries = res;
                }
            }
            #endregion 2. Формируем улыбку просто для отображения текущего положения потенциальной котировки

            PositionsManager posMan = PositionsManager.GetManager(m_context);
            if (m_cancelAllLong)
            {
                posMan.DropAllLongIvTargets(m_context);
            }
            if (m_cancelAllShort)
            {
                posMan.DropAllShortIvTargets(m_context);
            }

            #region 4. Котирование
            {
                var longTargets  = posMan.GetIvTargets(true);
                var shortTargets = posMan.GetIvTargets(false);
                var ivTargets    = longTargets.Union(shortTargets).ToList();
                for (int j = 0; j < ivTargets.Count; j++)
                {
                    var ivTarget = ivTargets[j];

                    // PROD-6102 - Требуется точное совпадение опционной серии
                    if (optSer.ExpirationDate.Date != ivTarget.SecInfo.Expiry.Date)
                    {
                        // Вывести предупреждение???
                        continue;
                    }

                    IOptionStrikePair pair;
                    double            k = ivTarget.SecInfo.Strike;
                    if (!optSer.TryGetStrikePair(k, out pair))
                    {
                        // Вывести предупреждение???
                        continue;
                    }

                    double      sigma;
                    QuoteIvMode quoteMode = ivTarget.QuoteMode;
                    if (quoteMode == QuoteIvMode.Absolute)
                    {
                        sigma = ivTarget.EntryIv;
                    }
                    else
                    {
                        sigma = oldInfo.ContinuousFunction.Value(k) + ivTarget.EntryIv;
                        if (!DoubleUtil.IsPositive(sigma))
                        {
                            //string msg = String.Format("[DEBUG:{0}] Invalid sigma:{1} for strike:{2}", GetType().Name, sigma, nodeInfo.Strike);
                            //m_context.Log(msg, MessageType.Warning, true);
                            continue;
                        }
                    }

                    //bool isCall = (futPx <= pair.Strike);
                    // Определяю тип опциона на основании информации в Задаче
                    StrikeType taskOptionType = StrikeType.Any;
                    if (ivTarget.SecInfo.StrikeType.HasValue)
                    {
                        taskOptionType = ivTarget.SecInfo.StrikeType.Value;
                    }

                    bool isCall;
                    if (taskOptionType == StrikeType.Call)
                    {
                        isCall = true;
                    }
                    else if (taskOptionType == StrikeType.Put)
                    {
                        isCall = false;
                    }
                    else
                    {
                        isCall = (futPx <= pair.Strike); // Это аварийная ситуация?
                    }
                    StrikeType optionType = isCall ? StrikeType.Call : StrikeType.Put;
                    Contract.Assert(pair.Tick < 1, $"#3 На тестовом контуре Дерибит присылает неправильный шаг цены! Tick:{pair.Tick}; Decimals:{pair.Put.Security.Decimals}");
                    double theorOptPxDollars = FinMath.GetOptionPrice(futPx, pair.Strike, dT, sigma, oldInfo.RiskFreeRate, isCall);
                    // Сразу(!!!) переводим котировку из баксов в битки
                    double theorOptPxBitcoins = theorOptPxDollars / scaleMult;

                    // Сдвигаем цену в долларах (с учетом ш.ц. в баксах)
                    theorOptPxDollars += ivTarget.EntryShiftPrice * pair.Tick * scaleMult;
                    theorOptPxDollars  = Math.Round(theorOptPxDollars / (pair.Tick * scaleMult)) * (pair.Tick * scaleMult);

                    // Сдвигаем цену в биткойнах (с учетом ш.ц. в битках)
                    theorOptPxBitcoins += ivTarget.EntryShiftPrice * pair.Tick;
                    theorOptPxBitcoins  = Math.Round(theorOptPxBitcoins / pair.Tick) * pair.Tick;
                    if ((!DoubleUtil.IsPositive(theorOptPxBitcoins)) || (!DoubleUtil.IsPositive(theorOptPxDollars)))
                    {
                        //string msg = String.Format("[DEBUG:{0}] Invalid theorOptPx:{1} for strike:{2}", GetType().Name, theorOptPx, nodeInfo.Strike);
                        //m_context.Log(msg, MessageType.Warning, true);
                        continue;
                    }

                    IOptionStrike optStrike = isCall ? pair.Call : pair.Put;
                    ISecurity     sec       = optStrike.Security;
                    double        totalQty  = posMan.GetTotalQty(sec, m_context.BarsCount, TotalProfitAlgo.AllPositions, ivTarget.IsLong);
                    // Поскольку котирование страйка по волатильности -- это вопрос набора нужного количества СТРЕДДЛОВ,
                    // то учитывать надо суммарный объём опционов как в колах, так и в путах.
                    // НО ЗАДАЧУ-ТО Я СТАВЛЮ ДЛЯ КОНКРЕТНОГО ИНСТРУМЕНТА!
                    // Как быть?
                    //double totalQty = posMan.GetTotalQty(pair.Put.Security, m_context.BarsCount, TotalProfitAlgo.AllPositions, ivTarget.IsLong);
                    //totalQty += posMan.GetTotalQty(pair.Call.Security, m_context.BarsCount, TotalProfitAlgo.AllPositions, ivTarget.IsLong);
                    double targetQty = Math.Abs(ivTarget.TargetShares) - totalQty;
                    // Если имеется дробный LotTick (как в Дерибит к примеру), то надо предварительно округлить
                    targetQty = sec.RoundShares(targetQty);
                    if (targetQty > 0)
                    {
                        string note = String.Format(CultureInfo.InvariantCulture,
                                                    "{0}; ActQty:{1}; Px:{2}; IV:{3:P2}",
                                                    ivTarget.EntryNotes, targetQty, theorOptPxBitcoins, sigma);
                        if (ivTarget.IsLong)
                        {
                            posMan.BuyAtPrice(m_context, sec, targetQty, theorOptPxBitcoins, ivTarget.EntrySignalName, note);
                        }
                        else
                        {
                            posMan.SellAtPrice(m_context, sec, targetQty, theorOptPxBitcoins, ivTarget.EntrySignalName, note);
                        }
                    }
                    else
                    {
                        string msg = String.Format(CultureInfo.InvariantCulture,
                                                   "IvTarget cancelled. SignalName:{0}; Notes:{1}", ivTarget.EntrySignalName, ivTarget.EntryNotes);
                        posMan.CancelVolatility(m_context, ivTarget, msg);

                        // TODO: потом убрать из ГЛ
                        m_context.Log(msg, MessageType.Info, true, new Dictionary <string, object> {
                            { "VOLATILITY_ORDER_CANCELLED", msg }
                        });
                    }
                }
            }
            #endregion 4. Котирование

            #region 5. Торговля
            if (m_executeCommand && (!DoubleUtil.IsZero(m_qty)))
            {
                double k;
                if ((!Double.TryParse(m_strike, out k)) &&
                    (!Double.TryParse(m_strike, NumberStyles.Any, CultureInfo.InvariantCulture, out k)))
                {
                    return(res);
                }

                var pair = (from p in pairs where DoubleUtil.AreClose(k, p.Strike) select p).SingleOrDefault();
                if (pair == null)
                {
                    return(res);
                }

                InteractiveObject obj = (from o in controlPoints where DoubleUtil.AreClose(k, o.Anchor.ValueX) select o).SingleOrDefault();
                if (obj == null)
                {
                    return(res);
                }

                // TODO: для режима котирования в абсолютных числах сделать отдельную ветку
                //double iv = obj.Anchor.ValueY;
                const QuoteIvMode QuoteMode = QuoteIvMode.Relative;
                if (posMan.BlockTrading)
                {
                    string msg = String.Format(RM.GetString("OptHandlerMsg.PositionsManager.TradingBlocked"),
                                               m_context.Runtime.TradeName + ":QuoteIv");
                    m_context.Log(msg, MessageType.Warning, true);
                    return(res);
                }

                // Выбираю тип инструмента пут или колл?
                bool isCall;
                if (m_optionType == StrikeType.Call)
                {
                    isCall = true;
                }
                else if (m_optionType == StrikeType.Put)
                {
                    isCall = false;
                }
                else
                {
                    isCall = (futPx <= k);
                }

                double iv     = m_shiftIv;
                int    shift  = m_shiftPriceStep;
                var    option = isCall ? pair.Call : pair.Put;
                if (m_qty > 0)
                {
                    // Пересчитываю целочисленный параметр Qty в фактические лоты конкретного инструмента
                    double actQty  = m_qty * option.LotTick;
                    string sigName = String.Format(CultureInfo.InvariantCulture,
                                                   "Qty:{0}; IV:{1:P2}+{2}; dT:{3}; Mode:{4}", actQty, iv, shift, dT, QuoteMode);
                    posMan.BuyVolatility(m_context, option, Math.Abs(actQty), QuoteMode, iv, shift, "BuyVola", sigName);

                    m_context.Log(sigName, MessageType.Info, false);
                }
                else if (m_qty < 0)
                {
                    // Пересчитываю целочисленный параметр Qty в фактические лоты конкретного инструмента
                    double actQty  = m_qty * option.LotTick;
                    string sigName = String.Format(CultureInfo.InvariantCulture,
                                                   "Qty:{0}; IV:{1:P2}+{2}; dT:{3}; Mode:{4}", actQty, iv, shift, dT, QuoteMode);
                    posMan.SellVolatility(m_context, option, Math.Abs(actQty), QuoteMode, iv, shift, "SellVola", sigName);

                    m_context.Log(sigName, MessageType.Info, false);
                }
            }
            #endregion 5. Торговля

            return(res);
        }
コード例 #2
0
        /// <summary>
        /// Метод под флаг TemplateTypes.INTERACTIVESPLINE
        /// </summary>
        public InteractiveSeries Execute(InteractiveSeries smile, IOptionSeries optSer, InteractiveSeries quoteIv, double scaleMult, int barNum)
        {
            if ((smile == null) || (optSer == null))
            {
                return(Constants.EmptySeries);
            }

            int barsCount = m_context.BarsCount;

            if (!m_context.IsLastBarUsed)
            {
                barsCount--;
            }
            if (barNum < barsCount - 1)
            {
                return(Constants.EmptySeries);
            }

            SmileInfo oldInfo = smile.GetTag <SmileInfo>();

            if ((oldInfo == null) || (oldInfo.ContinuousFunction == null))
            {
                return(Constants.EmptySeries);
            }

            double futPx = oldInfo.F;
            double dT    = oldInfo.dT;

            // 1. Формируем маркеры заявок
            List <InteractiveObject> controlPoints          = new List <InteractiveObject>();
            PositionsManager         posMan                 = PositionsManager.GetManager(m_context);
            IList <PositionsManager.IvTargetInfo> ivTargets = posMan.GetIvTargets(m_isLong, true);

            for (int j = 0; j < ivTargets.Count; j++)
            {
                var ivTarget = ivTargets[j];

                // PROD-6102 - Требуется точное совпадение опционной серии
                if (optSer.ExpirationDate.Date != ivTarget.SecInfo.Expiry.Date)
                {
                    // Вывести предупреждение???
                    continue;
                }

                IOptionStrikePair pair;
                double            k = ivTarget.SecInfo.Strike;
                if (!optSer.TryGetStrikePair(k, out pair))
                {
                    // Вывести предупреждение???
                    continue;
                }

                double      sigma;
                QuoteIvMode quoteMode = ivTarget.QuoteMode;
                if (quoteMode == QuoteIvMode.Absolute)
                {
                    sigma = ivTarget.EntryIv;
                }
                else
                {
                    sigma = oldInfo.ContinuousFunction.Value(k) + ivTarget.EntryIv;
                    if (!DoubleUtil.IsPositive(sigma))
                    {
                        //string msg = String.Format("[DEBUG:{0}] Invalid sigma:{1} for strike:{2}", GetType().Name, sigma, nodeInfo.Strike);
                        //m_context.Log(msg, MessageType.Warning, true);
                        continue;
                    }
                }

                bool isCall = (futPx <= k);
                if ((ivTarget.SecInfo.StrikeType != null) &&
                    (ivTarget.SecInfo.StrikeType.Value != StrikeType.Any))
                {
                    isCall = (ivTarget.SecInfo.StrikeType.Value == StrikeType.Call);
                }
                StrikeType optionType = isCall ? StrikeType.Call : StrikeType.Put;
                Contract.Assert(pair.Tick < 1, $"На тестовом контуре Дерибит присылает неправильный шаг цены! Tick:{pair.Tick}; Decimals:{pair.Put.Security.Decimals}");
                double theorOptPxDollars = FinMath.GetOptionPrice(futPx, pair.Strike, dT, sigma, oldInfo.RiskFreeRate, isCall);
                // Сразу(!!!) переводим котировку из баксов в битки
                double theorOptPxBitcoins = theorOptPxDollars / scaleMult;

                // Сдвигаем цену в долларах (с учетом ш.ц. в баксах)
                theorOptPxDollars += ivTarget.EntryShiftPrice * pair.Tick * scaleMult;
                theorOptPxDollars  = Math.Round(theorOptPxDollars / (pair.Tick * scaleMult)) * (pair.Tick * scaleMult);

                // Сдвигаем цену в биткойнах (с учетом ш.ц. в битках)
                theorOptPxBitcoins += ivTarget.EntryShiftPrice * pair.Tick;
                theorOptPxBitcoins  = Math.Round(theorOptPxBitcoins / pair.Tick) * pair.Tick;
                if ((!DoubleUtil.IsPositive(theorOptPxBitcoins)) || (!DoubleUtil.IsPositive(theorOptPxDollars)))
                {
                    //string msg = String.Format("[DEBUG:{0}] Invalid theorOptPx:{1} for strike:{2}", GetType().Name, theorOptPx, nodeInfo.Strike);
                    //m_context.Log(msg, MessageType.Warning, true);
                    continue;
                }

                // Пересчитываем сигму обратно, ЕСЛИ мы применили сдвиг цены в абсолютном выражении
                if (ivTarget.EntryShiftPrice != 0)
                {
                    sigma = FinMath.GetOptionSigma(futPx, pair.Strike, dT, theorOptPxDollars, oldInfo.RiskFreeRate, isCall);
                    if (!DoubleUtil.IsPositive(sigma))
                    {
                        //string msg = String.Format("[DEBUG:{0}] Invalid sigma:{1} for strike:{2}", GetType().Name, sigma, nodeInfo.Strike);
                        //m_context.Log(msg, MessageType.Warning, true);
                        continue;
                    }
                }

                double totalQty;
                if (isCall)
                {
                    totalQty = posMan.GetTotalQty(pair.Call.Security, m_context.BarsCount, TotalProfitAlgo.AllPositions, ivTarget.IsLong);
                }
                else
                {
                    totalQty = posMan.GetTotalQty(pair.Put.Security, m_context.BarsCount, TotalProfitAlgo.AllPositions, ivTarget.IsLong);
                }
                double targetQty = Math.Abs(ivTarget.TargetShares) - totalQty;

                // ReSharper disable once UseObjectOrCollectionInitializer
                InteractivePointActive tmp = new InteractivePointActive();

                // Попробуем по-простому?
                tmp.Tag = ivTarget;

                tmp.IsActive     = true;
                tmp.ValueX       = k;
                tmp.ValueY       = sigma;
                tmp.DragableMode = DragableMode.None;
                if (ivTarget.EntryShiftPrice == 0)
                {
                    tmp.Tooltip = String.Format(CultureInfo.InvariantCulture,
                                                " F: {0}\r\n K: {1}; IV: {2:P2}\r\n {3} px {4} rIV {5:P2} @ {6}",
                                                futPx, k, sigma, optionType, theorOptPxBitcoins, ivTarget.EntryIv, targetQty);
                }
                else
                {
                    string shiftStr = (ivTarget.EntryShiftPrice > 0) ? "+" : "-";
                    shiftStr    = shiftStr + Math.Abs(ivTarget.EntryShiftPrice) + "ps";
                    tmp.Tooltip = String.Format(CultureInfo.InvariantCulture,
                                                " F: {0}\r\n K: {1}; IV: {2:P2}\r\n {3} px {4} rIV {5:P2} {6} @ {7}",
                                                futPx, k, sigma, optionType, theorOptPxBitcoins, ivTarget.EntryIv, shiftStr, targetQty);
                }

                //tmp.Color = Colors.White;
                //if (m_qty > 0)
                //    tmp.Geometry = Geometries.Triangle;
                //else if (m_qty < 0)
                //    tmp.Geometry = Geometries.TriangleDown;
                //else
                //    tmp.Geometry = Geometries.None;

                InteractiveObject obj = new InteractiveObject();
                obj.Anchor = tmp;

                controlPoints.Add(obj);
            }

            // ReSharper disable once UseObjectOrCollectionInitializer
            InteractiveSeries res = new InteractiveSeries(); // Здесь так надо -- мы делаем новую улыбку

            res.ControlPoints = new ReadOnlyCollection <InteractiveObject>(controlPoints);

            if (controlPoints.Count > 0)
            {
                res.ClickEvent -= InteractiveSplineOnClickEvent;
                res.ClickEvent += InteractiveSplineOnClickEvent;

                m_clickableSeries = res;
            }

            return(res);
        }