Часть 9 - Проходим АД AdoNeta, или о сохранении параметров в базе данных
Как известно, в базовой версии S#.Shell отсутствует функционал, позволяющий восстановить позиции после перезапуска робота.
В версии от КазайМазай был предложен метод сохранения в xml-файл через набор свойств базовой стратегии (OrdersByTransactionId, SecuritiesByTransactionId, TradesByTransactionId, PortfoliosByTransactionId, ExchangesByTransactionId) типа SettingsStorage.
При этом у меня при всех старании активировать данное сохранение, данный метод не заработал - при активации RestorePositionsOnStart записанные теги оказывались пустыми.
На форуме так же описан метод сохранения ордеров в txt-файл, который судя по простоте работает.
Но все же сохранение в файл при всей простоте восстановления позиций такого важного элемента в созхранении записей как ведение управленческого учета прибылей и убытков от работы робота.
Именно по этой причине целесообразно реализовать функционал сохранения истории торгов в базу данных (MSSQLSRV, MSACCESS, SQLLITE и пр.) - гибкость в последобработке данной информации позволяет автоматизировать ведение учета результатов торгов.
Рассмотрим реализацию сохранения состояния робота в базу данных основе AdoNet.
- Создадим базовый класс 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.
- Для однозначной идентификации сохраненной стратегии используем 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();
- Теперь напишем функции сохранения и загрузки заявок/сделок из 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);
}
- Вызываем из нужных мест функции 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);
- Последний штрих - создаем в параметрах 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 из сохраненных сделок.