← Zurück

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

War dieses Thema hilfreich?

Thema teilen

Kommentare (27)

Anmelden oder Konto erstellen, Melden Sie sich an oder registrieren Sie sich, um einen Kommentar zu hinterlassen

Kazai Mazai
Kazai Mazai Autor

Часть 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 Autor

Часть 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 Autor

Переделать метод сохранения, который в 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 Autor

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

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

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

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

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

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

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

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

dij1
dij1

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

Kazai Mazai
Kazai Mazai Autor

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

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

Kazai Mazai
Kazai Mazai Autor

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

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 Autor

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