← Zurück

Создание роботов с помощью S#. Часть 3. Реализация интерфейсов

Теперь приступим к реализации интерфейсов из 2 части статьи “Создание роботов с помощью S#. Часть 2. Базовый класс для всех стратегий”. Здесь западные специалисты рекомендуют вначале реализовывать тестовые версии интерфейсов. И писать сразу UNIT-тесты. Смотрите пример SampleEmulationTesting из библиотеки S#. Но ведь программист — это высшая форма существования разума во Вселенной, и поэтому, в принципе не может ошибаться. Сразу в бой!

QuikTraderBuilder Реализуем создание торговой системы Quik. Основной метод:

public ITrader BuildTrader()

Нужно учитывать, что метод могут одновременно вызвать из разных потоков и создавать только 1 Quik.

	public sealed class QuikTraderBuilder : ITraderBuilder, ILogSource
	{
		private object lockObject = new object();
		private Task task;
		private bool isConnected;

		public string Path { get; set; }
		public string Login { get; set; }
		public string Password { get; set; }

		public string DdeServer { get; set; }
		public string DllName { get; set; }

		private string title;
		public string Title
		{
			get { return title ?? Login; }
			set { title = value; }
		}

		public ITrader Trader { get; private set; }
		public ICandleManager CandleManager { get; private set; }
		public event Action IsConnectedChanged;

		public ITrader BuildTrader()
		{
			lock (lockObject)
			{
				if (task == null)
					task = Task.Factory.StartNew(CreateTrader);
			}
			task.Wait();
			return Trader;
		}

		private void CreateTrader()
		{
			QuikTerminal terminal = RunTerminalInternal();
			QuikTrader quikTrader;
			WriteLog(ErrorTypes.None, "Создаем шлюз взаимодействия с системой Quik.");
			if (!string.IsNullOrEmpty(DdeServer))
			{
				if (!string.IsNullOrEmpty(DllName))
					quikTrader = new QuikTrader(terminal.DirectoryName, DdeServer, DllName);
				else
					quikTrader = new QuikTrader(terminal.DirectoryName, DdeServer);
			}
			else
			{
				quikTrader = new QuikTrader(terminal.DirectoryName);
			}
			quikTrader.SecuritiesTable.Columns.Add(DdeSecurityColumns.MarginBuy);
			quikTrader.SecuritiesTable.Columns.Add(DdeSecurityColumns.MinPrice);
			quikTrader.SecuritiesTable.Columns.Add(DdeSecurityColumns.MaxPrice);
			quikTrader.ReConnectionSettings.Interval = TimeSpan.FromSeconds(10);
			quikTrader.ReConnectionSettings.WorkingTime = Exchange.Rts.WorkingTime;

			quikTrader.Connected += () =>
			{
				UpdateIsConnected(null);
				if (!quikTrader.IsExportRunning)
				{
					WriteLog(ErrorTypes.None, "Запускаем экспорт данных.");
					quikTrader.StartExport();
					Verify(quikTrader.Terminal);
				}
			};
			quikTrader.ConnectionError += ex => UpdateIsConnected(ex);
			quikTrader.Disconnected += () => UpdateIsConnected(null);

			Trader = quikTrader;
			CandleManager = new CandleManager(quikTrader);

			WriteLog(ErrorTypes.None, "Производим подключение.");
			quikTrader.Connect();
		}

		private void UpdateIsConnected(Exception ex)
		{
			var trader = Trader;
			bool isConnectedNew = trader != null && trader.IsConnected;
			if (isConnected != isConnectedNew)
			{
				isConnected = isConnectedNew;
				WriteLog(ErrorTypes.None, isConnectedNew ? "Соединение подключено." : "Соединение отключено.");
				RaiseIsConnectedChanged();
				if (ex != null) WriteLog(ErrorTypes.Error, ex.Message);
			}
		}

		private void RaiseIsConnectedChanged()
		{
			var handler = IsConnectedChanged;
			if (handler != null) handler();
		}

		public void RunTerminal()
		{
			RunTerminalInternal();
		}

		private QuikTerminal RunTerminalInternal()
		{
			QuikTerminal terminal = QuikTerminal.Get(Path);
			if (!terminal.IsLaunched)
				terminal.Launch();
			if (!terminal.IsConnected)
				terminal.Login(Login, Password);
			return terminal;
		}

		private void Verify(QuikTerminal terminal)
		{
			var errors = terminal.GetTableSettings();
			foreach (var error in errors)
			{
				string message = string.Format("Таблица {0}. {1}", error.Table.Caption, error.Error.Message);
				WriteLog(ErrorTypes.Warning, message);
			}
		}

		public void Dispose()
		{
			if (task != null)
				task.Dispose();
			if (CandleManager != null)
				CandleManager.Dispose();
			if (Trader != null)
				Trader.Dispose();
		}

		#region ILogSource
		public Ecng.Collections.INotifyList<ILogSource> Childs
		{
			get { return null; }
		}

		private Guid id = Guid.NewGuid();
		public Guid Id
		{
			get { return id; }
		}

		public event Action<LogMessage> Log;

		public string Name
		{
			get { return Title; }
		}

		public ILogSource Parent
		{
			get { return null; }
		}

		private void RaiseLog(LogMessage logMessage)
		{
			var handler = Log;
			if (handler != null)
				handler(logMessage);
		}

		private void WriteLog(ErrorTypes errorType, string message)
		{
			LogMessage logMessage = new LogMessage(this, DateTime.Now, errorType, message);
			RaiseLog(logMessage);
		}
		#endregion
	}

Создание других торговых систем оставим читателю в качестве домашнего задания.

PortfolioSelector Реализуем получение портфеля по его имени. Основной момент: ждем, когда будет инициализирована сумма на счете, т.к. она нужна для определения размера позиции.

	public class PortfolioSelector : IPortfolioSelector
	{
		private string title;
		public string Title
		{
			get { return title ?? PortfolioName; }
			set { title = value; }
		}

		public string PortfolioName { get; set; }

		public Portfolio GetPortfolio(ITrader trader)
		{
			Portfolio result = FindPortfolio(trader);
			if (result != null)
				return result;
			ManualResetEvent manualResetEvent = new ManualResetEvent(false);
			Action<IEnumerable<Portfolio>> onPortfoliosChanged = p =>
			{
				if (FindPortfolio(trader) != null)
					manualResetEvent.Set();
			};
			trader.NewPortfolios += onPortfoliosChanged;
			trader.PortfoliosChanged += onPortfoliosChanged;
			manualResetEvent.WaitOne(TimeSpan.FromSeconds(30));
			trader.NewPortfolios -= onPortfoliosChanged;
			trader.PortfoliosChanged -= onPortfoliosChanged;
			result = FindPortfolio(trader);
			return result;
		}

		private Portfolio FindPortfolio(ITrader trader)
		{
			return trader.Portfolios.FirstOrDefault(p => p.Name == PortfolioName && p.BeginAmount.Value != 0);
		}
	}

SecuritySelector Поиск инструмента по его коду. Реализация аналогична PortfolioSelector.

	public class SecuritySelector : ISecuritySelector
	{
		private string title;
		public string Title
		{
			get { return title ?? SecurityCode; }
			set { title = value; }
		}

		public string SecurityCode { get; set; }

		public Security GetSecurity(ITrader trader)
		{
			Security result = FindSecurity(trader);
			if (result != null)
				return result;
			ManualResetEvent manualResetEvent = new ManualResetEvent(false);
			Action<IEnumerable<Security>> onNewSecurities = s =>
			{
				if (FindSecurity(trader) != null)
					manualResetEvent.Set();
			};
			trader.NewSecurities += onNewSecurities;
			manualResetEvent.WaitOne(TimeSpan.FromSeconds(30));
			trader.NewSecurities -= onNewSecurities;
			result = FindSecurity(trader);
			return result;
		}

		private Security FindSecurity(ITrader trader)
		{
			return trader.Securities.FirstOrDefault(s => s.Code == SecurityCode);
		}
	}

MarginVolumeSizer Определение размера позиции по лимиту открытых позиций и гарантийному обеспечению.

	public class MarginVolumeSizer : IVolumeSizer
	{
		public double Ratio { get; set; }
		public decimal MaxCapital { get; set; }

		public MarginVolumeSizer()
		{
			MaxCapital = decimal.MaxValue;
		}

		public int GetVolume(Portfolio portfolio, Security security)
		{
			decimal capital = Math.Min(portfolio.BeginAmount.Value, MaxCapital);
			int quantity = (int)(capital * (decimal)Ratio / security.MarginBuy);
			return quantity;
		}
	}

RegistrySettingsProvider Чтение и запись состояния стратегии из реестра.

	public class RegistrySettingsProvider : ISettingsProvider
	{
		public string SubKey { get; set; }

		public RegistrySettingsProvider()
		{
			SubKey = @"Software\FinDirector";
		}

		public string ReadSetting(string name)
		{
			using (RegistryKey registryKey = Registry.CurrentUser.CreateSubKey(SubKey))
			{
				return (string)registryKey.GetValue(name);
			}
		}

		public void WriteSetting(string name, string value)
		{
			using (RegistryKey registryKey = Registry.CurrentUser.CreateSubKey(SubKey))
			{
				registryKey.SetValue(name, value);
			}
		}
	}

FinamHistoryCandleProvider Получение исторических свечей с сайта финама. Реализацию не выкладываю, т.к. много кода.

	public class FinamHistoryCandleProvider : IHistoryCandleProvider
	{
		public int FinamSecurityCode { get; set; }
		public TimeSpan TimeFrame { get; set; }

		public FinamHistoryCandleProvider()
		{
			FinamSecurityCode = 17455;
			TimeFrame = TimeSpan.FromHours(1);
		}

		public List<TimeFrameCandle> GetHistoryCandles(DateTime beginDate, DateTime endDate)
		{
			…
}

:[:::right Автор статьи — Вадим Чижов

War dieses Thema hilfreich?

Thema teilen

Kommentare (12)

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

Serg
Serg

Спасибо за статьи) Пытаюсь разбираться. Завис на реализации QuikTraderBuilder. Никак не разберусь почему монитор не получает сообщения от QuikTraderBuilder, видимо что-то с реализацией ILogSource.

private void RaiseLog(LogMessage logMessage)
{
  var handler = Log;
  if (handler != null)
    handler(logMessage);   <<-- здесь возникает ошибка Current thread is not a GUI.
}

Буду рад помощи)

FinDirector
FinDirector Autor
LogManager.Listeners.Add(new GuiLogListener(_monitor));
Serg
Serg

FinDirector: ...

Да я вроде так и делаю но почемуто все вообще виснет. Может проблема в версии 4.0.23? Вы какую версию используете?

ps: да и в логфайл сообщения от трейдера также не попадают( Мне кажется что гуи синхронизация здесь непричем. Скорее проблема с реализацией ILogSourse. Хотя могу только догадыватся так как не профи) FinDirector, подскажите, у вас то все работает или всетаки чтото меняли после публикации своих статей?

FinDirector
FinDirector Autor

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

Serg
Serg
Serg

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

serebryakov_a
serebryakov_a

Serg: Даже не знаю в чем точто была проблема но все заработало. FinDirector, с вашего позволения могу выложить проект-шаблон целиком.

Добрый день! Попытался реализовать описанный выше каркас - появились ошибки реализации интерфейсов(ITraderBuilder, ILogSource). версия библиотеки 4.1.6. Можете ли вы поделиться реализацией проекта-шаблона или подсказать в чем там были траблы?

Gunn7
Gunn7

Присоединяюсь к serq, спасибо за статьи. Может все же выложите реализацию получения свечей с финама. Повис на этом уже который день...

FinDirector
FinDirector Autor

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Net;
using FinDirector.Infrastructure;

namespace FinDirector.Modules.SharedServices
{
	public class FinamHistoryCandleProvider : IHistoryCandleProvider
	{
		public int FinamSecurityCode { get; set; }

		[Description("Таймфрейм.")]
		public TimeSpan TimeFrame { get; set; }

		public FinamHistoryCandleProvider()
		{
			FinamSecurityCode = 17455;
			TimeFrame = TimeSpan.FromHours(1);
		}

		public IList<Candle> GetHistoryCandles(DateTime beginDate, DateTime endDate)
		{
			string uri = GetInstrumentUrl(beginDate, endDate);
			WebRequest request = WebRequest.Create(uri);
			using (StreamReader sr = new StreamReader(request.GetResponse().GetResponseStream()))
			{
				IList<Candle> result = new CandleReader().ReadCandles(sr);
				return result;
			}
		}

		private int GetPeriodCode()
		{
			int period = 3;
			if (TimeFrame == TimeSpan.FromMinutes(5))
				period = 3;
			else if (TimeFrame == TimeSpan.FromHours(1))
				period = 7;
			else if (TimeFrame == TimeSpan.FromDays(1))
				period = 8;
			return period;
		}

		private string GetInstrumentUrl(DateTime beginDate, DateTime endDate)
		{
			int period = GetPeriodCode();

			string Ticker = "data";
			string fileName = Ticker + ".txt";
			string url = string.Format(CultureInfo.InvariantCulture,
				"http://195.128.78.52/{0}?d=d&market=14&em={8}&df={2}&mf={3}&yf={4}&dt={5}&mt={6}&yt={7}&p={9}&f={0}&e=.txt&cn={1}&dtf=1&tmf=1&MSOR=0&sep=1&sep2=1&datf=1&at=1",
				fileName, Ticker,
				beginDate.Day, beginDate.Month - 1, beginDate.Year,
				endDate.Day, endDate.Month - 1, endDate.Year,
				FinamSecurityCode, period);
			return url;
		}
	}
}

	class CandleReader
	{
		private bool SkipFirstLine = true;
		private char Separator = ',';

		public IList<Candle> ReadCandles(StreamReader sr)
		{
			List<Candle> result = new List<Candle>();
			if (SkipFirstLine)
			{
				sr.ReadLine();
			}
			while (!sr.EndOfStream)
			{
				string line = sr.ReadLine();
				string[] values = line.Split(new char[] { Separator });
				Candle candle = ReadCandle(values);
				result.Add(candle);
			}
			return result;
		}

		private Candle ReadCandle(string[] line)
		{
			int dateIndex = 2;
			int timeIndex = 3;
			int openIndex = 4;
			int highIndex = 5;
			int lowIndex = 6;
			int closeIndex = 7;

			int date = Int32.Parse(line[dateIndex], CultureInfo.InvariantCulture);
			int time = Int32.Parse(line[timeIndex], CultureInfo.InvariantCulture);

			var result = new Candle()
			{
				DateTime = new DateTime(date / 10000, (date / 100) % 100, date % 100,
					time / 10000, (time / 100) % 100, 0),
				OpenPrice = double.Parse(line[openIndex], CultureInfo.InvariantCulture),
				ClosePrice = double.Parse(line[closeIndex], CultureInfo.InvariantCulture),
				HighPrice = double.Parse(line[highIndex], CultureInfo.InvariantCulture),
				LowPrice = double.Parse(line[lowIndex], CultureInfo.InvariantCulture)
			};

			return result;
		}
	}


Den Serg
Den
Den

FinDirector: ...

Пока, я просто выбираю руками нужный инструмент и слушаю tcpmon'ом запрос браузра и оттуда достаю код.

Подскажите, пожалуйста, как получить список всех доступных кодов инструментов?

Gunn7
Gunn7

Огромное спасибо!

VassilSanych
VassilSanych

Смотрю, многие, в том числе и я, пользовались приведённым кодом, как основой. У многих в цитируемом тексте присутствует WriteLog :) Небольшое замечание: для подключения источника логирования не нужно реализовывать ILogSource. Достаточно унаследоваться от BaseLogReceiver<ВашКласс> и тогда можно замечательно использовать метод AddLog, и методы предоставляемые для него классом LoggingHelper: AddErrorLog, AddInfoLog и т.п. Правда для полного логирования необходимо добавить ещё

public override LogLevels LogLevel
{
	get { return LogLevels.Debug; }
	set { }
} 

по вкусу :)

Геннадий Ванин (Gennady Vanin) kornego
Геннадий Ванин (Gennady Vanin)

VassilSanych: ... по вкусу :) Если этого альтруиста не забанить, то, тогда, прийдётся менять бизнес-модель с бесплатного коммерческого продукта на платный некоммерческий продукт

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

VassilSanych - коварный и лукавый волонтёр-разработчик, который хочет бесплатно поучаствовать в развитии проекта StockSharp, поломав давно установившийся и отлаженный статус-кво!