← 返回

S#.Shell. Manual

Часть 1. Starting up

Первым делом неплохо было бы разобраться с коннекторами.

Уже есть Альфадирект и Квик. Мне бы хотелось подрубить свой кастомный для IB. Но незадача: В каркасе старая версия S#. Это грозит проблемами в работе с гидрой и коннекторами. У меня возникли сразу, потому что коннектор был на 4.1.19.1.

Значит заодно и обновлюсь.

Удаляю все, что связано со stocksharp и Ecng из reference, и добавляю заново все свежее. У меня вот такой список получился.

1

После обновления, ничего не заработает.

Поправил в ConnectionEngine для квика и для альфа аналогично.

2

После ребилда все должно взлететь.

Добавил, естественно, нужные Refercences, и создание коннектора по аналогии.

3

4

Настройки. Тут есть целый менеджер настроек, который отвечает за сохранение, и т.д.

Добавил в список коннекторов и нужные настройки в SettingsProperties

5

6

Ну вот и все.

7

7

评论 (27)

登录创建账户, 登录或注册以发表评论

Kazai Mazai
Kazai Mazai 作者

Часть 2. Почва для удобного добавления стратегий.

В прошлый раз я немного поторопился с обновлением версии S#. Нашлись кое-какие неполадки в коннекторе и пришлось откатиться. Тем не менее, там все актуально за исключением парочки нюансов при создании новых подключений. Главное делать по аналогии, и все будет хорошо👍

Добавил базовые свойства стратегии.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using StockSharp.BusinessEntities;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;

namespace Robot
{
  public class BaseStrategyProperties:INotifyPropertyChanged
	{
		private string _name = "Новая стратегия";
		private string _security = "TEST@TEST";
		private string _portfolio = "TEST_PORTFOLIO";
        private decimal _volume = 1;

        private Dictionary<long, Order> _ordersByTransactionId = new Dictionary<long, Order>();
        private Dictionary<long, Security> _securitiesByTransactionId = new Dictionary<long, Security>();
        private Dictionary<long, Portfolio> _portfoliosByTransactionId = new Dictionary<long, Portfolio>();
        private Dictionary<long, IEnumerable<Trade>> _tradesByTransactionId = new Dictionary<long, IEnumerable<Trade>>();
        private Dictionary<long, Exchange> _exchangesByTransactionId = new Dictionary<long, Exchange>();

		private bool _schedulerIsEnabled;
		private List<TimeRangeProperties> _schedule = new List<TimeRangeProperties>(10);
 

		[DisplayName(@"Название")]
		[Description(@"Название стратегии")]
		[Category(@"Основные")]
		//[CategoryOrder(0)]
		[PropertyOrder(0)]
		public string Name
		{
			get { return _name; }
			set
			{
				_name = value;
				OnPropertyChanged("Name");
			}
		}

		[DisplayName(@"Инструмент")]
		[Description(@"Код торгового инструмента")]
		[Category(@"Основные")]
		//[CategoryOrder(0)]
		[PropertyOrder(1)]
		[Editor(typeof(SecurityIdEditor), typeof(SecurityIdEditor))]
		public string Security
		{
			get { return _security; }
			set
			{
				_security = value;
				OnPropertyChanged("Security");
			}
		}

		[DisplayName(@"Портфель")]
		[Description(@"Имя портфеля")]
		[Category(@"Основные")]
		//[CategoryOrder(0)]
		[PropertyOrder(3)]
		[Editor(typeof(PortfolioNameEditor), typeof(PortfolioNameEditor))]
		public string Portfolio
		{
			get { return _portfolio; }
			set
			{
				_portfolio = value;
				OnPropertyChanged("Portfolio");
			}
		}

        [DisplayName(@"Объем")]
        [Description(@"Объем для открытия позиций")]
        [Category(@"Основные")]
        //[CategoryOrder(0)]
        [PropertyOrder(4)]
        public decimal Volume
        {
            get { return _volume; }
            set
            {
                if (value < 0)
                {
                    return;
                }

                _volume = value;
                OnPropertyChanged("Volume");
            }
        }

 
 

		[Category(@"Планировщик")]
		//[CategoryOrder(5)]
		[DisplayName(@"Включен")]
		[Description(@"Включен ли запуск по расписанию для данной стратегии.")]
		[PropertyOrder(0)]
		public bool SchedulerIsEnabled
		{
			get { return _schedulerIsEnabled; }
			set
			{
				_schedulerIsEnabled = value;
				OnPropertyChanged("SchedulerIsEnabled");
			}
		}

		[Category(@"Планировщик")]
		//[CategoryOrder(5)]
		[DisplayName(@"Время работы")]
		[Description(@"Настройка времени автоматической работы стратегии.")]
		[PropertyOrder(1)]
		[Editor(typeof(ScheduleEditor), typeof(ScheduleEditor))]
		public List<TimeRangeProperties> Schedule
		{
			get { return _schedule; }
			set
			{
				_schedule = value;
				OnPropertyChanged("Schedule");
			}
		}

        [DisplayName(@"ОрдерПоАйдиТранзакции")]
        [Description(@"ОрдерПоАйдиТранзакции")]
        [Category(@"Для работы")]
        [PropertyOrder(0)]
        public Dictionary<long, Order> OrdersByTransactionId
        {
            get { return _ordersByTransactionId; }
            set
            {
                _ordersByTransactionId = value;
                OnPropertyChanged("OrdersByTransactionId");
            }
        }

        [DisplayName(@"ИнструментПоАйдиТранзакции")]
        [Description(@"ИнструментПоАйдиТранзакции")]
        [Category(@"Для работы")]
        [PropertyOrder(0)]
        public Dictionary<long, Security> SecuritiesByTransactionId
        {
            get { return _securitiesByTransactionId; }
            set
            {
                _securitiesByTransactionId = value;
                OnPropertyChanged("SecuritiesByTransactionId");
            }
        }

        [DisplayName(@"СделкиПоАйдиТранзакции")]
        [Description(@"СделкиПоАйдиТранзакции")]
        [Category(@"Для работы")]
        [PropertyOrder(0)]
        public Dictionary<long, IEnumerable<Trade>> TradesByTransactionId
        {
            get { return _tradesByTransactionId; }
            set
            {
                _tradesByTransactionId = value;
                OnPropertyChanged("TradesByTransactionId");
            }
        }

        [DisplayName(@"ПортфелиПоАйдиТранзакции")]
        [Description(@"ПортфелиПоАйдиТранзакции")]
        [Category(@"Для работы")]
        [PropertyOrder(0)]
        public Dictionary<long, Portfolio> PortfoliosByTransactionId
        {
            get { return _portfoliosByTransactionId; }
            set
            {
                _portfoliosByTransactionId = value;
                OnPropertyChanged("PortfoliosByTransactionId");
            }
        }
        [DisplayName(@"БиржиПоАйдиТранзакции")]
        [Description(@"БиржиПоАйдиТранзакции")]
        [Category(@"Для работы")]
        [PropertyOrder(0)]
        public Dictionary<long, Exchange> ExchangesByTransactionId
        {
            get { return _exchangesByTransactionId; }
            set
            {
                _exchangesByTransactionId = value;
                OnPropertyChanged("ExchangesByTransactionId");
            }
        }

       
		public event PropertyChangedEventHandler PropertyChanged;

		public void OnPropertyChanged(string name)
		{
			var handler = PropertyChanged;
			if (handler != null)
			{
				handler(this, new PropertyChangedEventArgs(name));
			}
		}
	}
}

и базовую стратегию, восстанавливающую ордера,сделки и позиции при перезапуске.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using StockSharp.Algo;
using StockSharp.BusinessEntities;

namespace Robot.Base
{
   public class BaseSavingStatesStrategy: BaseStrategy
    {
       public BaseStrategyProperties Params { set; get; }
       private bool _isFirstStart;

       public BaseSavingStatesStrategy()
       {
           _isFirstStart = true;

       }

       protected void SaveStateToParams()
       {

           foreach (var order in Orders)
           {
               var transactionId = order.TransactionId;
               if (Params.OrdersByTransactionId.ContainsKey(transactionId))
               {
                   Params.OrdersByTransactionId[transactionId] = order;
               }
               else
               {
                   Params.OrdersByTransactionId.Add(transactionId, order);
               }

               var myTrades = order.GetTrades();
               List<Trade> trades = new List<Trade>();
               foreach (var myTrade in myTrades)
               {
                   trades.Add(myTrade.Trade);
               }
               if (Params.TradesByTransactionId.ContainsKey(transactionId))
               {
                   Params.TradesByTransactionId[transactionId] = trades;
               }
               else
               {
                   Params.TradesByTransactionId.Add(transactionId, trades);
               }

               if (Params.SecuritiesByTransactionId.ContainsKey(transactionId))
               {
                   Params.SecuritiesByTransactionId[transactionId] = order.Security;
               } 
               else
               {
                   Params.SecuritiesByTransactionId.Add(transactionId, order.Security);
               }
               if (Params.ExchangesByTransactionId.ContainsKey(transactionId))
               {
                   Params.ExchangesByTransactionId[transactionId] = order.Security.ExchangeBoard.Exchange;
               }
               else
               {
                   Params.ExchangesByTransactionId.Add(transactionId, order.Security.ExchangeBoard.Exchange);
               }
               if (Params.PortfoliosByTransactionId.ContainsKey(transactionId))
               {
                   Params.PortfoliosByTransactionId[transactionId] = order.Portfolio;
               }
               else
               {
                   Params.PortfoliosByTransactionId.Add(transactionId, order.Portfolio);

               }
              
           }
       }

       protected void LoadStateFromParams()
       {
               foreach (var transactionId in Params.OrdersByTransactionId.Keys)
           {
               var order = Params.OrdersByTransactionId[transactionId];
               if (order != null)
               {
                   order.Trader = Trader;
                   var security = Params.SecuritiesByTransactionId[transactionId];
                   if (security != null)
                   {

                       var exchange = Params.ExchangesByTransactionId[transactionId];
                       if (exchange != null) security.ExchangeBoard.Exchange = exchange;

                       security.Trader = Trader;
                       order.Security = security;

                   }

                   var portfolio = Params.PortfoliosByTransactionId[transactionId];
                   if (portfolio != null)
                   {
                       Portfolio.Trader = Trader;
                       order.Portfolio = portfolio;
                   }

                   var myTrades = new List<MyTrade>();
                   var trades = Params.TradesByTransactionId[transactionId];

                   if (trades != null)
                       foreach (var trade in trades)
                       {
                           var myTrade = new MyTrade();
                           myTrade.Order = order;
                           myTrade.Trade = trade;
                           myTrades.Add(myTrade);

                       }
                   if (myTrades.Count > 0)
                   {
                       AttachOrder(order, myTrades);
                   }
                    
               }
           }

       }

       protected  override  void OnStarted()
       {
           base.OnStarted();

         if(_isFirstStart)
         {
             _isFirstStart = false;
             LoadStateFromParams();

         }

       }

       protected override void OnStopped()
       {
           SaveStateToParams();
           
           base.OnStopped();
       }

      
    }
}

Правильней было бы сохранять все в бд, используя EntityRegistry, но как-то с базой гидры не хотело взлетать. А так работает, ну и окей. Не так уж часто стратегии перезапускаются в самом деле.

Немного поменял добавление стратегий. Теперь, когда нужно будет включить новую стратегию, можно просто сделать для нее команду добавления, приписать в xml и она появится в менюшке добавления. Главное - все по аналогии.👍

1

Bond
Kazai Mazai
Kazai Mazai 作者

Часть 3.1. Добавление своих стратегий.

Эта история полна нюансов.

В шелле хорошо показано, на примере одной стратегии как все работает и т.д.

Добавить свою взамен существующей тоже легко - меняем да и все. Либо по-варварски вставляем в существующую новые внутренности.

Но тут не так все гибко, чтобы наряду с существующими в два щелчка добавить новые стратегии и чтобы все прекрасно работало и сохранялось.

Можно чуть менее чем полностью переделать SettingsEngine, половину кода связанного с добавлением стратегий, и только потом добавлять какие угодно новые стратегии в два щелчка.

А можно сделать минимум изменений, прикрутив маленькие костыли и затем добавлять новые стратегии не в 2 а в 6 щелчков, о чем и пойдет речь дальше.

Я уже подготовил небольшую почву, наподобие той, про которую было во 2-ой части. Когда мне удастся подружиться с ТФС, эти изменения уже будут в шелле.

Может показаться, что кода много, но на самом деле там 90% копи паст.

Добавляем заготовку для стратегии, для параметров и параметров тестирования, которые когда-нибудь наполним содержанием.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Robot.Strategies
{
    public class HustleEveryDayStrategyProperties :BaseShellStrategyProperties
    {
        

    }

    public class  HustleEveryDayStrategyTestingProperties:BaseShellTestingProperties
    {
        

    }


   public class HustleEveryDayStrategy:BaseShellStrategy
    {


    }
}

Идем в SettingsEngine, добавляем методы для сохранения и загрузки параметров для нового типа стратегий.


private const string HustleEveryDayStrategies = @"HustleEveryDayStrategies.xml";
private const string TestingHustleEveryDayStrategies = @"TestingHustleEveryDayStrategies.xml";

        public void SaveHustleEveryDayStrategiesProperties(List<HustleEveryDayStrategyProperties> properties)
        {
            this.AddInfoLog("Сохранение стратегий в {0}", HustleEveryDayStrategies);
            using (var writer = new StreamWriter(HustleEveryDayStrategies, false))
            {
                var mySerializer = new XmlSerializer(typeof(List<HustleEveryDayStrategyProperties>));
                mySerializer.Serialize(writer, properties);
            }
        }


        public List<HustleEveryDayStrategyProperties> LoadHustleEveryDayStrategiesProperties()
        {
            this.AddInfoLog("Загрузка стратегий из {0}", HustleEveryDayStrategies);

            var result = new List<HustleEveryDayStrategyProperties>();

            if (File.Exists(HustleEveryDayStrategies))
            {
                try
                {
                    using (var reader = new StreamReader(HustleEveryDayStrategies))
                    {
                        var x = new XmlSerializer(typeof(List<HustleEveryDayStrategyProperties>));
                        result = (List<HustleEveryDayStrategyProperties>)x.Deserialize(reader);
                    }
                }
                catch (Exception ex)
                {
                    this.AddErrorLog("Ошибка загрузки {0}: {1}", HustleEveryDayStrategies, ex.Message);
                }
            }

            return result;
        }

        public void SaveTestingHustleEveryDayStrategyProperties(List<HustleEveryDayStrategyTestingProperties> properties)
        {
            this.AddInfoLog("Сохранение тестовых стратегий в {0}", TestingHustleEveryDayStrategies);
            using (var writer = new StreamWriter(TestingHustleEveryDayStrategies, false))
            {
                var mySerializer = new XmlSerializer(typeof(List<HustleEveryDayStrategyTestingProperties>));
                mySerializer.Serialize(writer, properties);
            }
        }

        public List<HustleEveryDayStrategyTestingProperties> LoadTestingHustleEveryDayStrategyProperties()
        {
            this.AddInfoLog("Загрузка стратегий тестирования из {0}", TestingHustleEveryDayStrategies);

            var result = new List<HustleEveryDayStrategyTestingProperties>();

            if (File.Exists(TestingStrategiesXml))
            {
                using (var reader = new StreamReader(TestingHustleEveryDayStrategies))
                {
                    var x = new XmlSerializer(typeof(List<HustleEveryDayStrategyTestingProperties>));
                    result = (List<HustleEveryDayStrategyTestingProperties>)x.Deserialize(reader);
                }
            }

            return result;
        }


Идем в Main.

Добавляем маленький костыль.



private List<HustleEveryDayStrategy> _hustleEveryDayStrategies = new List<HustleEveryDayStrategy>();

private List<HustleEveryDayStrategy> _hustleEveryDayTestingStrategies = new List<HustleEveryDayStrategy>();
	

И почти как под копирку методы для добавления стратегий:

 
private void AddHustleEveryDayStrategy()
        {
            var properties = new HustleEveryDayStrategyProperties
            {
                // TODO: check if the stategy with same does exist
                Name = "Новая стратегия {0}".Put(_newStrategyCount++),
            };

            AddHustleEveryDayStrategy(properties);
            SaveStrategies();
        }

        private void AddHustleEveryDayStrategy(HustleEveryDayStrategyProperties properties)
		{
			var strategy = new HustleEveryDayStrategy
			{
				Params = properties,
				Trader = ConnectionEngine.Instance.Trader
			};

			_logManager.Sources.Add(strategy);

		    var doc = new LayoutDocument
		    {
                Title = strategy.Params.Name,
                Content = new StrategyDocument
				{
					Strategy = strategy
				},
		        CanClose = false
		    };

			strategy.Params.PropertyChanged += (s, a) =>
			{
				if (a.PropertyName == "Name")
				{
					doc.Title = strategy.Params.Name;
				}

				SaveStrategies();
			};

			_documents.Add(strategy, doc);
                        _hustleEveryDayStrategies.Add(strategy);

			this.AddInfoLog("Добавлена стратегия '{0}'", strategy.Name);

			StrategiesDocumentPane.Children.Add(doc);
		}

        private void AddHustleEverydayTestingStrategy()
        {
            var properties = new HustleEveryDayStrategyTestingProperties
            {
                Name = "Тестирование {0}".Put(_newTestingCount++)
            };

            AddTestingStrategy(properties);
            SaveTestingStrategies();
        }

        private void AddHustleEverydayTestingStrategy(HustleEveryDayStrategyTestingProperties properties)
        {
            var strategy = new HustleEveryDayStrategy
            {
                Params = properties,
                Trader = ConnectionEngine.Instance.Trader
            };

            var doc = new LayoutDocument()
            {
                Title = strategy.Params.Name,
                Content = new TestingDocument
                {
                    Strategy = strategy
                },
                CanClose = false
            };

            strategy.Params.PropertyChanged += (s, a) =>
            {
                if (a.PropertyName == "Name")
                {
                    doc.Title = strategy.Params.Name;
                }

                SaveTestingStrategies();
            };

            _testingDocuments.Add(strategy, doc);
            _hustleEveryDayTestingStrategies.Add(strategy);

            this.AddInfoLog("Добавлена тестовая стратегия '{0}'", strategy.Name);

            StrategiesDocumentPane.Children.Add(doc);
        }



Нужно добавить в InitializeConfiguration() - методе восстановления из настроек.

var hustleProperties = SettingsEngine.Instance.LoadHustleEveryDayStrategiesProperties();

            foreach (var property in hustleProperties)
            {
                AddHustleEveryDayStrategy(property);
            }


            var testingHustleProperties = SettingsEngine.Instance.LoadTestingHustleEveryDayStrategyProperties();

            foreach (var properties in testingHustleProperties)
            {
                AddTestingStrategy(properties);
            }

Ну вот. Теперь сохранение настроек стратегий будет работать не правильно, а удаление будет удалять только из интерфейса, а при перезапуске все будет по-прежнему. И это только на первый взгляд.

Как починить это, а также о том, как добавить кнопки добавления стратегии в интерфейс, узнаем в следующей подчасти части 3.

Bond
Bond
Bond

Прям триллер какой-то! Ждем продолжения! 👍

Kazai Mazai
Kazai Mazai 作者

Переделать метод сохранения, который в MainWindow.xaml.cs можно как-нибудь в таком духе:

private void SaveStrategies()
		{
			

		    SettingsEngine.Instance.SaveHustleEveryDayStrategiesProperties(
		        _hustleEveryDayStrategies.Select(s => (HustleEveryDayStrategyProperties)s.Params).ToList());

            var otherStrategies = _documents.Keys.Where( s=>! _hustleEveryDayStrategies.Any(str=> str.Params.Name==s.Params.Name)).Select(strategy => strategy.Params).ToList();
            SettingsEngine.Instance.SaveStrategies(otherStrategies);
		}

Сохранять сначала наши новые кастомные стратегии, а потом фильтровать все, например по имени. Но по идее, если уж пилить стратегии, то для каждого вашего типа стратегий будет свой массив, который и следует сохранять по отдельности, и ничего уже потом не фильтровать.

Аналогично с сохранением свойств тестовых стратегий:

  private void SaveTestingStrategies()
        {
            SettingsEngine.Instance.SaveTestingHustleEveryDayStrategyProperties(
                _hustleEveryDayTestingStrategies.Select(s => (HustleEveryDayStrategyTestingProperties)s.Params).ToList());

            var strategies = _testingDocuments.Keys.Where( s=>! _hustleEveryDayTestingStrategies.Any(str=> str.Params.Name==s.Params.Name)).Select(strategy => (BaseShellTestingProperties)strategy.Params).ToList();

            SettingsEngine.Instance.SaveTestingStrategies(strategies);
        }

В методе ```csharp private void ExecutedRemoveStrategy(object sender, ExecutedRoutedEventArgs e)


Там где у нас обработка нажатого "да"

```csharp
if (result == MessageBoxResult.Yes)
			{
				this.AddInfoLog("Удаление стратегии '{0}'", strategy.Params.Name);

				var doc = _documents[strategy];
				_documents.Remove(strategy);

				StrategiesDocumentPane.RemoveChild(doc);
			    _hustleEveryDayStrategies.RemoveAll(s => s.Params.Name == strategy.Params.Name);
				SaveStrategies();
			}

Удаляем еще и из нашего списка стратегий с фильтрацией по имени. Лучше, конечно, что б у всех стратегий был какой-то id, и все сравнения проводить по нему.

Что бы можно было было из интерфейса добавить свежих стратегий:

Добавляем команду там же в MainWindow.xaml.cs,

public static RoutedCommand AddHustleStrategyCommand = new RoutedCommand();

далее методы для команды.

 private void ExecutedAddHustleStrategy(object sender, ExecutedRoutedEventArgs e)
        {
            AddHustleEveryDayStrategy();

            AllStrategies.ListViewStrategies.ItemsSource = _documents.Keys.ToList();
        }
        private void CanExecuteAddHuslteStrategy(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true;
            e.Handled = true;
        }

Нужно теперь подключить команду к gui. В MainWindow.xaml:

Там есть такая штука, комманд биндинг. Добавляем вот что:

<CommandBinding Command="{x:Static Robot:MainWindow.AddHustleStrategyCommand}"
                    Executed="ExecutedAddHustleStrategy"
                    CanExecute="CanExecuteAddHustleStrategy" />

далее в нем же ищем "AddStrategyCommand"? находим его в контексте меню итемс, и аналогично добавляем

   <MenuItem Header="Добавить хассол" Command="{x:Static Robot:MainWindow.AddHustleStrategyCommand}"/>

Ну вот, теперь все готово.

Bond
Kazai Mazai
Kazai Mazai 作者

Небольшое лирическое отступление...

Там выше в стратегии реализовывались методы для сохранения ордеров, позиций и т.д.

У меня так раньше было реализовано, не на s# shell, но все же. Возможно версия s# была другая, возможно я где-то наошибался, но недавно на s# shell уже обнаружил, что такой способ не работает. Т.е. если положить Dictionary с ордерами (то что в базовых Properties) в SettingsStorage и попытаться сериализовать, ничего не получится. Если положить SettingsStorage с ордерами в SettingsStorage, тоже не заработало, хотя раньше все прокатывало, если мне не изменяет память.

Во-вторых это, на мой взгляд, не совсем корректно, если в стратегию ордера и сделки поступают не только из Trader, но еще и напрямую из хранилища.

Может возникнуть какая-нибудь дурацкая ситуация, если после перезапуска один и тот же ордер прийдет в стратегию и шлюза и из хранилища, и бог его знает как-там оно все разбираться внутри будет.

Поэтому, в Properties стратегии лучше хранить просто список orderIDs, брать их от Trader'a и привязывать.

Если торговый терминал не передает историю ордеров, прийдется "усовершенствовать" Trader и реализовать возможность сохранения в бд.

Вот такие вот рекомендации.🙂

dij1
dij1

Вопрос немного не в тему. Удалось ли Вам поймать устойчивую конфигурацию работы с Plaza 2, т.е. версию, которая с плазой пашет?

Kazai Mazai
Kazai Mazai 作者

dij1: Вопрос немного не в тему. Удалось ли Вам поймать устойчивую конфигурацию работы с Plaza 2, т.е. версию, которая с плазой пашет?

Я плазой вообще дел не имел

Kazai Mazai
Kazai Mazai 作者

По просьбам телезрителей(как минимум одного) сделал зарисовочку для сопровождения позиции. Открывается прямо при старте, и выходит в какое-то время, а также по стопам и нескольким ценовым тейкам, передвигая стоп в б\у. Есть кое-какие лишние проверки, потому что кусочки взяты из стратегии на нескольких инструментах, но хуже не будет.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Logging;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;

namespace Robot.Strategies
{
    public class HustleEveryDayStrategyProperties :BaseShellStrategyProperties
    {

        private decimal _takeProfit1 = 10000000;
        [DisplayName(@"1-й тейк профит")]
        [Description(@"Цена для 1-й цели")]
        [Category(@"Параметры")]
        [PropertyOrder(0)]
        public decimal TakeProfit1
        {
            get { return _takeProfit1; }
            set
            {
                _takeProfit1 = value;
                OnPropertyChanged("TakeProfit1");
            }
        }



        private decimal _takeProfit2 = 10000000;
        [DisplayName(@"2-й тейк профит")]
        [Description(@"Цена для 2-й цели")]
        [Category(@"Параметры")]
        [PropertyOrder(0)]
        public decimal TakeProfit2
        {
            get { return _takeProfit2; }
            set
            {
                _takeProfit2 = value;
                OnPropertyChanged("TakeProfit2");
            }
        }
        private decimal _takeProfit3 = 10000000;
        [DisplayName(@"3-й тейк профит")]
        [Description(@"Цена для 3-й цели")]
        [Category(@"Параметры")]
        [PropertyOrder(0)]
        public decimal TakeProfit3
        {
            get { return _takeProfit3; }
            set
            {
                _takeProfit3 = value;
                OnPropertyChanged("TakeProfit3");
            }
        }

        private decimal _stoploss = 10000000;
        [DisplayName(@"стоп лосс")]
        [Description(@"Цена для стоп лосса")]
        [Category(@"Параметры")]
        [PropertyOrder(0)]
        public decimal Stoploss
        {
            get { return _stoploss; }
            set
            {
                _stoploss = value;
                OnPropertyChanged("Stoploss");
            }
        }
        private DateTime _closeTime = new DateTime(2000,1,1,1,1,1);
        [DisplayName(@"Время закрытия")]
        [Description(@"Время закрытия сделки")]
        [Category(@"Параметры")]
        [PropertyOrder(0)]
        public DateTime CloseTime
        {
            get { return _closeTime; }
            set
            {
                _closeTime = value;
                OnPropertyChanged("CloseTime");
            }
        }

        private OrderDirections _orderDirection = OrderDirections.Buy;
        [DisplayName(@"Направление сделки")]
        [Description(@"Направление сделки")]
        [Category(@"Параметры")]
        [PropertyOrder(0)]
        public OrderDirections OrderDirection
        {
            get { return _orderDirection; }
            set
            {
                _orderDirection = value;
                OnPropertyChanged("OrderDirection");
            }
        }


    }

    public class  HustleEveryDayStrategyTestingProperties:BaseShellTestingProperties
    {
        

    }


   public class HustleEveryDayStrategy:BaseShellStrategy
    {
       protected override void OnStarted()
       {

           base.OnStarted();

           var currentTime = Trader.GetMarketTime(Security.ExchangeBoard.Exchange);
           this.AddInfoLog("Текущее рыночное время: {0}.".Put(currentTime));

           var currentClosePositionsTime = ((HustleEveryDayStrategyProperties) Params).CloseTime;
           this.AddInfoLog("Текущее время проверки выхода по тайм-стопу: {0}.".Put(currentClosePositionsTime));
           if (currentTime > currentClosePositionsTime)
           {
               var newClosePositionsTime = new DateTime(currentTime.Year, currentTime.Month, currentTime.Day,
                                                        currentClosePositionsTime.Hour, currentClosePositionsTime.Minute,
                                                        currentClosePositionsTime.Second);
               this.AddInfoLog("Новое время проверки выхода по тайм-стопу: {0}.".Put(newClosePositionsTime));
               ((HustleEveryDayStrategyProperties) Params).CloseTime = newClosePositionsTime;
           }

           
           Security
             .WhenTimeCome(((HustleEveryDayStrategyProperties)Params).CloseTime)
             .Do(CheckForExitByTimeTargets)
             .Apply(this);

           Security
                   .WhenChanged()
                   .Do(CheckForExitByPriceTargets)
                   .Apply(this);

           SubscriptionEngine.Instance.RegisterSecurity(this, Security);

           if(((HustleEveryDayStrategyProperties)Params).OrderDirection==OrderDirections.Buy)
           {
               PlaceBuyMarketOrder(Security,Volume);

           }
           else
           {
               PlaceSellMarketOrder(Security, Volume);
           }

       }

       private  void CheckForExitByPriceTargets()
       {

         this.AddInfoLog("Проверка выхода по целям.");
       
         if( PositionManager.Positions.Any(p=> p.Security.Code==Security.Code))
         {
             var position = PositionManager.Positions.First(p => p.Security.Code == Security.Code);
             if (position.CurrentValue > 0)
             {
                 if (Security.LastTrade.Price <= ((HustleEveryDayStrategyProperties)Params).Stoploss)
                 { 
                    PlaceSellMarketOrder(position.Security, position.CurrentValue);
                    return;
                 }

                 if (Security.LastTrade.Price >= ((HustleEveryDayStrategyProperties)Params).TakeProfit3)
                 {
                     PlaceSellMarketOrder(position.Security, position.CurrentValue);
                     return;
                 }

                 if (Security.LastTrade.Price >= ((HustleEveryDayStrategyProperties)Params).TakeProfit2)
                 {
                     PlaceSellMarketOrder(position.Security,decimal.Floor(0.5m*position.CurrentValue));
                     ((HustleEveryDayStrategyProperties)Params).Stoploss =
                         ((HustleEveryDayStrategyProperties)Params).TakeProfit1;

                     return;
                 }
                 if (Security.LastTrade.Price >= ((HustleEveryDayStrategyProperties)Params).TakeProfit1)
                 {
                   
                     PlaceSellMarketOrder(position.Security, decimal.Floor(0.33m * position.CurrentValue));
                   
                     if(MyTrades.Any(t=>t.Trade.Security.Code==Security.Code))
                     {
                        var myTradesOrderedByDate = MyTrades.ToList().OrderBy(t => t.Trade.Time);
                        var trade = myTradesOrderedByDate.Last(t => t.Trade.Security.Code == Security.Code);
                        var entryPrice = trade.Trade.Price;

                         ((HustleEveryDayStrategyProperties) Params).Stoploss = entryPrice;

                     }
                     return;
                 }




             }
             else
             {
                 if (position.CurrentValue < 0)
                 {
                     if (Security.LastTrade.Price >= ((HustleEveryDayStrategyProperties)Params).Stoploss)
                     {
                         PlaceBuyMarketOrder(position.Security, position.CurrentValue);
                         return;
                     }

                     if (Security.LastTrade.Price <= ((HustleEveryDayStrategyProperties)Params).TakeProfit3)
                     {
                         PlaceBuyMarketOrder(position.Security, position.CurrentValue);
                         return;
                     }

                     if (Security.LastTrade.Price <= ((HustleEveryDayStrategyProperties)Params).TakeProfit2)
                     {
                         PlaceBuyMarketOrder(position.Security, decimal.Floor(0.5m * position.CurrentValue));
                         ((HustleEveryDayStrategyProperties)Params).Stoploss =
                             ((HustleEveryDayStrategyProperties)Params).TakeProfit1;

                         return;
                     }
                     if (Security.LastTrade.Price <= ((HustleEveryDayStrategyProperties)Params).TakeProfit1)
                     {

                         PlaceBuyMarketOrder(position.Security, decimal.Floor(0.33m * position.CurrentValue));

                         if (MyTrades.Any(t => t.Trade.Security.Code == Security.Code))
                         {
                             var myTradesOrderedByDate = MyTrades.ToList().OrderBy(t => t.Trade.Time);
                             var trade = myTradesOrderedByDate.Last(t => t.Trade.Security.Code == Security.Code);
                             var entryPrice = trade.Trade.Price;

                             ((HustleEveryDayStrategyProperties)Params).Stoploss = entryPrice;

                         }
                         return;
                     }




                 }
             }
          
              
         }
          

       }


       private  void CheckForExitByTimeTargets()
       {

           this.AddInfoLog("Выход по тайм-стопу");
       
             foreach (var position in PositionManager.Positions)
             {
                 if(position.CurrentValue>0)
                 {
                     PlaceSellMarketOrder(position.Security,position.CurrentValue);

                 }
           
                 if (position.CurrentValue < 0)
                 {
                     PlaceBuyMarketOrder(position.Security, -position.CurrentValue);


                 }
                  

             }

       }

       protected void PlaceBuyMarketOrder(Security security, decimal volume)
       {
           var order = this.BuyAtMarket(volume);
           order.Security = security;

           order
                          .WhenChanged()
                          .Do(() => this.AddInfoLog("Изменилось остояние заявки"))
                          .Once()
                          .Apply(this);



           order
               .WhenRegistered()
               .Do(() => this.AddInfoLog("Заявка успешно зарегистрирована"))
               .Once()
               .Apply(this);

           order
               .WhenRegisterFailed()
               .Do(() => this.AddInfoLog("Заявка не принята биржей"))
               .Once()
               .Apply(this);

           order
               .WhenMatched()
               .Do(() =>
               {

                   this.AddInfoLog("Заявка полностью исполнена");
               })
               .Once()
               .Apply(this);

           // регистрирация заявки

           RegisterOrder(order);

       }
       protected void PlaceSellMarketOrder(Security security, decimal volume)
       {
           var order = this.SellAtMarket(volume);
           order.Security = security;
           order
                          .WhenChanged()
                          .Do(() => this.AddInfoLog("Изменилось остояние заявки"))
                          .Once()
                          .Apply(this);



           order
               .WhenRegistered()
               .Do(() => this.AddInfoLog("Заявка успешно зарегистрирована"))
               .Once()
               .Apply(this);

           order
               .WhenRegisterFailed()
               .Do(() => this.AddInfoLog("Заявка не принята биржей"))
               .Once()
               .Apply(this);

           order
               .WhenMatched()
               .Do(() =>
               {

                   this.AddInfoLog("Заявка полностью исполнена");
               })
               .Once()
               .Apply(this);

           // регистрирация заявки

           RegisterOrder(order);

       }
   

    }
}

Churchill liftrade dimtrdr
liftrade
liftrade

Интересно, это только у меня в каркасе в таблице сделки, отображается только первая сделка? Никакие последующие в нее не заносятся... Или это общий баг такой?

Kazai Mazai
Kazai Mazai 作者

liftrade: Интересно, это только у меня в каркасе в таблице сделки, отображается только первая сделка? Никакие последующие в нее не заносятся... Или это общий баг такой?

да у меня тоже был такой баг, почему-то insert не так работает, как хотелось бы.

В StrategyDocument.xaml.cs меняем закомментированное на незакомментированное.


_strategy.NewMyTrades += trades =>
				{
					foreach (var trade in trades)
					{
                                                _tradeGrid.Trades.Add(trade.Trade);
						//_tradeGrid.Trades.Insert(0, trade.Trade);
					}
				};

Aton5
Aton5

где взять исходники S#.Shell, тем у кого лицензия это позволяет? Дайте, пожалуйста, ссылку.

dij1
dij1

скачал версию 4.2.2, там вообще Shell не открывается, библиотек основных нет.

JaguarFX
JaguarFX

Месяц прошел - и молчание разработчиков!)) На самом деле Shell действительно достаточно сырой продукт, в который необходимо много вложить чтобы получить выхлоп. Даже в версии от Казакова Сергея достаточно ошибок и неточностей, на исправление которых у меня ушло множество времени.

Так почти месяц назад я поставил для себя цель провести миграцию своих торговых роботов с платформы TradeMatic на платформу S#.Shell, и за это время наработал материал, которым стоит поделиться дабы не возникали подобные вопросы.

JaguarFX
JaguarFX

Часть 4. Универсализация создания, тестирования и исполнения стратегий

Первое что мне бросилось в глаза в версии от Kazai Mazai, которую я продолжил допиливать - отсутствие универсальности в исполнении/тестировании одной и той же стратегии. Так если запустить стратегию RobotStrategy на тестирование, то следующий участок кода стратегии:


... lock (this)
	{
	  if (_bidOrder == null && _askOrder == null)
		{
		// проверяем на сигнал на вход
	       var spread = (ask - bid) / Security.MinStepSize;

		if (spread >= ((RobotStrategyProperties)Params).Spread)
...

вызовет ошибку "Не удалось привести тип объекта "System.Collections.Generic.List1[Robot.Strategies.HustleEveryDayStrategyTestingProperties]" к типу "System.Collections.Generic.List1[Robot.Strategies.HustleEveryDayStrategyProperties]".

Для решения этой проблемы введем в класс BaseShellStrategyProperties два параметра "Класс стратегии" и "Режим запуска":



        public enum enMode
        {
           Execution,
           Testing
        }

        private string _classname = "Класс стратегии";
        [DisplayName(@"Класс")]
        [Description(@"Класс стратегии")]
        [Category(@"Основные")]
        [PropertyOrder(1)]
        public string ClassName
        {
            get { return _classname; }
            set
            {
                _classname = value;
                OnPropertyChanged("ClassName");
            }
        }


        private enMode _mode = enMode.Testing;
        [Category(@"Основные")]
        [DisplayName(@"Режим запуска")]
        [Description(@"Режим запуска стратегии")]
        [PropertyOrder(2)]
        public enMode Mode
        {
            get { return _mode; }
            set
            {
                _mode = value;
                OnPropertyChanged("Mode");
            }
        }

Данные свойства необходимо определять во всех процедурах типа AddХХХХStrategy.


     private void AddRobotStrategy()
        {
            var properties = new RobotStrategyProperties
            {
                ClassName = "Robot",
                Mode = enMode.Execution

4.1. Универсализация исполнения стратегий Все параметры внутри стратегии обертываем процедурой if, которая проверяет режим запуска и проводит правильное приведение типов данных:


            int StopVal = 1;
            if (Params.Mode == enMode.Execution)
                StopVal = ((RobotStrategyProperties)Params).Stop;
            else
                StopVal = ((RobotTestingProperties)Params).Stop;

4.2. Универсализация тестирования стратегий В основную процедуру запуска стратегий на тестирование StartTesingStrategy добавляем проверка класса стратегии при задании специфичных параметров:


            if (parameters.ClassName == "Sma" || parameters.ClassName == "TLS")
            {
                switch (parameters.ClassName)
                {
                    case  "Sma":
                        ((SmaStrategy)_strategy).series = _series;
                        break;
                    case "TLS":
                        ((TLSStrategy)_strategy).series = _series;
                        break;
                }
            }

4.3. Универсализация сохранения настроек стратегий В основной процедуре сохранения настроек стратегии SaveStrategies проводим модификацию запроса Where


var robotstrategies = _documents.Keys.Where(str => str.Params.ClassName == "Robot" && str.Params.Mode == enMode.Execution).Select(str => (RobotStrategyProperties)str.Params).ToList();
SettingsEngine.Instance.SaveRobotStrategies(robotstrategies);

var hustlestrategies = _documents.Keys.Where(str => str.Params.ClassName == "Hustle" && str.Params.Mode == enMode.Execution).Select(str => (HustleStrategyProperties)str.Params).ToList();
SettingsEngine.Instance.SaveHustleStrProp(hustlestrategies);

4.4. Устранение бага отображения стратегий на панели "Стратегии" В поставляемой версии на панели "Стратегии" не отражаются тестовые стратегии. Для устранения этого бага проводим универсализацию хранения стратегий в переменной _documents. а) везде в процедурах создания стратегий проводим сохранение стратегий в переменную _documents:


                _documents.Add(strategy, doc); 

б) в свойстве SelectedStrategy дописываем условие выбора документа типа TestingDocument:


                var doc = dockManager.ActiveContent;

                if (doc is StrategyDocument)
                {
                    var content = (StrategyDocument)doc;
                    result = content.Strategy;
                }
                else
                {
                    var content = (TestingDocument)doc;
                    result = content.Strategy;
                }
                return result;

Теперь все отображается корректно. в) дополнительно можно вообще удалить процедуру SaveTestingStrategies, поместив соответствующие обработки в SaveStrategies:


var robotTstrategies = _documents.Keys.Where(str => str.Params.ClassName == "Robot" && str.Params.Mode == enMode.Testing).Select(str => (RobotStrategyTestingProperties)str.Params).ToList();
SettingsEngine.Instance.SaveRobotTestingStrategies(robotTstrategies);

var hustleTstrategies = _documents.Keys.Where(str => str.Params.ClassName == "Hustle" && str.Params.Mode == enMode.Testing).Select(str => (HustleStrategyTestingProperties)str.Params).ToList();
SettingsEngine.Instance.SaveHustleTestStrProp(hustleTstrategies);

После всех этих универсализаций добавление новой стратегии действительно становится достаточно простым делом.

JaguarFX
JaguarFX

Часть 5. Чему нас учит семья и школа

Интерфейс тестирования S#.Shell по умолчанию не достаточен для эффективного тестирования стратегии при ее первичном кодировании, так как в этом тонком процессе переноса собственного нейронного вещества в машинный код возникает множество нюансов а-ля "сделка не выполнилась в силу неизвестных причин", в которых невозможно разобраться без соответствующих инструментов.

Поэтому добавляем в наш S.Shell основные инструменты визуализации процесса исполнения стратегии, которые нам уже знакомы по курсу S#.Education - исторический график свечей, таблицу заявок и таблицу собственных сделок.

5.1. Создаем на TestingPanel элемент типа TabControl, в который добавляем вкладки для каждого инструмента:


                <TabItem x:Name="TabHist" Header="History">
                    <chart:Chart x:Name="HistoryChart" MinHeight="200" Height="Auto" x:FieldModifier="public"/>
                </TabItem>
                <TabItem x:Name="TabPNL" Header="PNL">
                    <chart:EquityCurveChart x:Name="CurveChart" MinHeight="200" Height="Auto"  x:FieldModifier="public"/>
                </TabItem>
                <TabItem x:Name="TabORD" Header="Orders">
                    <xaml:OrderGrid x:Name="myOrders" MinHeight="200" Height="Auto" x:FieldModifier="public"/>
                </TabItem>
                <TabItem x:Name="TabTRD" Header="Trades">
                    <xaml:MyTradeGrid x:Name="myTrades" MinHeight="200" Height="Auto" x:FieldModifier="public"/>
                </TabItem>

5.2. В общем-то мы понимаем, что графические элементы в целом тормозят тестирование и нужны будут не всегда, а в основном при кодинге новых стратегий, поэтому в свойства базовой стратегии тестирования BaseShellStrategy добваляем свойства для управления отображением их в ходе тестирования:


        private bool _chartPL = false;
        [Category(@"Отрисовка")]
        [DisplayName(@"График P&L")]
        [Description(@"Вывод графика P&L при тестировании")]
        [PropertyOrder(0)]
        public bool chartPL
        {
            get { return _chartPL; }
            set
            {
                _chartPL = value;
                OnPropertyChanged("chartPL");
            }
        }

        private bool _chartHC = false;
        [Category(@"Отрисовка")]
        [DisplayName(@"График свечей")]
        [Description(@"Вывод графика свечей  при тестировании")]
        [PropertyOrder(2)]
        public bool chartHC
        {
            get { return _chartHC; }
            set
            {
                _chartHC = value;
                OnPropertyChanged("chartHC");
            }
        }

        private bool _chartTrd = false;
        [Category(@"Отрисовка")]
        [DisplayName(@"Сделки")]
        [Description(@"Вывод всех сделок на график истории")]
        [PropertyOrder(2)]
        public bool chartTrd
        {
            get { return _chartTrd; }
            set
            {
                _chartTrd = value;
                OnPropertyChanged("chartTrd");
            }
        }

5.3. В процедуре тестирования стратегии StartTesingStrategy прописываем код для обоработки каждого нового инструмента: а) для истории свечей:


            if (parameters.chartHC)
            {
                    histChart = doc.TestingPanel.HistoryChart;
                    histChart.Areas.Clear();
                    histChart.IsAutoScroll = true;
                    _area = new ChartArea(); //создаем область на графике
                    histChart.Areas.Add(_area); //добавляем область на график
                    _candlesElem = new ChartCandleElement(); //создаем элемент свечи
                    _area.Elements.Add(_candlesElem); //добавляем элемент в область вывода графика
                    _candleManager.Processing += (series, cnd) =>
                    {
                        if (cnd.State == CandleStates.Finished)
                        {
                            var myDic = new Dictionary<IChartElement, object>();
                            myDic.Add(_candlesElem, cnd);
                            MainWindow.Instance.GuiAsync(() => histChart.ProcessValues(cnd.OpenTime, myDic));
                        }
                    };
            }

б) для отражения истории сделок:


            if (parameters.chartHC && parameters.chartTrd)
            {
                    var _tradeElement = new ChartTradeElement();
                    _area.Elements.Add(_tradeElement);
                    _strategy.NewMyTrades += (trds) => 
                        trds.ForEach(t =>
                        {
                            var tradeTime = _timeFrame.GetCandleBounds(t.Trade.Time).Min;
                            var myDic = new Dictionary<IChartElement, object>() { { _tradeElement, t } };
                            MainWindow.Instance.GuiAsync(() => histChart.ProcessValues(tradeTime, myDic));
                        });
            }

в) для таблицы заявок:


            var _myOrders = doc.TestingPanel.myOrders;
            _myOrders.Orders.Clear();
            _strategy.OrderRegistered += (ord) => MainWindow.Instance.GuiAsync(() => _myOrders.Orders.Add(ord));

г) для таблицы сделок:


            var _myTrades = doc.TestingPanel.myTrades;
            _myTrades.Trades.Clear();
            _strategy.NewMyTrades += (trd) => trd.ForEach(t => MainWindow.Instance.GuiAsync(() => _myTrades.Trades.Add(t)));

И в результате все работет отлично, кроме корректного отображения сделок))

kesot
kesot

А почему для установки стопов не пользуетесь StopLossStrategy, а в ручную их отслеживаете?

JaguarFX
JaguarFX

на мой взгляд использование дочерних стратегий тут дело вкуса - кому-то идет, а кому-то нет; в силу разных обстоятельств - и дебаггить их невозможно, условия срабатывания ограничены (н--р нет стоп-лосса исходя из % годовых) и пр.

JaguarFX
JaguarFX

Часть 6 - Тотальный апгрейд

Итак имеем S#.Shell на основе S#.API 4.1.15.0 и решаемся на апгрейд до версии 4.2.2.16. Сразу же получаем множество заменой iTrader на Connector, и еще с десяток проблемных мелочей типа замены свойств, которые решаются в течение получаса

Но так же вылезают две более серьезные проблемы, о которых имеет смысл тут рассказать.

  1. В XAML отваливается весь CommandBinding с тупым сообщением "Член не распознан")) Как единственное найденное 100% работающее решение данной проблемы - перенос всего CommandBindingа из XAML в программный код. Для этого делаем три простых шага:
  1. в MainWindow.xaml полностью удаляем содержание раздела <Window.CommandBindings> и привязки команд типа Command="", а вместо этого для каждого элемента, которому необходимо привязать RoutedCommand задаем имя через тег x:Name
  2. в MainWindow.xaml.cs создаем новую процедуру private void InitializeCommands(), в которой для каждого управляющего элемента создаем связь через класс CommandBinding (на примере пункта меню "Подключиться"):

            CommandBinding ccBinding1 = new CommandBinding(ConnectCommand, ExecutedConnect, CanExecuteConnect);
            this.CommandBindings.Add(ccBinding1);
            miConnect.Command = ConnectCommand;

где ConnectCommand - исходная RoutedCommand, ExecutedConnect - основная процедура для выполнения, CanExecuteConnect - связанная проверочная процедура возможности выполнения основной процедуры, miConnect - название элемента пункта меню "Подключиться", далее в тоже процедуре InitializeCommands() создаем необходимые элементы для быстрых клавиш:


            KeyGesture OpenKeyGesture1 = new KeyGesture(Key.P,ModifierKeys.Control);
            InputBinding KeyBinding1 = new KeyBinding(SettingsCommand, OpenKeyGesture1);
            this.InputBindings.Add(KeyBinding1);

  1. вызываем InitializeCommands() в теле процедуры MainWindow().
  1. В S#.API версии 4.2.2.16 обнаруживаем, что в режиме эмуляции ни один из коннекторов (проверено на AlfaTrader/TransaqTrader) не получает портфели. Соответственно тестирование в эмуляции невозможно. Для решения этой проблемы делаем два шага:
  1. создаем в классе ConnectionEngine свойство IsEmulation:

                   public bool IsEmulation { get { return settings.Emulation; } }

2)добавляем в код процедуры StartStrategy специальную проверку на режим эмуляции и в случае оного, создаем тестовый портфель, который и привязываем к коннектору:

 
		    var portfolio = ConnectionEngine.Instance.Trader.Portfolios.FirstOrDefault(p => p.Name == strategy.Params.Portfolio);
		    if (portfolio == null)
		    {
		        if (ConnectionEngine.Instance.IsEmulation)
		        {
                    var portf = new Portfolio{Name = "TestAcc",BeginValue = 1000000,CurrentValue = 1000000};
                    ConnectionEngine.Instance.Trader.RegisterPortfolio(portf);
                    ConnectionEngine.Instance.Trader.TransactionAdapter.SendInMessage(portf.ToMessage());
                    var pcm =new PortfolioChangeMessage{PortfolioName = portf.Name}.Add(PositionChangeTypes.BeginValue, portf.BeginValue);
		            ConnectionEngine.Instance.Trader.TransactionAdapter.SendInMessage(pcm);
                    var pcm2 = new PortfolioChangeMessage { PortfolioName = portf.Name }.Add(PositionChangeTypes.CurrentValue, portf.CurrentValue);
                    ConnectionEngine.Instance.Trader.TransactionAdapter.SendInMessage(pcm2);
		            strategy.Portfolio = portf;
                    strategy.AddWarningLog("Создан портфель {0} для запуска стратегии", portf.Name);
		        }
		        else
		        {
                    strategy.AddErrorLog("Не найден портфель {0} для запуска стратегии", strategy.Params.Portfolio);
                    return;    
		        }
		        
		    }
		    else
		        strategy.Portfolio = portfolio;

Вуаля- все работает как нужно!

methyst
methyst
methyst

lebedevsrg, собираетесь ли Вы выкладывать свою версию shell'a куда-нибудь?

JaguarFX
JaguarFX

methyst, тут такие правила что по окончанию срока обучения (1,5 или 3 мес ) пользователей отключают от TFS. По вопросу - полагаю что смысла каждому пользователю выкладывать свои программы на TFS особого нет. Материалы я тут опубликовал для общего развития тех кто захочет сам допиливать Shell.

Mikhail Sukhov
Mikhail Sukhov

lebedevsrg: methyst, тут такие правила что по окончанию срока обучения (1,5 или 3 мес ) пользователей отключают от TFS.

Таких правил нет. http://stocksharp.com/posts/m/29944/

JaguarFX
JaguarFX

Часть 7 - Чем StockSharp круче Tradematic, или о миграции стратегий

Итак рассмотрим вкратце миграцию стратегий на примере простейшей стратегии торговли по линиям капитала, которая подробно изложена тут (http://robotcraft.ru/vstroeniestrategii/strateg-akc). Заодно пополним коллекцию торговых стратегий StockSharp достаточно надежной и низкорискованной стратегий, хотя с небольшой годовой доходностью (9% -15%).

В целом система Tradematic так же позволяет программировать стратегии на с#, но без доступа к исходному коду оболочки, т.е. это что-то типа S#.Studio. При этом в ней есть ряд преимуществ по сравнению со S#.Studio:

  1. имеется встроенный оптимизатор стратегий,
  2. лицензия включает доступ к Серверу с историческими данными, позволяющему проводить облачное тестирование (как известно доступ к такому серверу StockSharp идет за отдельную плату),
  3. имеется "торговая книга", которая автоматически матчит открытие и закрытие позиций. Но все эти преимущества меркнут по сравнению с теми колоссальными возможностями, которые нам дает открытая библиотека StockSharp, и которые будут проиллюстрированы ниже.

Итак для реализации нашей стратегии создаем общий класс TradeManager, назначением которого будет:

  1. создание и ведение Плана торговли(Plan),

  2. ведение Торговой книги(Book). Данный класс сразу делаем общий, так как он в дальнейшем может быть использован в пирамидальных стратегиях любой сложности.

  3. Создаем класс TradeManager и его инициализатор:

 
    public class TradeManager : ILogReceiver
    {
        private BaseShellStrategy _stratergy { get; set; }
        public Dictionary<int, PlanElem> Plan;
        public SynchronizedCollection<BookElem> Book;
        
        public TradeManager(BaseShellStrategy _str)
        {
            if (_stratergy==null) _stratergy = _str;
            if (Plan == null) Plan = new Dictionary<int, PlanElem>();
            if (Book == null) Book = new SynchronizedCollection<BookElem>();
        }        
     ...
    }

  1. Для создания Плана торговли объявляем дополнительный класс PlanElem, каждый экземпляр которого будет содержать информацию об одной торговой линии:

    public class PlanElem
    {
        public int Id { get; set; }
        public decimal PlanPrice { get; set; }
        public string SignalName { get; set; }
        public int Vol { get; set; }
        public decimal Amount { get; set; }
        public decimal SellPrice { get; set; }
        public decimal RiskShort { get; set; }
        public decimal RiskLong { get; set; }
        public DateTime BuyDate { get; set; }
        public DateTime SellDate { get; set; }
        public long OrderId { get; set; }
        public long TradeId { get; set; }
    }

Так же создаем класс для быстрой параметризации Плана торговли при его первичном создании:


    // Параметры плана торговли
    public class PlanParams
    {
        public int MaxPriceVal { get; set; }
        public int MinPriceVal { get; set; }
        public int StepVal { get; set; }
        public decimal riskShortVal { get; set; }
        public decimal riskLongVal { get; set; }
        public decimal MarginVal { get; set; }
    }

Далее создаем метод CreateTradePlan для создания План торговли по указанным параметрам:


public void CreateTradePlan(PlanParams prm)
        {
            
            if (prm.MaxPriceVal <= prm.MinPriceVal || prm.MaxPriceVal == 0 || prm.MinPriceVal == 0 || prm.StepVal == 0 ||
                prm.StepVal >= prm.MinPriceVal)
                return;

            _NBins = 3 + (prm.MaxPriceVal - prm.MinPriceVal) / prm.StepVal;
            if (_NBins > prm.Maxlength)  // Ограничение на длину плана торгов
            {   
                this.AddErrorLog("Вычисленная длина плана превышает допустимую");
                return; 
            }

            decimal curLevel = 0;
            ShortCapVal = 0;
            LongCapVal = 0;

            for (int i = 0; i <= _NBins; i++)
            {
                
                planElem = new PlanElem();
                planElem.Id = i + 1;

                if (i == 0)
                {   planElem.PlanPrice = 0;
                    planElem.SellPrice = prm.MinPriceVal;
                }
                else if (i == 1)
                {   curLevel = prm.MinPriceVal;
                    planElem.SellPrice = curLevel + prm.StepVal;
                }
                else if (i == _NBins)
                {   curLevel = prm.MaxPriceVal;
                    planElem.SellPrice = prm.MaxPriceVal * 1000 + prm.StepVal;
                }
                else
                {   curLevel = curLevel + prm.StepVal;
                    planElem.SellPrice = curLevel + prm.StepVal;
                }

                planElem.PlanPrice = curLevel;
                planElem.Amount = _stratergy.Volume;
                if (prm.iType == SecurityTypes.Stock)
                {
                    planElem.CapShort = curLevel * _stratergy.Volume;
                    planElem.CapLong = curLevel * _stratergy.Volume;
                }
                else
                {
                    planElem.CapShort = (curLevel * prm.CapShortVal + prm.MarginVal) * _stratergy.Volume;
                    planElem.CapLong = (curLevel * prm.CapLongVal + prm.MarginVal) * _stratergy.Volume;
                }
                ShortCapVal = ShortCapVal + planElem.CapShort;
                LongCapVal = LongCapVal + planElem.CapLong;
                planElem.SignalName = "Buy at " + curLevel.ToString();
                planElem.Vol = 0;
                Plan.Add(i, planElem);
            }

            Book.Clear();
            this.AddInfoLog("Создан План торговли из {0} уровней ".Put(_NBins));
        }

  1. Создаем процедуры для ведения Торговой книги: а) в первую очередь делаем процедуру первичной регистрации RegisterPosition, которая будет срабатывать сразу полсе появления заявки:

        public void RegisterPosition(Order ord)
        {
            if (ord.Direction == OrderDirections.Sell) return; // отсев заявок на продажу
            if (ord.Comment.Length > 3) return; // отсев заявок с перерегситрацией

            int curi = Book.Count();
            var inum = (int)ord.Comment.To<int>(); // порядковый номер плана
            var pl = Plan.TryGetValue(inum - 1);
            var BookElem = new BookElem();
            BookElem.Id = curi;
            BookElem.PlanPrice = pl.PlanPrice;
            BookElem.SignalId = pl.Id;
            BookElem.Amount = (int)_stratergy.Volume;
            BookElem.BuyOrder = ord.TransactionId;
            BookElem.BuyOrderDate = ord.LastChangeTime;
            Book.Add(BookElem);
            ord.Comment = "Уровень {0} зарегистрирован".Put(inum);
            this.AddInfoLog("Уровень {0} зарегистрирован", ord.TransactionId);
        }

б) затем создаем процедуру UpdateBook, которая призвана записывать в Торговую книгу данные о связанных сделках покупки и продажи: в зависимости от направления проводим связывание элементов Заявка-Сделка:

  • сделки на покупку сопоставляются с заявками по TransactionId, который предварительно сохраняется в поле BuyOrder при регистрации,
  • сделки на продажу сопоставляются со сделками на покупки по TransactionId, записанным в поле Comment заявки на продажу.

        public void UpdateBook(IEnumerable<MyTrade> trades)
        {
            foreach (var myTrade in trades)
            {
                var ord = myTrade.Order;
                int ordnum = -1;

                if (ord.Direction == OrderDirections.Buy)
                { // дополняем таблицу данными о сделке покупки

                    string tID = ord.TransactionId.ToString();
                    if (tID.Length > 8)
                    { // вырезаем основную часть из заявок с переносом
                        tID = tID.Substring(0, 8);
                    }

                    var ordlast = Book.Where(f => f.BuyOrder.ToString() == tID);
                    if (ordlast.Any() == false)
                    {
                        for (int i = 0; i < Book.Count; i++)
                            if (Book[i].f.BuyOrder.ToString() == tID) ordnum = i + 1;
                    }
                    else ordnum = ordlast.First().Id; // Key связанной Позиции

                    planElem = Plan[Book[ordnum].SignalId-1];
                    planElem.TradeId = myTrade.Trade.Id; // регистрируем выполнение заявки
                    
                    Book[ordnum].BuyPrice = myTrade.Trade.Price;
                    Book[ordnum].BuyTrade = myTrade.Trade.Id;
                    Book[ordnum].BuyTradeDate = myTrade.Trade.Time;

                }
                else
                { // дополняем таблицу данными о сделке продажи

                    var ordlast = Book.Where(f => f.BuyOrder.ToString() == ord.Comment);
                    //ordlast = Book.Where(f => String.Compare(f.BuyOrder.ToString(),ord.Comment)==0);
                    if (ordlast.Any() == false)
                    {
                        for (int i = 0; i < Book.Count; i++)
                            if (Book[i].ToString() == ord.Comment) ordnum = i + 1;
                    }
                    else ordnum = ordlast.First().Id;
                    
                    planElem = Plan[Book[ordnum].SignalId - 1];
                    planElem.TradeId = 0; // регистрируем выполнение заявки
                    planElem.Vol = 0; // снимаем размер 

                    Book[ordnum].SellOrder = ord.TransactionId;
                    Book[ordnum].SellOrderDate = ord.Time;
                    Book[ordnum].SellPrice = myTrade.Trade.Price;
                    Book[ordnum].SellTrade = myTrade.Trade.Id;
                    Book[ordnum].SellTradeDate = myTrade.Trade.Time;
                    Book[ordnum].prFit = Book[ordnum].SellPrice - Book[ordnum].BuyPrice;
                    Book[ordnum].prPcn = Book[ordnum].prFit / Book[ordnum].BuyPrice;

                    var span = (Book[ordnum].SellTradeDate - Book[ordnum].BuyTradeDate);
                    decimal spanVal = 0; 
                    if   (span.TotalDays < 1) spanVal = 1;
                    else spanVal = (decimal) span.TotalDays;
                    Book[ordnum].prAnn = Book[ordnum].prPcn * 365 / spanVal;

                 }

            }
        }

Иван З. nik
JaguarFX
JaguarFX
  1. далее создаем торговую стратегию TLSStrategy, в которой основными свойствами будут

---
        private int _maxPrice = 110;
        [DisplayName(@"Max Price")]
        [Description(@"Минимальная цена")]
---
        private int _minPrice = 80;
        [DisplayName(@"Min Price")]
        [Description(@"Максимальная цена")]
---
        private int _step = 1;
        [DisplayName(@"Step")]
        [Description(@"Шаг цены")]
---
        private decimal _LongCapVal = 0;
        [Browsable(false)]
        [DisplayName(@"LongCapVal")]
        [Description(@"Оценка капитала")]
---
        private decimal _NBins = 0;
        [Browsable(false)]
        [DisplayName(@"NBins")]
        [Description(@"Количество уровней")]

В самой стратегии реализуем несколько открытых свойств для связи с интерфейсом:


        public int NBins { get; set; }
        public CandleSeries series { get; set; }
        public SynchronizedCollection<PlanElem> StrPlan { get; set; }
        public SynchronizedCollection<BookElem> StrBook { get; set; }
        public TradeManager trman;

И далее создаем три основных процедуры, которые будут обеспечивать ее работу: а) процедуру инициации, основное предназначение которой - создать План торгов до старта стратегии


 public void Initialize()
        {
            this.Volume = Params.Volume;
            if (StrPlan == null || StrPlan.Count == 0) StrPlan = new SynchronizedCollection<PlanElem>();

            prmStr = new PlanParams();
            prmStr.MaxPriceVal = Params.Mode == enMode.Execution ? ((TLSStrProp)Params).MaxPrice : ((TLSTestProp)Params).MaxPrice;
            prmStr.CurPriceVal = Params.Mode == enMode.Execution ? ((TLSStrProp)Params).СurPrice : ((TLSTestProp)Params).СurPrice;
            prmStr.MinPriceVal = Params.Mode == enMode.Execution ? ((TLSStrProp)Params).MinPrice : ((TLSTestProp)Params).MinPrice;
            prmStr.StepVal = Params.Mode == enMode.Execution ? ((TLSStrProp)Params).Step : ((TLSTestProp)Params).Step;
            prmStr.CapShortVal = Params.Mode == enMode.Execution ? ((TLSStrProp)Params).CapShort : ((TLSTestProp)Params).CapShort;
            prmStr.CapLongVal = Params.Mode == enMode.Execution ? ((TLSStrProp)Params).CapLong : ((TLSTestProp)Params).CapLong;
            prmStr.MarginVal = Params.Mode == enMode.Execution ? Security.MarginBuy : ((BaseShellTestProp)Params).Margin;
            prmStr.Maxlength = 500;
            prmStr.iType = Security.ExchangeBoard == ExchangeBoard.Forts ? SecurityTypes.Future : SecurityTypes.Stock;

            trman = new TradeManager(this);
            trman.CreateTradePlan(prmStr);
            StrBook = trman.Book;
        }

б) процедуру запуска, в которой устанавливаем основные вызовы процедур обработки свечей и ведения Торговой книги


        protected override void OnStarted()
        {
            base.OnStarted();
            if (StrPlan == null || StrPlan.Count == 0) Initialize();

            series
                .WhenCandlesFinished()
                .Do(ProcessCandle)
                .Apply(this);

            this.WhenOrderRegistered()
                .Do(trman.RegisterPosition) // Создание записи в журнале о заявке
                .Apply();

            this.WhenNewMyTrades()
                .Do(trman.UpdateBook) // Обновление позиции после выполнения заявки в торговой системе
                .Apply();
            
            this.OrdersKeepTime = TimeSpan.FromDays(30);
        }

в) и конечно же процедуру обработки свечей, в которой реализуем основную логику создания заявок на покупку/продажу актива:


private void ProcessCandle(Candle candle)
{
			
            var timeFrame = (TimeSpan)candle.Arg;
            var time = timeFrame.GetCandleBounds(Security).Min - timeFrame;
		    if (candle.OpenTime < time)
		        return;

		    _CandeslProcessed = _CandeslProcessed + 1;

            // если наша стратегия в процессе остановки
			if (ProcessState == ProcessStates.Stopping)
			{// отменяем активные заявки
				CancelActiveOrders();
				return;
			}

	   // обрабатываем новую свечку
            int mhit = 0;
	    decimal closepr = candle.ClosePrice;
            DateTime DT = candle.CloseTime;
            var planElemCur = new PlanElem();
            var planElemPrev = new PlanElem();

            for (int i = 1; i <= NBins; i++)
            {
                planElemCur = trman.Plan[i];
                planElemPrev = trman.Plan[i - 1];
                string s = "Hold";
                if (closepr > planElemPrev.PlanPrice && closepr <= planElemCur.PlanPrice)
                    mhit = i; // отмечаем уровень цены mhit как номер планового уровня i			

                if (mhit > 0 && planElemCur.Vol == 0 && planElemCur.TradeId == 0) // если цена ниже планового уровня, а уровень пуст то закупаем
                {   
                    var capAv = CheckCap();
                    if (capAv)
                    {//создаем ордер на открытие позиции
                        var order = this.CreateOrder(OrderDirections.Buy, closepr + 10 * Security.MinStepSize, Volume);
                        order.Comment = (i + 1).ToString(); // порядковый номер в плане i+1
                        planElemCur.Vol = (int)Volume;
                        planElemCur.BuyDate = DT;
                        RegisterOrder(order);
                        planElemCur.OrderId = order.TransactionId;
                    }
                    else
                    {  this.AddWarningLog("Trade is missed for capital breach!");
                    }                    
                }

                if (mhit == 0 && planElemCur.Vol > 0 && planElemPrev.TradeId > 0) // если на уровне ниже чем цена оказывается позиция j+1, то ее закрываем
                {
                    planElemPrev.SellDate = DT;
                    //создаем ордер на закрытие позиции                     
                    var orderCl = this.CreateOrder(OrderDirections.Sell, planElemPrev.SellPrice, planElemPrev.Vol);
                    orderCl.Comment = planElemPrev.OrderId.ToString(); // сохраняем первый номер для связки ордеров
                    RegisterOrder(orderCl);
                }
            }
    }

  1. очевидно, что капитала может не хватить в процессе резкого падения рынка, как это было при присоединении Крыма, поэтому а) создаем свойства стратегии для хранения размера использованного капитала и %

         private decimal _longCapt = 0;
        [Browsable(false)]
        [DisplayName(@"longCapt")]
        [Description(@"Использованный капитал")]
---
        private decimal _longPcnt = 0;
        [Browsable(false)]
        [DisplayName(@"longPcnt")]
        [Description(@"Процент использованния капитала")]

б) создаем в классе TradeManager счетчик CalcCapReq для подсчета использованного капитала:


        public void CalcCapReq()
        {
            int longCountVal = 0;
            decimal longCaptVal = 0;
            decimal longPcntVal = 0;

            if (Plan == null) return;

            for (int i = 0; i <= _NBins; i++)
            {
                if (Plan[i].Vol > 0)
                {
                    longCountVal = longCountVal + 1;
                    longCaptVal = longCaptVal + Plan[i].CapLong;
                }
            }

            if (LongCapVal > 0)
                longPcntVal = longCaptVal / LongCapVal;
            else longPcntVal = 0;
      }

в) вызываем данный счетчик после открытия длинной позиции, например в процедуре UpdateBook с) при обработки сигнала на покупки проверяем наличие капитала в процедуре CheckCap


        private bool CheckCap()
        {
            var lnCaP = (Params.Mode == enMode.Execution) ? ((TLSStrProp)Params).longPcnt : ((TLSTestProp)Params).longPcnt;
            if (lnCaP>(decimal)0.99) this.AddWarningLog("Capital limit is reached");
            return (lnCaP < 1);

        }

и если капитала недостаточно для удержания уже набранной позиции, то открытие новых позиций не проводим, создавая об этом запись в логе this.AddWarningLog("Trade is missed for capital breach!")

JaguarFX
JaguarFX

Итак в целом миграция завершена. Нужно признать что кода в StockSharp получилось гораздо больше, чем Tradematic - раза в три больше. Но тут как раз и пора показать основное преимущество StockSharp - открытость кода для создания пользователького интерфейса управления работающей стратегией.

  1. Создаем элементы для визуализации кастомарных свойств новой стратегии а) вначале создаем StrategyPlanDG для привязки Плана торговли

<Grid DataContext="{Binding RelativeSource={RelativeSource Self}}" >
        <DataGrid Name="PlanDG" AutoGenerateColumns="False" IsReadOnly="False" >
                <DataGrid.Columns>
                <DataGridTextColumn Width="30"  Header="Id" Binding="{Binding Path=Id}" IsReadOnly="True" />
                <DataGridTextColumn Width="80"  Header="Сигнал" Binding="{Binding Path=SignalName}" IsReadOnly="True"/>
                <DataGridTextColumn Width="80" Header="Покупка" Binding="{Binding Path=PlanPrice, StringFormat=N0}" IsReadOnly="True"/>
                <DataGridTextColumn Width="80" Header="Продажа" Binding="{Binding Path=SellPrice, StringFormat=N0}" IsReadOnly="True" />
                <DataGridTextColumn Width="60"  Header="Объем" Binding="{Binding Path=Amount, ValidatesOnExceptions=True,  StringFormat=F0}" IsReadOnly="True" />
                <DataGridTextColumn Width="60" Header="Прибыль" Binding="{Binding Path=LevelGain, StringFormat=P2}" IsReadOnly="True" />
                <DataGridTextColumn Width="80" Header="CapShort" Binding="{Binding Path=CapShort,StringFormat=N0}" IsReadOnly="True" />
                <DataGridTextColumn Width="80" Header="CapShort%" Binding="{Binding Path=CapShortPcn,StringFormat=P2}" IsReadOnly="True" />
                <DataGridTextColumn Width="80" Header="CapLong" Binding="{Binding Path=CapLong,StringFormat=N0}" IsReadOnly="True" />
                <DataGridTextColumn Width="80" Header="CapLong%" Binding="{Binding Path=CapLongPcn,StringFormat=P2}" IsReadOnly="True" />
                <DataGridTextColumn Width="100" Header="Посл.покупка" Binding="{Binding Path=BuyDate, StringFormat='dd.MM.yy HH:mm'}" IsReadOnly="True"/>
                <DataGridTextColumn Width="60"  Header="Позиция" Binding="{Binding Path=Vol, Mode=TwoWay, StringFormat=N0}" />
                <DataGridTextColumn Width="100" Header="Посл.продажа" Binding="{Binding Path=SellDate, StringFormat='dd.MM.yy HH:mm'}" IsReadOnly="True"/>
            </DataGrid.Columns>

и выводим его на вкладку в элементе TestingPanel стандартного интерфейса S#.Shell +

 
                <TabItem x:Name="TabPlan" Header="Plan" Visibility="Hidden">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="25"></RowDefinition>
                            <RowDefinition Height="25"></RowDefinition>
                            <RowDefinition Height="*"></RowDefinition>
                        </Grid.RowDefinitions>
                        <DockPanel>
                            <Button x:Name="btnCalc" Width="80" Content="Create" Click="BtnCalc_OnClick" Margin="10,5,0,0" HorizontalAlignment="Left" ></Button>
                            <Button x:Name="btnFill" Width="80" Content="Fill" Click="BtnFill_OnClick" Margin="10,5,0,0" HorizontalAlignment="Left" />
                            <Button x:Name="btnRecalc" Width="80" Content="Usage" Click="BtnRecalc_OnClick" Margin="10,5,0,0" HorizontalAlignment="Left" />
                        </DockPanel>
                        <DockPanel x:Name="CapitalInfo" Grid.Row="1" x:FieldModifier="public">
                            <Label Content="Уровней" Width="60" HorizontalAlignment="Left" />
                            <TextBlock x:Name="nbins" Width="40" Text="{Binding Path=NBins}" x:FieldModifier="public" HorizontalAlignment="Left" VerticalAlignment="Center" />
                            <Label Content="Cap Short" Width="70" HorizontalAlignment="Left" />
                            <TextBlock x:Name="shortVal" Width="60" Text="{Binding Path=ShortCapVal, StringFormat=N0}" x:FieldModifier="public" HorizontalAlignment="Left" VerticalAlignment="Center" />
                            <Label Content="Cap Long" Width="70" HorizontalAlignment="Left" />
                            <TextBlock x:Name="longVal" Width="60" x:FieldModifier="public" Text="{Binding Path=LongCapVal, StringFormat=N0}" HorizontalAlignment="Left" VerticalAlignment="Center" />
                            <Label Content="Занято" Width="60" HorizontalAlignment="Left" Margin="10,0,0,0" />
                            <TextBlock x:Name="filVal" Width="40"  Text="{Binding Path=longCount}" x:FieldModifier="public" HorizontalAlignment="Left" VerticalAlignment="Center" />
                            <Label Content="Исп. капитала" Width="100" HorizontalAlignment="Left" />
                            <TextBlock x:Name="utilVal" Width="60"  Text="{Binding Path=longCapt, StringFormat=N0}" x:FieldModifier="public" HorizontalAlignment="Left" VerticalAlignment="Center" />
                            <Label Content="Исп.%" Width="50" HorizontalAlignment="Left" />
                            <TextBlock x:Name="utilPcn" Width="40" Text="{Binding Path=longPcnt, StringFormat=P2}" x:FieldModifier="public" HorizontalAlignment="Left" VerticalAlignment="Center" />
                        </DockPanel>
                        <robot:StrategyPlanDG x:Name="myPlanDG" x:FieldModifier="public" Grid.Row="2"/>
                    </Grid>
                </TabItem>

по умолчанию делаем элемент скрытым, чтобы не мешал при работе других стратегий. б) создаем StrategyBook для визуализации торговой книги


    <ListView x:Name="BookView"  x:FieldModifier="public" SelectionMode="Single"
              DataContext="{Binding RelativeSource={RelativeSource Self}}"
              HorizontalContentAlignment="Stretch">
        <ListView.View >
            <GridView >
                <GridViewColumn Width="30"  Header="Id" DisplayMemberBinding="{Binding Path=Id}" />
                    <GridViewColumn Width="80" Header="Уровень" DisplayMemberBinding="{Binding Path=PlanPrice, StringFormat=F2}"/>
                    <GridViewColumn Width="100" Header="Заявка покупки" DisplayMemberBinding="{Binding Path=BuyOrder}" />
                <GridViewColumn Width="80" Header="Дата заявки" DisplayMemberBinding="{Binding Path=BuyOrderDate, StringFormat='dd.MM.yy HH:mm'}" />
                    <GridViewColumn Width="100" Header="Сделка покупки" DisplayMemberBinding="{Binding Path=BuyTrade, StringFormat=F0}" />
                <GridViewColumn Width="80" Header="Дата покупки" DisplayMemberBinding="{Binding Path=BuyTradeDate,StringFormat='dd.MM.yy HH:mm'}" />
                    <GridViewColumn Width="90" Header="Цена покупки" DisplayMemberBinding="{Binding Path=BuyPrice, StringFormat=F2}" />
                    <GridViewColumn Width="50"  Header="Объем" DisplayMemberBinding="{Binding Path=Amount}" />
                    <GridViewColumn Width="100" Header="Заявка продажи" DisplayMemberBinding="{Binding Path=SellOrder}" />
                <GridViewColumn Width="80" Header="Дата заявки" DisplayMemberBinding="{Binding Path=SellOrderDate, StringFormat='dd.MM.yy HH:mm'}" />
                    <GridViewColumn Width="100" Header="Сделка продажи" DisplayMemberBinding="{Binding Path=SellTrade, StringFormat=F0}" />
                <GridViewColumn Width="80" Header="Дата продажи" DisplayMemberBinding="{Binding Path=SellTradeDate, StringFormat='dd.MM.yy HH:mm'}" />
                    <GridViewColumn Width="90" Header="Цена продажи" DisplayMemberBinding="{Binding Path=SellPrice, StringFormat=N2}" />
                    <GridViewColumn Width="80" Header="Прибыль" DisplayMemberBinding="{Binding Path=prFit, StringFormat=F2}" />
                    <GridViewColumn Width="60" Header="Приб.%" DisplayMemberBinding="{Binding Path=prPcn, StringFormat=P2}" />
                    <GridViewColumn Width="+60" Header="Приб.год." DisplayMemberBinding="{Binding Path=prAnn, StringFormat=P2}" />
            </GridView>
            </ListView.View>
        </ListView>

и так же выводим его в TestingPanel со начальным скрытым состоянием:


                <TabItem x:Name="TabBook" Header="Book" Visibility="Hidden">
                    <robot:StrategyBook x:Name="myMEQ" MinHeight="200" Height="Auto" x:FieldModifier="public"/>
                </TabItem>

Как результат - получаем готовый интерфейс для управления работой стратегии в процессе ее выполнения. Это именно то, что никак невозможно сделать в Tradematic в силу закрытости кода.

JaguarFX
JaguarFX

Часть 8 - Универсализация интерфейсов исполнения стратегий

Сделаем еще один шаг к раз0витию S#.Shell - проведем универсализацию интерфейсов исполнения. Как известно в стандартной версии S#.Shell тестовые стратегии выполняются на панеле (документе), где присутствуют графики и где можно добавить собственные панели (см. Часть 5), а исполнение на живом подключении проводится на панеле с сокращенным набором элементов. Это не всегда удобно. Да и для новых стратегий тоже важно в первые месяцы их жизни следить за их состоянием с использованием графических элементов.

Для решения этой задачи вначале разбираемся как в В S#.Shell создаются панели. Панель это на самом деле объект типа LayoutDocument из библиотеки AvalonDock, в котором можно задавать различный контент путем присвоения свойства Content типа object. При этом в свойство Content записывается пользовательский элемент управления типа UserControl, графические элементы которого и определяют расположение и набор элементов. Исполняемая стратегия у данного элемент управления это всего лишь свойство типа BaseShellStrategy, которое очевидно может выполняться в любом режиме - как тестовом, так и боевом.

С учетом этого для получения возможности запускать стратегии с любым типом панели делаем следующие вещи:

  1. Создаем в свойствах стратегии BaseShellStrategyPropeties признак , который указываем на каком виде документа выполняется стратегия:

        private enDocType _doctype = enDocType.TestDoc;
        [Category(@"Основные")]
        [DisplayName(@"Тип документа")]
        [Description(@"Тип документа стратегии")]
        [PropertyOrder(2)]
        public enDocType DocType
        {
            get { return _doctype; }
            set
            {
                _doctype = value;
                OnPropertyChanged("DocType");
            }
        }

При этом enDocType задаем как перечисляемый тип


    public enum enDocType
    {
        TestDoc,
        ExecDoc
    }

  1. В классе MainWindow во всех процедурах типа ExecutedAddХХХХStrategy создания исполняемых стратегий, в которых мы хотим получить выбор типа документа исполнения, задаем возможность указать желаемый вид документа:

            DialogResult result;
            string selectstr = "Тип документа: сокращенный(Y) или полный(N)?";
            result = System.Windows.Forms.MessageBox.Show(selectstr, "Выбор типа документа", MessageBoxButtons.YesNo);
            if (result == System.Windows.Forms.DialogResult.Yes)
                properties.DocType = enDocType.ExecDoc;
            else
                properties.DocType = enDocType.TestDoc;

  1. Далее классе MainWindow в связанных процедурах создания стратегий модифицируем создание LayoutDocument, используя указанный признак для присвоения желаемого типа документа

            LayoutDocument doc=new LayoutDocument();
            doc.Content = docType == enDocType.ExecDoc? (object) new StrategyDocument {Strategy = strategy}: new TestingDocument {Strategy = strategy};
            doc.Title = strategy.Params.Name;
            doc.CanClose = false;

  1. Из процедуры StartTesingStrategy выводим весь код по инициализации графических панелей в отдельную процедуру InitializePanes, которую затем безу3словно вызываем из StartTesingStrategy и условино вызываем из процедуры StartStrategy:

if (_strategy.Params.Mode == enMode.Testing)
{
            Action InitPanes = null;
		    InitPanes = () =>
                    {
                        var doc = SelectedTestingDocument;
                        InitializePanes(doc);
		     };

		    if (dockManager.Dispatcher.CheckAccess())
		        InitPanes();
		    else
		        dockManager.Dispatcher.BeginInvoke(DispatcherPriority.Normal, InitPanes);
}

Причем по каким-то причинам в случае прямого вызова InitializePanes(doc) при исполнении стратегии происходят ошибки связанные с пересечением потоков, поэтому как в приведенном выше коде использовалась лямбда-функция InitPanes, которая получает тестовый документ через SelectedTestingDocument и вызывввается в потокобезопасном режиме.

И вот все готово!

JaguarFX
JaguarFX

Часть 9 - Проходим АД AdoNeta, или о сохранении параметров в базе данных

Как известно, в базовой версии S#.Shell отсутствует функционал, позволяющий восстановить позиции после перезапуска робота. В версии от КазайМазай был предложен метод сохранения в xml-файл через набор свойств базовой стратегии (OrdersByTransactionId, SecuritiesByTransactionId, TradesByTransactionId, PortfoliosByTransactionId, ExchangesByTransactionId) типа SettingsStorage. При этом у меня при всех старании активировать данное сохранение, данный метод не заработал - при активации RestorePositionsOnStart записанные теги оказывались пустыми. На форуме так же описан метод сохранения ордеров в txt-файл, который судя по простоте работает.

Но все же сохранение в файл при всей простоте восстановления позиций такого важного элемента в созхранении записей как ведение управленческого учета прибылей и убытков от работы робота.

Именно по этой причине целесообразно реализовать функционал сохранения истории торгов в базу данных (MSSQLSRV, MSACCESS, SQLLITE и пр.) - гибкость в последобработке данной информации позволяет автоматизировать ведение учета результатов торгов.

Рассмотрим реализацию сохранения состояния робота в базу данных основе AdoNet.

  1. Создадим базовый класс StorageEngine Данный класс будет хранить обертки для подключений. По умолчанию программируем только для одного вида базы данных - MSSQLSRV, хотя при дальнейшей проработки данного класса его можно сделать универсальным провайдером функций для работы с любым выбранным пользователем видом базы данных, для которого есть драйвера AdoNet (MSSQLSRV, MSACCESS, SQLLITE)

а) функция инициализации будет содержать переменную по проверке готовности БД IsVerified, а так же вызов заданных пользователем параметров: curStor - вид базы данных, ShellConfigDB - название каталога MSSQLSRV (файла для MSACCESS и SQLLITE)


        private StorageEngine()
		{
            Name = "StorageEngine";
            IsVerified = false;
            ShellConfigDB = SettingsEngine.Instance.Properties.ShellConfigDB;
            curStor = SettingsEngine.Instance.Properties.DataStorIn;
		}

б) переменные для хранения универсальных команд создания базовых таблиц (tblOrders - таблица заявок, tblTrades - таблица сделок), добавления данных и удаления данных из этих таблиц


        public const string CreateOrders = @"CREATE TABLE tblOrders " +
                                            "(StrategyId varchar(50), " +
                                            "TransactionId INT, " +
                                            "OrdId BIGINT, " +
                                            "Direction varchar(4), " +
                                            "Type varchar(10), " +
                                            "ExpiryDate date, " +
                                            "State varchar(10), " +
                                            "Status varchar(10), " +
                                            "Security varchar(20), " +
                                            "ExBoard varchar(10), " +
                                            "Portfolio varchar(10), " +
                                            "Price FLOAT, " +
                                            "Time datetime , " +
                                            "Volume int , " +
                                            "Comment varchar(50))";

        public const string CreateTrades = @"CREATE TABLE tblTrades " +
                                            "(StrategyId varchar(50), " +
                                            "TransactionId int , " +
                                            "TradeId int, " +
                                            "Price float, " +
                                            "Time datetime, " +
                                            "Volume int)";

        public const string SelectOrders =
                 @"SELECT StrategyId, OrdId, TransactionId, Direction, Type, ExpiryDate, State, Status, 
                   Security, ExBoard, Portfolio, Price, Time, Volume, Comment " +
                  @"FROM tblOrders WHERE StrategyId=@SID";

        public const string DeleteOrders =
            @"DELETE FROM tblOrders WHERE StrategyId=@SID";

        public const string SelectTrades =
            @"SELECT StrategyId, TransactionId, TradeId, Price, Time, Volume " +
            @"FROM tblTrades WHERE StrategyId=@SID and TransactionId=@TID";

        public const string DeleteTrades =
            @"DELETE FROM tblTrades WHERE StrategyId=@SID";


в) создадим ряд оберток для различных функций по работе с базами данных: в данном случае функции возвращают MSSQLSRV- специфичные объекты, и не являются универсальными обертками, но принцип ясен



        public SqlConnection GetConnection()
        {
            SqlConnectionStringBuilder sqlConStr = new SqlConnectionStringBuilder();
            sqlConStr.DataSource = @"(local)";
            sqlConStr.IntegratedSecurity = true;
            sqlConStr.InitialCatalog = ShellConfigDB;
            SqlConnection result = new SqlConnection(sqlConStr.ConnectionString);
            return result;
        }

        public SqlDataAdapter OpenData()
        {
            SqlDataAdapter da1 = new SqlDataAdapter();
            return da1;
        }

        public SqlCommand SqlCmd(string sqlScmd, DbConnection sqlCon)
        {
            SqlCommand cmd1 = new SqlCommand(sqlScmd, (SqlConnection)sqlCon);
            return cmd1;
        }

        public SqlCommandBuilder BuildCmd(DbDataAdapter da)
        {
            SqlCommandBuilder cb1 = new SqlCommandBuilder((SqlDataAdapter)da);
            return cb1;
        }


г) создадим функцию VerifyStor, которая будет вызываться при первичном обращении к классу для проверки готовности БД к приему данных, если БД не готова - отсутствуют таблицы, то автоматом создаются новые структуры:


        public void VerifyStor()
        {
            if (IsVerified) return;

            switch (curStor)
            {
                case SettingsProperties.DataStorType.MSSQLSRV:

                    SqlConnection sqlCon = GetConnection();
                    sqlCon.Open();
                    if (sqlCon.State != ConnectionState.Open)
                    {
                        MessageBox.Show("Подключение завершилось с ошибкой");
                        return;
                    }

                    var sqlStr = @"SELECT count(name)  FROM [ShellAdvanced].[sys].[tables] where name = 'tblOrders'";
                    SqlCommand sqlCmd = new SqlCommand(sqlStr, sqlCon);
                    int rowCount = (int) sqlCmd.ExecuteScalar();
                    sqlCmd.Dispose();

                    if (rowCount == 0)
                    {
                        SqlCommand sqlCmd1 = new SqlCommand(CreateOrders, sqlCon);
                        sqlCmd1.ExecuteNonQuery();
                        sqlCmd1.Dispose();

                        SqlCommand sqlCmd2 = new SqlCommand(CreateTrades, sqlCon);
                        sqlCmd2.ExecuteNonQuery();
                        sqlCmd2.Dispose();

                        SqlCommand sqlCmd3 = new SqlCommand(CreateBook, sqlCon);
                        sqlCmd3.ExecuteNonQuery();
                        sqlCmd3.Dispose();

                    }
                    sqlCon.Close();
                    break;
                 
            }

            IsVerified = true;
        }

При этом конструкция if (IsVerified) return; позволяет минимизировать время проверки за счет сохранения статуса "Проверено" в свойстве IsVerified.

  1. Для однозначной идентификации сохраненной стратегии используем Strategy.Id - свойство типа GUID, которое генерится автоматически при создании новой стратегии и в силу алгоритма генерации GUID позволяет фактически уникально определить стратегию.

а) в свойства добавляем поле StrategyID для сохранения GUID в текстовом виде



        private string _StrategyID;

        [DisplayName(@"Strategy ID")]
        [Description(@"Уникальный идентификатор стратегии")]
        [Category(@"Основные")]
        [PropertyOrder(7)]
        public string StrategyID
        {
            get { return _StrategyID; }
            set
            {
                _StrategyID = value;
                OnPropertyChanged("StrategyID");
            }
        }

б) вносим восстановление GUID в различные функции создания стратегий а-ля AddRobotStrategy/AddRobotTestStrategy в классе MainWindow:


            if (properties.StrategyID != null)
                strategy.Id = new Guid(properties.StrategyID);
            else
                properties.StrategyID = strategy.Id.ToString();

  1. Теперь напишем функции сохранения и загрузки заявок/сделок из MSSQLSRV, разместив их в классе SettingsEngine

а) функция SaveToDB(Strategy str) принимает на вход стратегию, сделки и заявки которой необходимо сохранить



public void SaveToDB(Strategy str)
	    {

            SqlDataAdapter sqlDa;
            DataTable dataTable;
	        SqlCommand sqlSCmd; // Select command
            SqlCommand sqlICmd; // Insert command
            SqlCommand sqlDCmd; // Delete commend
	        SqlCommandBuilder sqlCMDB;

            StorageEngine.Instance.VerifyStor(); // Проверка базы


            var sqlCon = StorageEngine.Instance.GetConnection();

            // Сохраняем сделки
                var sid = str.Id.ToString();
                var qrOrders = from r in str.MyTrades
                               select
                                   new
                                   {   StriD = sid,
                                       TID = r.Order.TransactionId,
                                       OrdId = r.Order.Id,
                                       OrdDir = r.Order.Direction.ToString(),
                                       OrdType = r.Order.Type.ToString(),
                                       r.Order.ExpiryDate,
                                       OrdState = r.Order.State.ToString(),
                                       OrdStatus = r.Order.Status.ToString(),
                                       SecId = r.Order.Security.Id,
                                       SecExch = r.Order.Security.Board.Code,
                                       Portname = r.Order.Portfolio.Name,
                                       r.Order.Price,
                                       r.Order.LastChangeTime,
                                       r.Order.Volume,
                                       r.Order.Comment
                                   };

                if (qrOrders.Count()==0) return;

                sqlDa = StorageEngine.Instance.OpenData();
                sqlSCmd = StorageEngine.Instance.SqlCmd(StorageEngine.SelectOrders, sqlCon);
                sqlSCmd.Parameters.AddWithValue("@SID", sid);
	            sqlDa.SelectCommand = sqlSCmd;

                sqlCMDB = StorageEngine.Instance.BuildCmd(sqlDa);
                sqlICmd = sqlCMDB.GetInsertCommand();

                dataTable = new DataTable();
                sqlDa.Fill(dataTable);

                sqlCon.Open();
                sqlDCmd = StorageEngine.Instance.SqlCmd(StorageEngine.DeleteOrders, sqlCon);
                sqlDCmd.Parameters.AddWithValue("@SID", sid);
                sqlDCmd.ExecuteNonQuery();
                sqlCon.Close();

                foreach (var qrOrder in qrOrders)
                {
                    DataRow rw = dataTable.NewRow();
                    rw["StrategyId"] = qrOrder.StriD;
                    rw["TransactionId"] = qrOrder.TID;
                    rw["OrdId"] = qrOrder.OrdId;
                    rw["Direction"] = qrOrder.OrdDir;
                    rw["Type"] = qrOrder.OrdType;
                    rw["ExpiryDate"] = qrOrder.ExpiryDate;
                    rw["State"] = qrOrder.OrdState;
                    rw["Status"] = qrOrder.OrdStatus;
                    rw["Security"] = qrOrder.SecId;
                    rw["ExBoard"] = qrOrder.SecExch;
                    rw["Portfolio"] = qrOrder.Portname;
                    rw["Price"] = qrOrder.Price;
                    rw["Time"] = qrOrder.LastChangeTime;
                    rw["Volume"] = qrOrder.Volume;
                    rw["Comment"] = qrOrder.Comment;
                    dataTable.Rows.Add(rw);
                }
                var cnt = sqlDa.Update(dataTable);
                str.AddInfoLog("В базу сохранено {0} заявок", cnt);
                
                dataTable.Dispose();

                // Запись сделок
                var qrTrades = from tr in str.MyTrades
                               select
                                   new
                                   {   StrId = sid,
                                       TID = tr.Order.TransactionId,
                                       tr.Trade.Id,
                                       tr.Trade.Price,
                                       tr.Trade.Time,
                                       tr.Trade.Volume};

                sqlDa = StorageEngine.Instance.OpenData();
                sqlSCmd = StorageEngine.Instance.SqlCmd(StorageEngine.SelectTrades, sqlCon);
                sqlSCmd.Parameters.AddWithValue("@SID", sid);
                sqlSCmd.Parameters.AddWithValue("@TID", "");
                sqlDa.SelectCommand = sqlSCmd;

                sqlCMDB = StorageEngine.Instance.BuildCmd(sqlDa);
                sqlICmd = sqlCMDB.GetInsertCommand();

                dataTable = new DataTable();
                sqlDa.Fill(dataTable);
                
                // Удаляем старые записи
                sqlCon.Open();
                sqlDCmd = StorageEngine.Instance.SqlCmd(StorageEngine.DeleteTrades, sqlCon);
                sqlDCmd.Parameters.AddWithValue("@SID", sid);
                sqlDCmd.ExecuteNonQuery();
                sqlCon.Close();

                foreach (var qrTrade in qrTrades)
                {
                    DataRow rw = dataTable.NewRow();
                    rw["StrategyId"] = qrTrade.StrId;
                    rw["TransactionId"] = qrTrade.TID;
                    rw["TradeId"] = qrTrade.Id;
                    rw["Price"] = qrTrade.Price;
                    rw["Time"] = qrTrade.Time;
                    rw["Volume"] = qrTrade.Volume;
                    dataTable.Rows.Add(rw);
                }
                var cntn = sqlDa.Update(dataTable);

                str.AddInfoLog("В базу сохранено {0} сделок", cntn);         
                
	    }

б) функция LoadFromDB (Strategy str) принимает на вход стратегию, сделки и заявки которой необходимо загрузить ```csharp

    public void LoadFromDB(Strategy str)
      {

        var sqlCon = StorageEngine.Instance.GetConnection();
        SqlDataAdapter sqlDa;
        SqlCommand sqlSCmd; // Select command
        DataTable TableOrders = new DataTable();
        DataTable TableTrades = new DataTable();
        DataTable TableBook = new DataTable();


            var sid = str.Id.ToString();
            sqlDa = StorageEngine.Instance.OpenData();
            sqlSCmd = StorageEngine.Instance.SqlCmd(StorageEngine.SelectOrders, sqlCon);
            sqlSCmd.Parameters.AddWithValue("@SID", sid);
            sqlDa.SelectCommand = sqlSCmd;
            sqlDa.Fill(TableOrders);

            str.AddInfoLog("Из базы загружено {0} заявок", TableOrders.Rows.Count.ToString());    
            if (TableOrders.Rows.Count == 0) return;
            
            foreach (DataRow rw in TableOrders.Rows)
            {
                long tid = (int)rw["TransactionId"];
                var order = new Order() { TransactionId = tid };
                long oid = (long)rw["OrdId"];
                order.Id = oid;
                if (order != null)
                {
                    order.Connector = str.Connector;
                    var security = new Security() {Id = (string) rw["Security"]};
                    if (security != null)
                    {
                        var exchange = new ExchangeBoard() {Code = (string)rw["ExBoard"]};
                        if (exchange != null) 
                            security.Board = exchange;

                        security.Connector = str.Connector;
                        order.Security = security;
                    }
                    
                    OrderTypes OrdType;
                    OrderTypes.TryParse((string)rw["Type"], out OrdType);
                    order.Type = OrdType;

                    Sides OrderDir;
                    Sides.TryParse((string)rw["Direction"], out OrderDir);
                    order.Direction = OrderDir;

                    OrderStates OrdState;
                    OrderStates.TryParse((string)rw["State"],out OrdState);
                    order.State = OrdState;

                    order.Portfolio = new Portfolio() {Name = (string)rw["Portfolio"]};
                    order.ExpiryDate = (DateTime)rw["ExpiryDate"];
                    order.Price = (decimal)(double)rw["Price"];
                    order.Volume = (decimal)(int)rw["Volume"];
                    order.Time = (DateTime)rw["Time"];
                    order.LastChangeTime = (DateTime)rw["Time"];
                    if (! DBNull.Value.Equals(rw["Comment"]))
                        { order.Comment = (string)rw["Comment"]; } // DBNUll to String

                    var myTrades = new List<MyTrade>(); // Все сделки по заявке с номером tid
                    sqlSCmd = StorageEngine.Instance.SqlCmd(StorageEngine.SelectTrades, sqlCon);
                    sqlSCmd.Parameters.AddWithValue("@SID", sid);
                    sqlSCmd.Parameters.AddWithValue("@TID", tid);
                    var sqlTDa = StorageEngine.Instance.OpenData();
                    sqlDa.SelectCommand = sqlSCmd;
                    sqlDa.Fill(TableTrades);

                    foreach (DataRow tr in TableTrades.Rows)
                     {
                            var myTrade = new MyTrade(); 
                            myTrade.Order = order;
                            var thisTrade = new Trade() { Id = (long)(int)tr["TradeId"], Price = (decimal)(double)tr["Price"], Time = (DateTime)tr["Time"], Volume = (decimal)(int)tr["Volume"] };
                            myTrade.Trade = thisTrade;
                            myTrade.Trade.OrderDirection = order.Direction;
                            myTrade.Trade.Security = security;
                            myTrade.Trade.Status = 0;
                            myTrades.Add(myTrade);

                        }

                    try
                    {
                        str.AttachOrder(order, myTrades);
                    }
                    catch (Exception ex)
                    {
                          Debug.WriteLine(ex.Message);
                    }
                    TableTrades.Clear();   

                }
            }
            TableOrders.Dispose();
            TableTrades.Dispose();          	               

        
    }

в) тут же создаем универсальный загрузчик заявок и сделок на панели тестового документа, который как научились в Части 8 использовать как для тестовых прогонов, так и для реального запуска робота.
```csharp

        public void AddHistOrdersPane(TestingDocument doc)
        {
            Strategy str = doc.Strategy;

            var _myOrders = doc.TestingPanel.myOrders;
            _myOrders.Orders.AddRange(str.Orders);

            var _myTrades = doc.TestingPanel.myTrades;
            _myTrades.Trades.AddRange(str.MyTrades);
	    }

  1. Вызываем из нужных мест функции SaveToDB/LoadFromDB

а) запиливаем вызов SaveToDB в функцию SaveStrategies, проверяя при этом наличие в параметрах стратегии установленного свойства RestorePositionsOnStart


                    var StrToSave = _documents.Keys.Where(str => str.Params.RestorePositionsOnStart).ToList();
                    foreach (BaseShellStrategy strategy in StrToSave )
		                SettingsEngine.Instance.SaveToDB(strategy );
		           

б) аналогично запаиваем вызов SaveToDB в правила WhenOrderRegistered / WhenNewMyTrades

в) запихиваем вызов LoadFromDB в различные функции создания стратегий а-ля AddRobotStrategy/AddRobotTestStrategy в классе MainWindow:


            if (strategy.Params.RestorePositionsOnStart) SettingsEngine.Instance.LoadFromDB(strategy);

  1. Последний штрих - создаем в параметрах S#.Shell (класс SettingsProperties) свойства для сохранения переменных curStor - вид базы данных, ShellConfigDB - название каталога MSSQLSRV (файла для MSACCESS и SQLLITE)
 
         public enum DataStorType {SQLLITE,MSSQLSRV}

        private DataStorType _dataStor;
        [Category(@"Сохранение")]
        [DisplayName(@"База данных")]
        [Description(@"Вариант сохранения данных")]
        [PropertyOrder(1)]
        public DataStorType DataStorIn
        {
            get { return _dataStor; }
            set
            {
                _dataStor = value;
                OnPropertyChanged("DataStorIn");
            }
        }

        private string _ShellConfigDB;
        [Category(@"Сохранение")]
        [DisplayName(@"Название базы")]
        [Description(@"Название базы данных или имя файла")]
        [PropertyOrder(2)]
        public string ShellConfigDB
        {
            get { return _ShellConfigDB; }
            set
            {
                _ShellConfigDB = value;
                OnPropertyChanged("ShellConfigDB");
            }
        }

И вуаля - сохранение в базу MSSQLSRV работает как часы! Выглядит все просто, но чтобы пройти реально этот Ад, мне потребовалось 2 месяца арбайтена и штудирена))

Аналогично можно сохранять любые другие свойства стратегии, например PnL. Хотя тут я пошел другим путем и сделал динамическое восстановление PnL из сохраненных сделок.

JaguarFX
JaguarFX

Часть 10 - Обеспечение стабильной работы робота

Итак после всех мероприятий робот работает стабильно, торгует и восстанавливает при сбоях позиции - и самое главное - зарабатывает деньги. Но только КОГДА работает!

И тут возникают две дополнительные проблемы с обеспечением стабильности работы.

  1. Автоматический запуск и начало работы Торги на ММВБ начинаются в 10 и завершаются в 23:45, соответственно ставим задачу на автозапуск ПК в 09:55 и погружаем ПК на ночь в соню. И тут утром обнаруживается, что по истечении 5 минут работы система автоматически переходит в режим сна! При этом настройки питания - "высокая производительность", т.е. никаких отключений по времени. Начинаем разбираться и выясняем, что при использовании SSD-накопителей "умные" драйверы Windows8 мониторят действия пользователя (мыши, клавиатуры) и при их отсутствии все равно усыпляют ПК. Отключить это невозможно. Итак - лечим это через С#.
  1. Создаем класс MouseOperations, в который выносим основные функции эмуляции управления мышью:
  • SetCursorPosition,
  • GetCursorPosition,
  • MouseEvent.

class MouseOperations
    {
        [Flags]
        public enum MouseEventFlags
        {
            LeftDown = 0x00000002,
            LeftUp = 0x00000004,
            MiddleDown = 0x00000020,
            MiddleUp = 0x00000040,
            Move = 0x00000001,
            Absolute = 0x00008000,
            RightDown = 0x00000008,
            RightUp = 0x00000010
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct MousePoint
        {
            public int X;
            public int Y;

            public MousePoint(int x, int y)
            {
                X = x;
                Y = y;
            }
        }


        [DllImport("user32.dll", EntryPoint = "SetCursorPos")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetCursorPos(int X, int Y);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GetCursorPos(out MousePoint lpMousePoint);

        [DllImport("user32.dll")]
        private static extern void mouse_event(int dwFlags, int dx, int dy, int dwData, int dwExtraInfo);

        public static void SetCursorPosition(int X, int Y)
        {
            SetCursorPos(X, Y);
        }

        public static void SetCursorPosition(MousePoint point)
        {
            SetCursorPos(point.X, point.Y);
        }

        public static MousePoint GetCursorPosition()
        {
            MousePoint currentMousePoint;
            var gotPoint = GetCursorPos(out currentMousePoint);
            if (!gotPoint) { currentMousePoint = new MousePoint(0, 0); }
            return currentMousePoint;
        }

        public static void MouseEvent(MouseEventFlags value)
        {
            MousePoint position = GetCursorPosition();

            mouse_event
                ((int)value,
                 position.X,
                 position.Y,
                 0,
                 0);
        }
    }

При этом данные функции нужно делать именно производными от системной библиотеки user32.dll, т.к. использование System.Windows.Forms.Cursor и System.Windows.Input.Cursor не дет нужного эффекта - это все го лишь функции отрисовки изображения курсора, а нам нужны функции эмуляции работы мыши.

  1. в этом же классе создаем функцию отрисовки движения мыши по экрану:

public static void BresenhamCircle(int x0, int y0, int radius)
        {
            int x = radius;
            int y = 0;
            int radiusError = 1 - x;
            while (x >= y)
            {
                
                SetCursorPosition(x + x0, y + y0);
                SetCursorPosition(y + x0, x + y0);
                SetCursorPosition(-x + x0, y + y0);
                

                y++;
                if (radiusError < 0)
                {
                    radiusError += 2 * y + 1;
                }
                else
                {
                    x--;
                    radiusError += 2 * (y - x + 1);
                }
            }
        }

  1. вызываем функцию из какого-либо переодического события, например из функции OnTimeChanged(), по которой робот проверяет время работы стратегии

var isContain = Helper.Contains(strategy.Params.Schedule, time);
if (isContain) MouseOperations.BresenhamCircle(200, 200, 200); 

И вот каждые 30сек мы получаем маленькое и практически незаметное для пользователя подергивание мышки, которое однако выполняет свою основную функцию - предотвращает погружение ПК в преждевременный сон.

  1. Женский пол. Это у меня оказалось второй проблемой, так как приходя домой раньше меня девушка каждый раз "нечаянно" выключала работа)) И хотя вечерняя сессия не такая бурная по событиям как дневная, но все я стал фиксить упущенные из-за этого прибыльные сделки. И тут мы открываем для себя мир ситемного треша, в смысле трея))
  1. устанавливаем NU-get пакет Hardcodet.NotifyIcon.Wpf, который обеспечивает работы с иконками в системной трее.

  2. создаем пользовательский словарь ресурсов SysTrayResources.xaml, в котором создаем TaskbarIcon - объект иконки с системной трее, и его контекстное меню


<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:tb="http://www.hardcodet.net/taskbar"
                    xmlns:robot="clr-namespace:Robot">

    
  <ContextMenu x:Shared="false" x:Key="SysTrayMenu">
        <MenuItem Header="Show Window" Command="{Binding ShowWindowCommand}" />
        <MenuItem Header="Hide Window" Command="{Binding HideWindowCommand}" />
        <Separator />
        <MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}" />
    </ContextMenu>


    <tb:TaskbarIcon x:Key="SysTrayIcon"
                    IconSource="/Service/stocksharp.ico"
                    ToolTipText=""
                    DoubleClickCommand="{Binding ShowWindowCommand}"
                    ContextMenu="{StaticResource SysTrayMenu}">

        <tb:TaskbarIcon.DataContext>
            <robot:SysTrayViewModel />
        </tb:TaskbarIcon.DataContext>
    </tb:TaskbarIcon>

</ResourceDictionary>

Как видно из кода пункты контекстного меню вызывают функции ShowWindowCommand/HideWindowCommand/ExitApplicationCommand, которые расположены в классе SysTrayViewModel.

  1. данный словарь необходимо вызвать из App.xaml, что объект отрисовался сразу после запуска программы

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Service\SysTrayResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>

  1. далее создаем класс SysTrayViewModel, в котором размешаем команды скрытия/отображения робота:

    public class SysTrayViewModel
    {

        public ICommand ShowWindowCommand
        {
            get
            {
                return new DelegateCommand
                {
                    CanExecuteFunc = () => Application.Current.MainWindow.Visibility == Visibility.Hidden,
                    CommandAction = () =>
                    {
                   if (Application.Current.MainWindow == null) Application.Current.MainWindow = new MainWindow();
                   else Application.Current.MainWindow.Visibility = Visibility.Visible;
                    }
                };
            }
        }

        public ICommand HideWindowCommand
        {
            get
            {
                return new DelegateCommand
                {
                    CommandAction = () => Application.Current.MainWindow.Visibility = Visibility.Hidden,
                    CanExecuteFunc = () => Application.Current.MainWindow.Visibility == Visibility.Visible
                };
            }
        }

        public ICommand ExitApplicationCommand
        {
            get
            {
                return new DelegateCommand { CommandAction = () => Application.Current.Shutdown() };
            }
        }
    }

При этом нужно пояснить что DelegateCommand - это класс-делегат для ускоренного создания выполняемых команд


    public class DelegateCommand : ICommand
    {
        public Action CommandAction { get; set; }
        public Func<bool> CanExecuteFunc { get; set; }

        public void Execute(object parameter)
        {
            CommandAction();
        }

        public bool CanExecute(object parameter)
        {
            return CanExecuteFunc == null || CanExecuteFunc();
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }

Итак - контекстное меню прекрасно работает. Но этого мало))

  1. Создаем в настройках свойство Starthide, и помещаем в процедуру MainWindow() простую проверку на необходимость сразу скрыть робота после старта:

            if (SettingsEngine.Instance.Properties.Starthide)
                Application.Current.MainWindow.Visibility = Visibility.Hidden;

  1. Ставим для отображения робота ПАРОЛЬ - как же без него! Для этого создаем маленькую пользовательскую форму UIpassword, в которой размещает поле для вода текста типа PasswordBox

    <Grid >
        <PasswordBox Name="pwdBox" HorizontalAlignment="Left" Margin="17,20,0,0" VerticalAlignment="Top" Width="145" 
                     PasswordChar="#" PasswordChanged="PasswordChangedHandler" Height="29"/>
        <Label Content="Password please" HorizontalAlignment="Left" Height="24" Margin="38,-4,0,0" VerticalAlignment="Top" Width="102"/>

    </Grid>

В событии PasswordChangedHandler прописываем проверку заданного нами пароля и если все ок - то отображаем основное окно робота


        private void PasswordChangedHandler(object sender, RoutedEventArgs e)
        {
            //Debug.WriteLine(pwdBox.Password);
            if (pwdBox.Password == "ххххххх")
            {
                Application.Current.MainWindow.Visibility = Visibility.Visible;
            }
        }

И после этого в коде класса SysTrayViewModel вместо мгновенного открытия приложения прописываем старт формы UIpassword


           CommandAction = () =>
            {
              if (Application.Current.MainWindow == null) Application.Current.MainWindow = new MainWindow();
              var newWnd = new UIpassword();
              newWnd.ShowDialog();
            }

kornego dimtrdr