Пробую реализовывать простые сценарии на S#. Один из таких сценариев - закрытие всех позиций через стратегию котирования (какую именно - не принципиально, пусть будет LimitQuotingStrategy). По этому поводу написано нехитрое тестовое WPF приложение (текст приведен ниже).
Проблема заключается в том, что если раскомментировать строки 314-326 и 329 (т.е. создание стратегии для каждого инструмента и её запуск), то в обработчике события NewMarketDepths вылетает исключение:
An exception of type 'System.NullReferenceException' occurred in WPF stocksharp study.exe but was not handled in user code
Additional information: Ссылка на объект не указывает на экземпляр объекта.
If there is a handler for this exception, the program may be safely continued.
Экспериментально установил, что исключение связано с тем, что идет обращение к d.BestBid.Price (т.е. получение последней лучшей котировки).
Если оставить в приведенном коде всё как есть с комментариями, то стаканы для инструментов, по которым есть открытые позиции, стартуют отлично и в текстовом поле LogWindow цена лучшего бида появляется - т.е. обработчик события NewMarketDepths не падает из-за пустого указателя.
Что я делаю не так? Очевидно, я не учел какую-то очень важную особенность при работе с библиотекой. Но какую???
Версия библиотеки - 4.1.6.
PS: Если кто-то найдет что-либо полезное в моем примере - не возбраняется забрать на заметку с целью дальнейшего использования. :)
Код MainWindow.xaml:
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="WPF_stocksharp_study.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d3="http://research.microsoft.com/DynamicDataDisplay/1.0"
xmlns:Xaml="clr-namespace:StockSharp.Xaml;assembly=StockSharp.Xaml"
Title="WPF_stocksharp_study"
x:Name="mainWindow"
ResizeMode="CanMinimize" Width="500" Height="520"
>
<StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
<Label Content="Путь к QUIK:" HorizontalAlignment="Left" VerticalAlignment="Center" />
<TextBox Name="Path" HorizontalAlignment="Left" VerticalAlignment="Center" Width="350" />
<Button Name="LocateQuikButton" Content="..." HorizontalAlignment="Left" VerticalAlignment="Stretch" Width="35" Margin="10,0,10,0" IsEnabled="True" Click="LocateQuikButton_Click" IsDefault="True" />
</StackPanel>
<StackPanel Margin="5">
<StackPanel Orientation="Horizontal">
<Button Name="ConnectButton" Content="Подключиться" Click="ConnectButton_Click" />
<Button Name="StartWatchSec1Sec2Button" Content="Включить экспорт инструментов" Click="StartWatchSec1Sec2Button_Click" IsEnabled="False" />
<Button Name="ShowDepthsForPositionsButton" Content="Экспорт инструментов в позициях" Click="ShowDepthsForPositionsButton_Click" IsEnabled="False" />
</StackPanel>
<StackPanel Orientation="Horizontal">
</StackPanel>
</StackPanel>
<Grid Margin="5" HorizontalAlignment="Left">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Label Content="Инструмент 1" Grid.Column="0" Grid.Row="0" />
<Label Content="Инструмент 2" Grid.Column="0" Grid.Row="1" />
<ComboBox x:Name="Security1List" SelectionChanged="SecurityList_SelectionChanged" HorizontalAlignment="Stretch" Grid.Column="1" Grid.Row="0" IsEnabled="False" />
<ComboBox x:Name="Security2List" SelectionChanged="SecurityList_SelectionChanged" HorizontalAlignment="Stretch" Grid.Column="1" Grid.Row="1" IsEnabled="False" />
</Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Left" Margin="5">
<Label Content="Позиции:" />
<ListView x:Name="MyPositions" Height="150" ItemsSource="{Binding}" FontSize="10" >
<ListView.View>
<GridView>
<GridViewColumn Width="100" Header="Счет" DisplayMemberBinding="{Binding Path=Portfolio.Name}" />
<GridViewColumn Width="100" Header="Инструмент" DisplayMemberBinding="{Binding Path=Security.Code}" />
<GridViewColumn Width="100" Header="Позиция" DisplayMemberBinding="{Binding Path=CurrentValue}" />
<GridViewColumn Width="100" Header="Заблокировано" DisplayMemberBinding="{Binding Path=BlockedValue}" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
<StackPanel>
<TextBox x:Name="LogWindow" IsReadOnly="True" VerticalScrollBarVisibility="Auto" VerticalContentAlignment="Top" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Height="166" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</Window>
Код MainWindow.xaml.cs
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Controls;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Ecng.Xaml;
using Ecng.ComponentModel;
using Ecng.Collections;
using Ecng.Common;
using System.Linq;
using StockSharp.Algo.Strategies;
using StockSharp.Algo;
using StockSharp.Logging;
using StockSharp.Xaml;
using StockSharp.BusinessEntities;
using StockSharp.Quik;
using MessageBox = System.Windows.MessageBox;
namespace WPF_stocksharp_study
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public QuikTrader _trader;
public ThreadSafeObservableCollection<Position> Positions = new ThreadSafeObservableCollection<Position>();
public static MainWindow Instance { get; private set; }
private bool _isConnected = false, _isDdeStarted = false, _isWatchingStarted = false, _isQuotesRegistered = false, _isPositionsRegistered = false;
private Security sec1, sec2;
private List<Strategy> _strategies = new List<Strategy>();
private LogManager LogManager = new LogManager();
public MainWindow()
{
InitializeComponent();
MainWindow.Instance = this;
this.DataContext = this;
this.Path.Text = QuikTerminal.GetDefaultPath();
MyPositions.DataContext = Positions;
var consoleLogListener = new ConsoleLogListener();
this.LogManager.Listeners.Add(consoleLogListener);
}
#region Routines
void StartDDE()
{
WriteLogMessage("Запускается экспорт DDE");
_trader.StartExport(new[]
{
_trader.SecuritiesTable,
_trader.EquityPositionsTable,
_trader.EquityPortfoliosTable,
_trader.OrdersTable,
_trader.TradesTable,
_trader.MyTradesTable
});
UpdateControls();
}
void StopDDE()
{
WriteLogMessage("Останавливается экспорт DDE");
_trader.StopExport();
UpdateControls();
}
void StartWatch()
{
this._isWatchingStarted = true;
this.sec1 = (Security)Security1List.SelectedItem;
this.sec2 = (Security)Security2List.SelectedItem;
if (sec1 != null)
this._trader.RegisterMarketDepth(this.sec1);
if (sec2 != null)
this._trader.RegisterMarketDepth(this.sec2);
_isQuotesRegistered = true;
UpdateControls();
}
void StopWatch()
{
if (_isQuotesRegistered)
{
this._trader.UnRegisterMarketDepth(this.sec1);
this._trader.UnRegisterMarketDepth(this.sec2);
}
this._isWatchingStarted = false;
UpdateControls();
}
void UpdateControls()
{
ConnectButton.Content = (_isConnected) ? "Отключить" : "Подключить";
_isDdeStarted = _trader.IsExportStarted;
StartWatchSec1Sec2Button.Content = (_isDdeStarted) ? "Выключить экспорт инструментов" : "Включить экспорт инструментов";
StartWatchSec1Sec2Button.IsEnabled = (Security1List.SelectedItem != null) && (Security2List.SelectedItem != null);
Security1List.IsEnabled = Security2List.IsEnabled = _isDdeStarted;
ShowDepthsForPositionsButton.IsEnabled = _isPositionsRegistered;
}
void WriteLogMessage(string message)
{
var _dt = DateTime.Now;
var str = _dt.ToString("yyyy-MM-dd HH:mm:ss.ffff - ") + message + "\n";
LogWindow.AppendText(str);
LogWindow.ScrollToEnd();
}
void CalculateAndUpdateBidAsk()
{
// TODO: Implement CalculateAndUpdateBidAsk
}
#endregion
#region Event handlers
#region Event handlers for UI elements
void ConnectButton_Click(object sender, RoutedEventArgs e)
{
if (!_isConnected)
{
if (this.Path.Text.IsEmpty())
MessageBox.Show(this, "Путь к Quik не выбран");
else
{
WriteLogMessage("Начинаем подключение!");
if (this._trader == null)
{
this._trader = new QuikTrader(this.Path.Text);
this.LogManager.Sources.Add(_trader);
// Подписываемся на событие появления портфелей
this._trader.NewPortfolios += portfolios => this.GuiAsync(() =>
{
WriteLogMessage("Портфели появились!");
});
this._trader.PortfoliosChanged += portfolios => this.GuiAsync(() =>
{
WriteLogMessage("Портфели изменились!");
portfolios.ForEach(p => WriteLogMessage(p.Name));
});
// Подписываемся на событие появления инструментов
this._trader.NewSecurities += securities => this.GuiAsync(() =>
{
Security1List.ItemsSource = this._trader.Securities;
Security2List.ItemsSource = this._trader.Securities;
WriteLogMessage("Инструменты появились!");
});
this._trader.NewMarketDepths += depths => this.GuiAsync(() =>
{
depths.ForEach(d =>
{
WriteLogMessage(string.Format("Появился стакан для инструмента {0}. Бид: {1}", d.Security.Code, d.BestBid.Price));
});
});
// Подписываемся на событие появления моих сделок
this._trader.NewMyTrades += myTrades => this.GuiAsync(() =>
{
foreach (var myTrade in myTrades)
{
var trade = myTrade.Trade;
WriteLogMessage(string.Format("Сделка {0} по цене {1} по бумаге {2} по объему {3} в {4}.", trade.Id, trade.Price, trade.Security.Code, trade.Volume, trade.Time));
}
});
// Подписываемся на событие подлючения
this._trader.Connected += () => this.GuiAsync(() =>
{
_isConnected = true;
WriteLogMessage("QUIK подключен!");
StartDDE();
UpdateControls();
});
//this._trader.NewPositions += positions => this.GuiAsync(() =>
// {
// Positions.Clear();
// Positions.AddRange(_trader.Positions.Where(p => p.CurrentValue != 0));
// });
this._trader.PositionsChanged += positions => this.GuiAsync(() =>
{
Positions.Clear();
Positions.AddRange(_trader.Positions.Where(p => p.CurrentValue != 0));
if (!_isPositionsRegistered)
{
_isPositionsRegistered = true;
UpdateControls();
}
});
// Подписываемся на событие отключения
this._trader.Disconnected += () => this.GuiAsync(() =>
{
_isConnected = false;
WriteLogMessage("QUIK отключен!");
UpdateControls();
});
this._trader.ConnectionError += (f) => this.GuiAsync(() =>
{
WriteLogMessage("Ошибка подключения. " + f.Message);
});
}
// Подключаемся
this._trader.Connect();
}
}
else
{
StopWatch();
StopDDE();
this._trader.Disconnect();
_isConnected = false;
UpdateControls();
}
}
void LocateQuikButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new FolderBrowserDialog();
if (!this.Path.Text.IsEmpty())
dlg.SelectedPath = this.Path.Text;
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
this.Path.Text = dlg.SelectedPath;
}
}
void StartWatchSec1Sec2Button_Click(object sender, RoutedEventArgs e)
{
if (_isWatchingStarted)
StopWatch();
else
StartWatch();
}
void SecurityList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
sec1 = (Security)Security1List.SelectedItem;
sec2 = (Security)Security2List.SelectedItem;
UpdateControls();
}
#endregion
#region Event handlers for application shutdown
protected override void OnClosing(CancelEventArgs e)
{
if (this._trader != null)
{
if (_isDdeStarted)
StopDDE();
if (_isWatchingStarted)
StopWatch();
this._trader.Dispose();
}
base.OnClosing(e);
}
protected override void OnClosed(EventArgs e)
{
System.Windows.Application.Current.Shutdown();
base.OnClosed(e);
}
#endregion
private void ShowDepthsForPositionsButton_Click(object sender, RoutedEventArgs e)
{
_trader.Positions.ForEach(p =>
{
WriteLogMessage("Актив: " + p.Security.Code + " Позиция: " + p.CurrentValue);
var _volume = p.CurrentValue;
if (_volume != 0)
{
WriteLogMessage("Запускается экспорт стакана для" + p.Security.Code);
_trader.RegisterMarketDepth(p.Security);
}
//var _depth = _trader.GetMarketDepth(p.Security).Clone();
//var _close = new LimitQuotingStrategy(_volume > 0 ? OrderDirections.Sell : OrderDirections.Buy, _volume.Abs(), (_depth.BestAsk.Price + _depth.BestBid.Price) / 2)
// {
// Trader = this._trader,
// Security = p.Security,
// Portfolio = p.Portfolio
// };
//this.LogManager.Sources.Add(_close);
//_strategies.Add(_close);
});
//_strategies.ForEach(s => s.Start());
}
#endregion
}
}