Mikhail Sukhov
|
Date: 3/11/2014
|
|
Thanks:
|
|
|
|
|
JaguarFX
|
Date: 4/14/2014
|
|
|
|
Часть 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). Данный класс сразу делаем общий, так как он в дальнейшем может быть использован в пирамидальных стратегиях любой сложности. 1) Создаем класс TradeManager и его инициализатор: Code
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>();
}
...
}
2) Для создания Плана торговли объявляем дополнительный класс PlanElem, каждый экземпляр которого будет содержать информацию об одной торговой линии: Code
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; }
}
Так же создаем класс для быстрой параметризации Плана торговли при его первичном создании: Code
// Параметры плана торговли
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 для создания План торговли по указанным параметрам: Code
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));
}
3) Создаем процедуры для ведения Торговой книги: а) в первую очередь делаем процедуру первичной регистрации RegisterPosition, которая будет срабатывать сразу полсе появления заявки: Code
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 заявки на продажу. Code
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;
}
}
}
|
|
|
|
|
JaguarFX
|
Date: 4/14/2014
|
|
|
|
4) далее создаем торговую стратегию TLSStrategy, в которой основными свойствами будут Code
---
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(@"Количество уровней")]
В самой стратегии реализуем несколько открытых свойств для связи с интерфейсом: Code
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;
И далее создаем три основных процедуры, которые будут обеспечивать ее работу: а) процедуру инициации, основное предназначение которой - создать План торгов до старта стратегии Code
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;
}
б) процедуру запуска, в которой устанавливаем основные вызовы процедур обработки свечей и ведения Торговой книги Code
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);
}
в) и конечно же процедуру обработки свечей, в которой реализуем основную логику создания заявок на покупку/продажу актива: Code
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);
}
}
}
5) очевидно, что капитала может не хватить в процессе резкого падения рынка, как это было при присоединении Крыма, поэтому а) создаем свойства стратегии для хранения размера использованного капитала и % Code
private decimal _longCapt = 0;
[Browsable(false)]
[DisplayName(@"longCapt")]
[Description(@"Использованный капитал")]
---
private decimal _longPcnt = 0;
[Browsable(false)]
[DisplayName(@"longPcnt")]
[Description(@"Процент использованния капитала")]
б) создаем в классе TradeManager счетчик CalcCapReq для подсчета использованного капитала: Code
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 Code
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!")
|
|
Thanks:
|
|
|
|
|
JaguarFX
|
Date: 4/14/2014
|
|
|
|
Итак в целом миграция завершена. Нужно признать что кода в StockSharp получилось гораздо больше, чем Tradematic - раза в три больше. Но тут как раз и пора показать основное преимущество StockSharp - открытость кода для создания пользователького интерфейса управления работающей стратегией. 6) Создаем элементы для визуализации кастомарных свойств новой стратегии а) вначале создаем StrategyPlanDG для привязки Плана торговли Code
<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 + Code
<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 для визуализации торговой книги Code
<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 со начальным скрытым состоянием: Code
<TabItem x:Name="TabBook" Header="Book" Visibility="Hidden">
<robot:StrategyBook x:Name="myMEQ" MinHeight="200" Height="Auto" x:FieldModifier="public"/>
</TabItem>
Как результат - получаем готовый интерфейс для управления работой стратегии в процессе ее выполнения. Это именно то, что никак невозможно сделать в Tradematic в силу закрытости кода.
|
|
Thanks:
|
|
|
|
|
JaguarFX
|
Date: 6/15/2014
|
|
|
|
Часть 8 - Универсализация интерфейсов исполнения стратегий Сделаем еще один шаг к раз0витию S#.Shell - проведем универсализацию интерфейсов исполнения. Как известно в стандартной версии S#.Shell тестовые стратегии выполняются на панеле (документе), где присутствуют графики и где можно добавить собственные панели (см. Часть 5), а исполнение на живом подключении проводится на панеле с сокращенным набором элементов. Это не всегда удобно. Да и для новых стратегий тоже важно в первые месяцы их жизни следить за их состоянием с использованием графических элементов. Для решения этой задачи вначале разбираемся как в В S#.Shell создаются панели. Панель это на самом деле объект типа LayoutDocument из библиотеки AvalonDock, в котором можно задавать различный контент путем присвоения свойства Content типа object. При этом в свойство Content записывается пользовательский элемент управления типа UserControl, графические элементы которого и определяют расположение и набор элементов. Исполняемая стратегия у данного элемент управления это всего лишь свойство типа BaseShellStrategy, которое очевидно может выполняться в любом режиме - как тестовом, так и боевом. С учетом этого для получения возможности запускать стратегии с любым типом панели делаем следующие вещи: 1. Создаем в свойствах стратегии BaseShellStrategyPropeties признак , который указываем на каком виде документа выполняется стратегия: Code
private enDocType _doctype = enDocType.TestDoc;
[Category(@"Основные")]
[DisplayName(@"Тип документа")]
[Description(@"Тип документа стратегии")]
[PropertyOrder(2)]
public enDocType DocType
{
get { return _doctype; }
set
{
_doctype = value;
OnPropertyChanged("DocType");
}
}
При этом enDocType задаем как перечисляемый тип Code
public enum enDocType
{
TestDoc,
ExecDoc
}
2. В классе MainWindow во всех процедурах типа ExecutedAddХХХХStrategy создания исполняемых стратегий, в которых мы хотим получить выбор типа документа исполнения, задаем возможность указать желаемый вид документа: Code
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;
3. Далее классе MainWindow в связанных процедурах создания стратегий модифицируем создание LayoutDocument, используя указанный признак для присвоения желаемого типа документа Code
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;
4. Из процедуры StartTesingStrategy выводим весь код по инициализации графических панелей в отдельную процедуру InitializePanes, которую затем безу3словно вызываем из StartTesingStrategy и условино вызываем из процедуры StartStrategy: Code
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 и вызывввается в потокобезопасном режиме. И вот все готово!
|
|
Thanks:
|
|
|
|
|
JaguarFX
|
Date: 7/15/2014
|
|
|
|
Часть 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) Code
private StorageEngine()
{
Name = "StorageEngine";
IsVerified = false;
ShellConfigDB = SettingsEngine.Instance.Properties.ShellConfigDB;
curStor = SettingsEngine.Instance.Properties.DataStorIn;
}
б) переменные для хранения универсальных команд создания базовых таблиц (tblOrders - таблица заявок, tblTrades - таблица сделок), добавления данных и удаления данных из этих таблиц Code
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- специфичные объекты, и не являются универсальными обертками, но принцип ясен Code
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, которая будет вызываться при первичном обращении к классу для проверки готовности БД к приему данных, если БД не готова - отсутствуют таблицы, то автоматом создаются новые структуры: Code
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. 2. Для однозначной идентификации сохраненной стратегии используем Strategy.Id - свойство типа GUID, которое генерится автоматически при создании новой стратегии и в силу алгоритма генерации GUID позволяет фактически уникально определить стратегию. а) в свойства добавляем поле StrategyID для сохранения GUID в текстовом виде Code
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: Code
if (properties.StrategyID != null)
strategy.Id = new Guid(properties.StrategyID);
else
properties.StrategyID = strategy.Id.ToString();
3. Теперь напишем функции сохранения и загрузки заявок/сделок из MSSQLSRV, разместив их в классе SettingsEngine а) функция SaveToDB(Strategy str) принимает на вход стратегию, сделки и заявки которой необходимо сохранить Code
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) принимает на вход стратегию, сделки и заявки которой необходимо загрузить Code
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 использовать как для тестовых прогонов, так и для реального запуска робота. Code
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);
}
4. Вызываем из нужных мест функции SaveToDB/LoadFromDB а) запиливаем вызов SaveToDB в функцию SaveStrategies, проверяя при этом наличие в параметрах стратегии установленного свойства RestorePositionsOnStart Code
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: Code
if (strategy.Params.RestorePositionsOnStart) SettingsEngine.Instance.LoadFromDB(strategy);
5. Последний штрих - создаем в параметрах S#.Shell (класс SettingsProperties) свойства для сохранения переменных curStor - вид базы данных, ShellConfigDB - название каталога MSSQLSRV (файла для MSACCESS и SQLLITE) Code
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 из сохраненных сделок.
|
|
Thanks:
|
|
|
|
|
JaguarFX
|
Date: 10/5/2014
|
|
|
|
Часть 10 - Обеспечение стабильной работы роботаИтак после всех мероприятий робот работает стабильно, торгует и восстанавливает при сбоях позиции - и самое главное - зарабатывает деньги. Но только КОГДА работает! И тут возникают две дополнительные проблемы с обеспечением стабильности работы. 1. Автоматический запуск и начало работы Торги на ММВБ начинаются в 10 и завершаются в 23:45, соответственно ставим задачу на автозапуск ПК в 09:55 и погружаем ПК на ночь в соню. И тут утром обнаруживается, что по истечении 5 минут работы система автоматически переходит в режим сна! При этом настройки питания - "высокая производительность", т.е. никаких отключений по времени. Начинаем разбираться и выясняем, что при использовании SSD-накопителей "умные" драйверы Windows8 мониторят действия пользователя (мыши, клавиатуры) и при их отсутствии все равно усыпляют ПК. Отключить это невозможно. Итак - лечим это через С#. 1) Создаем класс MouseOperations, в который выносим основные функции эмуляции управления мышью: - SetCursorPosition, - GetCursorPosition, - MouseEvent. Code
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 не дет нужного эффекта - это все го лишь функции отрисовки изображения курсора, а нам нужны функции эмуляции работы мыши. 2) в этом же классе создаем функцию отрисовки движения мыши по экрану: Code
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);
}
}
}
3) вызываем функцию из какого-либо переодического события, например из функции OnTimeChanged(), по которой робот проверяет время работы стратегии Code
var isContain = Helper.Contains(strategy.Params.Schedule, time);
if (isContain) MouseOperations.BresenhamCircle(200, 200, 200);
И вот каждые 30сек мы получаем маленькое и практически незаметное для пользователя подергивание мышки, которое однако выполняет свою основную функцию - предотвращает погружение ПК в преждевременный сон. 2. Женский пол. Это у меня оказалось второй проблемой, так как приходя домой раньше меня девушка каждый раз "нечаянно" выключала работа)) И хотя вечерняя сессия не такая бурная по событиям как дневная, но все я стал фиксить упущенные из-за этого прибыльные сделки. И тут мы открываем для себя мир ситемного треша, в смысле трея)) 1) устанавливаем NU-get пакет Hardcodet.NotifyIcon.Wpf, который обеспечивает работы с иконками в системной трее. 2) создаем пользовательский словарь ресурсов SysTrayResources.xaml, в котором создаем TaskbarIcon - объект иконки с системной трее, и его контекстное меню Code
<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. 2) данный словарь необходимо вызвать из App.xaml, что объект отрисовался сразу после запуска программы Code
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Service\SysTrayResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
3) далее создаем класс SysTrayViewModel, в котором размешаем команды скрытия/отображения робота: Code
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 - это класс-делегат для ускоренного создания выполняемых команд Code
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; }
}
}
Итак - контекстное меню прекрасно работает. Но этого мало)) 4) Создаем в настройках свойство Starthide, и помещаем в процедуру MainWindow() простую проверку на необходимость сразу скрыть робота после старта: Code
if (SettingsEngine.Instance.Properties.Starthide)
Application.Current.MainWindow.Visibility = Visibility.Hidden;
5) Ставим для отображения робота ПАРОЛЬ - как же без него! Для этого создаем маленькую пользовательскую форму UIpassword, в которой размещает поле для вода текста типа PasswordBox Code
<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 прописываем проверку заданного нами пароля и если все ок - то отображаем основное окно робота Code
private void PasswordChangedHandler(object sender, RoutedEventArgs e)
{
//Debug.WriteLine(pwdBox.Password);
if (pwdBox.Password == "ххххххх")
{
Application.Current.MainWindow.Visibility = Visibility.Visible;
}
}
И после этого в коде класса SysTrayViewModel вместо мгновенного открытия приложения прописываем старт формы UIpassword Code
CommandAction = () =>
{
if (Application.Current.MainWindow == null) Application.Current.MainWindow = new MainWindow();
var newWnd = new UIpassword();
newWnd.ShowDialog();
}
|
|
|
|