증권회사 api 체결 대이터와 호가 대이터의 수신 순서
주식 등이 체결되면 체결 대이터가 만들어지며 호가 대이터는 변한다. 이들은 논리적으로 동시에 일어나므로 한국거래소와 증권회사 등은 멀티뜨레드로 처리할 수 있다. 그러나 실제로 받아 보면 주로 체결 대이터 – 호가 대이터의 순서로 수신된다. 한국거래소가 어떻게 처리를 하는지는 모르나 내가 테스트한 이베스트투자증권은 싱글 뜨레드로 처리하고 있다. 문제는 이렇게 동기 처리를 해도 그 순서대로 클라이언트가 받는 건 아니라는 거다.
서버가 순차적으로 송신한 대이터는 수신되는 경로에 따라 그 순서가 바뀌어 수신될 수 있다. 심지어 보내는 서버는 한 대도 아니므로 서버들의 사정에 따라 애당초 바뀐 순서로 보낼 수도 있다. 대이터가 만들어지는 인터벌이 짧을수록 이런 현상이 일어날 가능성은 크다. 위처럼 거의 동시에 이루어지는 경우가 대표적인 예다. 그렇다면 서버가 송신한 일련의 대이터의 순서가 수신 과정에 뒤바뀌는 경우가 얼마나 될까?
낮 두 시 무렵에 코스피와 코스닥에서 etf 등을 제외한 약 2,500 종목들의 실시간 체결, 호가, 프로그램 매매 대이터를 수신하여 순서대로 60만 건 저장했다. 많은 거 같아도 고작 10분 정도의 분량이다. 이들 가운데 테스트에 이용할 것들은 종목별 프로그램 매매 대이터를 제외한 555,741개다.
매수 1호가가 직전 호가에 비해 변하지 않고 잔량만 줄어든 경우 줄어든 개수와 이 호가 대이터가 수신되기 바로 전과 후의 매도 체결 대이터에 담긴 체결량들을 비교하여 같은지 여부를 확인했다. 같다면 이들은 체결이라는 하나의 사건에 의해 동시에 만들어진 결과일 가능성이 크다. 단 이들은 모두 같은 시간에 만들어진 것들이어야 한다. 체결과 호가 대이터에는 초 단위까지 시간이 기록되어 있다. 하나의 사건으로부터 만들어진 데이터라면 시간은 모두 같아야 한다.
이상은 매도 체결의 경우이고 매수 체결은 반대로 연산했다.
555,741건의 체결과 호가 대이터 가운데 매수/매도 1호가가 변하지 않고 해당 호가의 잔량만 줄어든 호가 대이터는 129,524건이었고 이 대이터 직전에 같은 개수의 체결 대이터가 있었던 경우 즉 체결 – 호가의 바른 순서로 수신된 경우는 86,093건, 직후에 해당 체결이 있었던 경우는 1,054건이었다. 합은 87,147건으로 각각 98.79%와 1.21%였다. 대충 20~30%는 되지 않을까 예상했는데 무척 적은 편이다.
이 결과가 정밀한 건 아니다. 해당 호가의 바로 앞뒤 체결 사이에 다른 체결이나 호가 패킷이 끼어들거나 호가 잔량은 100이 줄었지만 체결 데이터는 1, 1, 1 … 등으로 나뉘어 올 수도 있기 때문이다. 증권회사는 만들어진 모든 대이터를 그대로 보내는 게 아니라 필요에 따라 적당히 필터링을 해서 모아 보내기도 해서 그렇다. 이 경우는 조건 만족 자체에 영향을 줄 뿐 순서가 뒤바뀌는 것에는 상관이 없다. 따라서 위 결론에 특별한 영향을 주지는 않는다.
int _FillQuoteCount = default;
int _MatchCountPrevious = default;
int _MatchCountNext = default;
int _QuoteVolumeDecreasedCount = default;
for (int i = 0; i < Blocks1.Objects.Count; i++)
{
if (Blocks1.Objects[i] is QuoteBlock _QuoteBlock)
{
if (Symbols.TryGetValue(_QuoteBlock.Code, out Symbol _Symbol))
{
_Symbol.Objects.Add(_QuoteBlock);
}
_FillQuoteCount++;
}
else if (Blocks1.Objects[i] is FillBlock _FillBlock)
{
if (Symbols.TryGetValue(_FillBlock.Code, out Symbol _Symbol))
{
_Symbol.Objects.Add(_FillBlock);
}
_FillQuoteCount++;
}
}
foreach (KeyValuePair<string, Symbol> item in Symbols)
{
for (int i = 1; i < item.Value.Objects.Count - 1; i++)
{
if (item.Value.Objects[i] is QuoteBlock _QuoteBlock)
{
if (_QuoteBlock.BuyPrice == item.Value.BuyQuotePrice && _QuoteBlock.BuyVolume < item.Value.BuyQuoteVolume)
{
_QuoteVolumeDecreasedCount++;
if (item.Value.Objects[i - 1] is FillBlock _FillBlockPrevious && !_FillBlockPrevious.Type && _QuoteBlock.Time == _FillBlockPrevious.Time && item.Value.BuyQuoteVolume - _QuoteBlock.BuyVolume == _FillBlockPrevious.Volume)
{
_MatchCountPrevious++;
}
else if (item.Value.Objects[i + 1] is FillBlock _FillBlockNext && !_FillBlockNext.Type && _QuoteBlock.Time == _FillBlockNext.Time && item.Value.BuyQuoteVolume - _QuoteBlock.BuyVolume == _FillBlockNext.Volume)
{
_MatchCountNext++;
}
}
else if (_QuoteBlock.SellPrice == item.Value.SellQuotePrice && _QuoteBlock.SellVolume < item.Value.SellQuoteVolume)
{
_QuoteVolumeDecreasedCount++;
if (item.Value.Objects[i - 1] is FillBlock _FillBlockPrevious && _FillBlockPrevious.Type && _QuoteBlock.Time == _FillBlockPrevious.Time && item.Value.SellQuoteVolume - _QuoteBlock.SellVolume == _FillBlockPrevious.Volume)
{
_MatchCountPrevious++;
}
else if (item.Value.Objects[i + 1] is FillBlock _FillBlockNext && _FillBlockNext.Type && _QuoteBlock.Time == _FillBlockNext.Time && item.Value.SellQuoteVolume - _QuoteBlock.SellVolume == _FillBlockNext.Volume)
{
_MatchCountNext++;
}
}
item.Value.BuyQuotePrice = _QuoteBlock.BuyPrice;
item.Value.BuyQuoteVolume = _QuoteBlock.BuyVolume;
item.Value.SellQuotePrice = _QuoteBlock.SellPrice;
item.Value.SellQuoteVolume = _QuoteBlock.SellVolume;
}
}
}
// fills & quotes count 555,741
// volume decreased quotes count 129,524
// previous fill matches count 86,093
// next fill matches count 1,054
// total matches count 87,147