Dados históricos
O teste com dados históricos permite tanto a análise de mercado para encontrar padrões como a otimização de parâmetros da estratégia. O trabalho principal é realizado pela classe HistoryEmulationConnector, que obtém os dados armazenados num repositório local através de uma API especial. Parâmetros adicionais são descritos na secção Definições de Teste.
O teste pode ser realizado usando vários tipos de dados de mercado:
- Tick trades (ITickTradeMessage)
- Livros de ordens (IOrderBookMessage)
- Velas de diferentes períodos
- OrderLog
- Level1 (melhores preços bid e ask)
- Combinações de diferentes tipos de dados
Se não existirem livros de ordens guardados para o período de teste, estes podem ser gerados com base nas transações usando MarketDepthGenerator ou reconstruídos a partir do order log usando OrderLogMarketDepthBuilder.
Os dados para testes históricos devem ser descarregados e guardados antecipadamente num formato especial S#. Isto pode ser feito manualmente usando Conectores e a Storage API, ou configurando e executando a aplicação especial Hydra.
Etapas principais do teste histórico
1. Configurar o armazenamento de dados
O primeiro passo é criar um objeto IStorageRegistry através do qual HistoryEmulationConnector irá aceder aos dados históricos:
// armazenamento para aceder a dados históricos
var storageRegistry = new StorageRegistry
{
// definir caminho para o diretório com dados históricos
DefaultDrive = new LocalMarketDataDrive(HistoryPath.Folder)
};
Caution
O construtor LocalMarketDataDrive recebe o caminho para o diretório raiz onde o histórico de todos os instrumentos está armazenado, não para um diretório com um instrumento específico. Por exemplo, se o arquivo HistoryData.zip foi descompactado para o diretório C:\MarketData\AAPL@NASDAQ\, deve passar o caminho C:\MarketData\ para LocalMarketDataDrive. Mais detalhes na secção API.
2. Criar instrumentos e carteiras
// criar instrumento de teste para o teste
var security = new Security
{
Id = SecId.Text, // o ID do instrumento corresponde ao nome da pasta com dados históricos
Code = secCode,
Board = board,
};
// carteira de teste
var portfolio = new Portfolio
{
Name = "test account",
BeginValue = 1000000,
};
3. Criar o conector de emulação
// criar conector para emulação
var connector = new HistoryEmulationConnector(
new[] { security },
new[] { portfolio })
{
EmulationAdapter =
{
Emulator =
{
Settings =
{
// executar a ordem se o preço histórico tocar no preço da nossa ordem limitada
// Por predefinição está desativado; o preço deve atravessar o preço da ordem limitada
// (modo de teste mais rigoroso)
MatchOnTouch = false,
// comissão para transações
CommissionRules = new ICommissionRule[]
{
new CommissionPerTradeRule { Value = 0.01m },
}
}
}
},
UseExternalCandleSource = emulationInfo.UseCandle != null,
CreateDepthFromOrdersLog = emulationInfo.UseOrderLog,
CreateTradesFromOrdersLog = emulationInfo.UseOrderLog,
HistoryMessageAdapter =
{
StorageRegistry = storageRegistry,
// definir intervalo de teste
StartDate = startTime,
StopDate = stopTime,
OrderLogMarketDepthBuilders =
{
{ secId, new ItchOrderLogMarketDepthBuilder(secId) }
}
},
// definir intervalo de atualização da hora de mercado
MarketTimeChangedInterval = timeFrame,
};
4. Subscrever eventos e configurar a geração de dados
Ao ligar, configuramos a receção dos dados necessários dependendo dos parâmetros de teste:
connector.SecurityReceived += (subscr, s) =>
{
if (s != security)
return;
// preencher valores Level1
connector.EmulationAdapter.SendInMessage(level1Info);
// subscrever os dados necessários dependendo das definições de teste
if (emulationInfo.UseMarketDepth)
{
connector.Subscribe(new(DataType.MarketDepth, security));
// se for necessário gerar livros de ordens
if (generateDepths || emulationInfo.UseCandle != null)
{
// se não houver dados históricos de livro de ordens disponíveis, mas forem exigidos pela estratégia,
// usar o gerador baseado nos últimos preços
connector.RegisterMarketDepth(new TrendMarketDepthGenerator(connector.GetSecurityId(security))
{
Interval = TimeSpan.FromSeconds(1), // frequência de atualização do livro de ordens - 1 s
MaxAsksDepth = maxDepth,
MaxBidsDepth = maxDepth,
UseTradeVolume = true,
MaxVolume = maxVolume,
MinSpreadStepCount = 2,
MaxSpreadStepCount = 5,
MaxPriceStepCount = 3
});
}
}
if (emulationInfo.UseOrderLog)
{
connector.Subscribe(new(DataType.OrderLog, security));
}
if (emulationInfo.UseTicks)
{
connector.Subscribe(new(DataType.Ticks, security));
}
if (emulationInfo.UseLevel1)
{
connector.Subscribe(new(DataType.Level1, security));
}
// iniciar a estratégia antes de a emulação começar
strategy.Start();
// iniciar o carregamento de dados históricos
connector.Start();
};
5. Criar e configurar a estratégia
// criar estratégia de negociação baseada em médias móveis com períodos 80 e 10
var strategy = new SmaStrategy
{
LongSma = 80,
ShortSma = 10,
Volume = 1,
Portfolio = portfolio,
Security = security,
Connector = connector,
LogLevel = DebugLogCheckBox.IsChecked == true ? LogLevels.Debug : LogLevels.Info,
// o intervalo predefinido é 1 min, o que é excessivo para um intervalo de vários meses
UnrealizedPnLInterval = ((stopTime - startTime).Ticks / 1000).To<TimeSpan>()
};
// configurar o tipo de dados usado para construir candles
if (emulationInfo.UseCandle != null)
{
strategy.CandleType = emulationInfo.UseCandle;
if (strategy.CandleType != TimeSpan.FromMinutes(1).TimeFrame())
{
strategy.BuildFrom = TimeSpan.FromMinutes(1).TimeFrame();
}
}
else if (emulationInfo.UseTicks)
strategy.BuildFrom = DataType.Ticks;
else if (emulationInfo.UseLevel1)
{
strategy.BuildFrom = DataType.Level1;
strategy.BuildField = emulationInfo.BuildField;
}
else if (emulationInfo.UseOrderLog)
strategy.BuildFrom = DataType.OrderLog;
else if (emulationInfo.UseMarketDepth)
strategy.BuildFrom = DataType.MarketDepth;
6. Visualizar resultados
Para apresentar visualmente os resultados do teste, subscrevemos alterações de P&L e de posição:
var pnlCurve = equity.CreateCurve(LocalizedStrings.PnL + " " + emulationInfo.StrategyName, Colors.Green, Colors.Red, DrawStyles.Area);
var realizedPnLCurve = equity.CreateCurve(LocalizedStrings.PnLRealized + " " + emulationInfo.StrategyName, Colors.Black, DrawStyles.Line);
var unrealizedPnLCurve = equity.CreateCurve(LocalizedStrings.PnLUnreal + " " + emulationInfo.StrategyName, Colors.DarkGray, DrawStyles.Line);
var commissionCurve = equity.CreateCurve(LocalizedStrings.Commission + " " + emulationInfo.StrategyName, Colors.Red, DrawStyles.DashedLine);
strategy.PnLReceived2 += (s, pf, t, r, u, c) =>
{
var data = equity.CreateData();
data
.Group(t)
.Add(pnlCurve, r - (c ?? 0))
.Add(realizedPnLCurve, r)
.Add(unrealizedPnLCurve, u ?? 0)
.Add(commissionCurve, c ?? 0);
equity.Draw(data);
};
var posItems = pos.CreateCurve(emulationInfo.StrategyName, emulationInfo.CurveColor, DrawStyles.Line);
strategy.PositionReceived += (s, p) =>
{
var data = pos.CreateData();
data
.Group(p.LocalTime)
.Add(posItems, p.CurrentValue);
pos.Draw(data);
};
// subscrever atualizações de progresso
connector.ProgressChanged += steps => this.GuiAsync(() => progressBar.Value = steps);
7. Iniciar o teste
// iniciar emulação
connector.Connect();
Implementação moderna de testes históricos
Nas versões mais recentes de S#, o exemplo de teste histórico foi significativamente modernizado e agora permite testar estratégias usando vários tipos de dados de mercado:
- Ticks (transações)
- Livros de ordens
- Velas de diferentes períodos
- Log de ordens
- Dados Level1 (melhores preços)
- Combinações de diferentes tipos de dados
É criado um separador separado com gráficos e estatísticas para cada tipo de dados:
// criar modos de teste
_settings = new[]
{
(
TicksCheckBox,
TicksProgress,
TicksParameterGrid,
// ticks
new EmulationInfo
{
UseTicks = true,
CurveColor = Colors.DarkGreen,
StrategyName = LocalizedStrings.Ticks
},
TicksChart,
TicksEquity,
TicksPosition
),
(
TicksAndDepthsCheckBox,
TicksAndDepthsProgress,
TicksAndDepthsParameterGrid,
// ticks + livros de ordens
new EmulationInfo
{
UseTicks = true,
UseMarketDepth = true,
CurveColor = Colors.Red,
StrategyName = LocalizedStrings.TicksAndDepths
},
TicksAndDepthsChart,
TicksAndDepthsEquity,
TicksAndDepthsPosition
),
// outras combinações de tipos de dados
};
Esta abordagem permite comparar visualmente o desempenho da estratégia ao usar diferentes fontes de dados.
Estratégia SMA melhorada
A estratégia de média móvel (SMA) foi redesenhada e agora usa uma abordagem mais moderna para a subscrição de dados e o processamento de candles:
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// criar subscrição de candles do tipo necessário
var dt = CandleTimeFrame is null
? CandleType
: DataType.Create(CandleType.MessageType, CandleTimeFrame);
var subscription = new Subscription(dt, Security)
{
MarketData =
{
IsFinishedOnly = true,
BuildFrom = BuildFrom,
BuildMode = BuildFrom is null ? MarketDataBuildModes.LoadAndBuild : MarketDataBuildModes.Build,
BuildField = BuildField,
}
};
// criar indicadores
var longSma = new SMA { Length = LongSma };
var shortSma = new SMA { Length = ShortSma };
// subscrever candles e ligá-los aos indicadores
SubscribeCandles(subscription)
.Bind(longSma, shortSma, OnProcess)
.Start();
// configurar apresentação no gráfico
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, shortSma, System.Drawing.Color.Coral);
DrawIndicator(area, longSma);
DrawOwnTrades(area);
}
// configurar proteção da posição
StartProtection(TakeValue, StopValue);
}
O processamento de candles e as decisões de negociação estão agora separados num método dedicado:
private void OnProcess(ICandleMessage candle, decimal longValue, decimal shortValue)
{
LogInfo(LocalizedStrings.SmaNewCandleLog, candle.OpenTime, candle.OpenPrice, candle.HighPrice, candle.LowPrice, candle.ClosePrice, candle.TotalVolume, candle.SecurityId);
// verificar se o candle está concluído
if (candle.State != CandleStates.Finished)
return;
// analisar o cruzamento dos indicadores
var isShortLessThenLong = shortValue < longValue;
if (_isShortLessThenLong == null)
{
_isShortLessThenLong = isShortLessThenLong;
}
else if (_isShortLessThenLong != isShortLessThenLong) // ocorreu cruzamento
{
// se a curta for inferior à longa - vender; caso contrário, comprar
var direction = isShortLessThenLong ? Sides.Sell : Sides.Buy;
// calcular volume para abertura de posição ou reversão
var volume = Position == 0 ? Volume : Position.Abs().Min(Volume) * 2;
// usar o preço de fecho do candle
var price = candle.ClosePrice;
if (direction == Sides.Buy)
BuyLimit(price, volume);
else
SellLimit(price, volume);
_isShortLessThenLong = isShortLessThenLong;
}
}
Definições adicionais de teste
Definições alargadas para testes estão disponíveis em S#, incluindo:
- Geração de livros de ordens com parâmetros especificados
- Definições de comissão
- Definições de slippage de preço
- Emulação de atraso de execução
Estas definições são descritas com mais detalhe na secção Definições de Teste.