Skip to content
67 changes: 67 additions & 0 deletions Brokerages/Bitfinex/BestBidAskUpdatedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;

namespace QuantConnect.Brokerages.Bitfinex
{
/// <summary>
/// Event arguments class for the <see cref="OrderBook.BestBidAskUpdated"/> event
/// </summary>
public sealed class BestBidAskUpdatedEventArgs : EventArgs
{
/// <summary>
/// Gets the new best bid price
/// </summary>
public Symbol Symbol { get; }

/// <summary>
/// Gets the new best bid price
/// </summary>
public decimal BestBidPrice { get; }

/// <summary>
/// Gets the new best bid size
/// </summary>
public decimal BestBidSize { get; }

/// <summary>
/// Gets the new best ask price
/// </summary>
public decimal BestAskPrice { get; }

/// <summary>
/// Gets the new best ask size
/// </summary>
public decimal BestAskSize { get; }

/// <summary>
/// Initializes a new instance of the <see cref="BestBidAskUpdatedEventArgs"/> class
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="bestBidPrice">The newly updated best bid price</param>
/// <param name="bestBidSize">>The newly updated best bid size</param>
/// <param name="bestAskPrice">The newly updated best ask price</param>
/// <param name="bestAskSize">The newly updated best ask size</param>
public BestBidAskUpdatedEventArgs(Symbol symbol, decimal bestBidPrice, decimal bestBidSize, decimal bestAskPrice, decimal bestAskSize)
{
Symbol = symbol;
BestBidPrice = bestBidPrice;
BestBidSize = bestBidSize;
BestAskPrice = bestAskPrice;
BestAskSize = bestAskSize;
}
}
}
204 changes: 202 additions & 2 deletions Brokerages/Bitfinex/BitfinexBrokerage.Messaging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
*/

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand All @@ -30,6 +32,10 @@ public partial class BitfinexBrokerage : BaseWebsocketsBrokerage, IDataQueueHand
/// <summary>
/// Wss message handler
/// </summary>
///
#region Declarations
private readonly ConcurrentDictionary<Symbol, OrderBook> _orderBooks = new ConcurrentDictionary<Symbol, OrderBook>();
#endregion
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void OnMessageImpl(object sender, WebSocketMessage e)
Expand All @@ -49,6 +55,23 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e)
_lastHeartbeatUtcTime = DateTime.UtcNow;
return;
}
else if (ChannelList.ContainsKey(id) && ChannelList[id].Name == "book")
{
if (raw[1].Type == JTokenType.Array)
{
//order book snapshot
var data = raw[1].ToObject(typeof(string[][]));
PopulateOrderBook(data, ChannelList[id].Symbol);
return;
}
else
{
//order book update
var data = raw.ToObject(typeof(string[]));
L2Update(data, ChannelList[id].Symbol);
}

}
else if (ChannelList.ContainsKey(id) && ChannelList[id].Name == "ticker")
{
//ticker
Expand All @@ -72,6 +95,39 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e)
PopulateTrade(term, data);
return;
}
else if (id == "0" && (term == "on" || term == "ou" || term == "oc"))
{
//order updates
Log.Trace("BitfinexBrokerage.OnMessage(): Order update");
var data = raw[2].ToObject(typeof(string[]));
UpdateOrder(term, data);
return;

}
else if (id == "0" && term == "os")
{
//order snapshot
Log.Trace("BitfinexBrokerage.OnMessage(): Order Snapshot");
var data = raw[2].ToObject(typeof(string[][]));
PopulateOrder(data);

}
else if (id == "0" && (term == "pn" || term == "pu" || term == "pc"))
{
//position updates
Log.Trace("BitfinexBrokerage.OnMessage(): Order update");
var data = raw[2].ToObject(typeof(string[]));
return;

}
else if (id == "0" && term == "ps")
{
//position snapshot
Log.Trace("BitfinexBrokerage.OnMessage(): Position Snapshot");
//var data = raw[2].ToObject(typeof(string[][]));
return;

}
else if (term == "ws")
{
//wallet
Expand All @@ -84,7 +140,7 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e)
return;
}
}
else if ((raw.channel == "ticker" || raw.channel == "trades") && raw.@event == "subscribed")
else if ((raw.channel == "ticker" || raw.channel == "trades" || raw.channel == "book") && raw.@event == "subscribed")
{
var channel = (string)raw.channel;
var currentChannelId = (string)raw.chanId;
Expand Down Expand Up @@ -119,7 +175,7 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e)
return;
}

Log.Trace("BitfinexBrokerage.OnMessage(): " + e.Message);
//Log.Trace("BitfinexBrokerage.OnMessage(): " + e.Message);
}
catch (Exception ex)
{
Expand All @@ -128,6 +184,95 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e)
}
}

private void PopulateOrderBook(string[][] data, string symbol)
{
OrderBook orderBook;
if (!_orderBooks.TryGetValue(symbol, out orderBook))
{
orderBook = new OrderBook(symbol);
_orderBooks[symbol] = orderBook;
}
else
{
orderBook.BestBidAskUpdated -= OnBestBidAskUpdated;
orderBook.Clear();
}

foreach (var item in data)
{
var msg = new Messages.OrderBook(item);
// Positive values -> bid
if (msg.Price > 0)
{
orderBook.UpdateAskRow(msg.Price, msg.Amount);
}
// negative values -> ask.
else if (msg.Price < 0)
{
orderBook.UpdateBidRow(msg.Price, msg.Amount);
}

orderBook.BestBidAskUpdated += OnBestBidAskUpdated;
}
return;
}

private void OnBestBidAskUpdated(object sender, BestBidAskUpdatedEventArgs e)
{
EmitQuoteTick(e.Symbol, e.BestBidPrice, e.BestBidSize, e.BestAskPrice, e.BestAskSize);
}

private void L2Update(string[] data, string symbol)
{
var orderBook = _orderBooks[symbol];

var msg = new Messages.L2Update(data);

// Positive values -> bid
if (msg.Price > 0)
{
if (msg.Count == 0)
{
orderBook.RemoveAskRow(msg.Price);
}
else
{
orderBook.UpdateAskRow(msg.Price, msg.Amount);
}
}
// negative values -> ask.
else if (msg.Price < 0)
{
if (msg.Count == 0)
{
orderBook.RemoveBidRow(msg.Price);
}
else
{
orderBook.UpdateBidRow(msg.Price, msg.Amount);
}
}
return;
}

private void EmitQuoteTick(Symbol symbol, decimal bidPrice, decimal bidSize, decimal askPrice, decimal askSize)
{
lock (Ticks)
{
Ticks.Add(new Tick
{
AskPrice = askPrice,
BidPrice = bidPrice,
Value = (askPrice + bidPrice) / 2m,
Time = DateTime.UtcNow,
Symbol = symbol,
TickType = TickType.Quote,
AskSize = askSize,
BidSize = bidSize
});
}
}

private void PopulateTicker(string response, string symbol)
{
var data = JsonConvert.DeserializeObject<string[]>(response, settings);
Expand Down Expand Up @@ -168,6 +313,51 @@ private void PopulateTradeTicker(string response, string symbol)
}
}

private void UpdateOrder(string term, string[] data)
{
var msg = new Messages.OrderUpdate(data);
OrderDirection direction = msg.OrderAmount < 0 ? OrderDirection.Sell : OrderDirection.Buy;
Symbol symbol = Symbol.Create(msg.OrderPair.ToUpper(), SecurityType.Crypto, BrokerageMarket);

Log.Trace(msg.ToString());
// order new
if (term == "on")
{
// var orderEvent = new OrderEvent
//(
// orderId, symbol, DateTime.UtcNow, OrderStatus.New,
// direction, msg.OrderPrice, msg.OrderAmount, 0, "Bitfinex New Order Event"
//);
// OnOrderEvent(orderEvent);
return;
}
// todo - order update
else if (term == "ou")
{
return;
}
// order cancel
else if (term == "oc")
{
// var orderEvent = new OrderEvent
//(
// orderId, symbol, DateTime.UtcNow, OrderStatus.Canceled,
// direction, msg.OrderPrice, msg.OrderAmount, 0, "Bitfinex Cancel Order Event"
//);
//OnOrderEvent(orderEvent);
return;
}
}

private void PopulateOrder(string[][] data)
{
foreach (var item in data)
{
var msg = new Messages.OrderUpdate(item);
Log.Trace(msg.ToString());
}
}

private void PopulateWallet(string[][] data)
{
foreach (var item in data)
Expand Down Expand Up @@ -303,6 +493,16 @@ public void Subscribe(Packets.LiveNodePacket job, IEnumerable<Symbol> symbols)
pair = item.Value
}));

WebSocket.Send(JsonConvert.SerializeObject(new
{
@event = "subscribe",
channel = "book",
pair = item.Value,
prec = "P0", // default P0
freq = "F0", // default F0
length = "5" // default 25
}));

Log.Trace("BitfinexBrokerage.Subscribe(): Sent subcribe for " + item.Value);
}
}
Expand Down
61 changes: 61 additions & 0 deletions Brokerages/Bitfinex/Messages/L2Update.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace QuantConnect.Brokerages.Bitfinex.Messages
{
/// <summary>
/// Ticker message object
/// </summary>
public class L2Update : BaseMessage
{
private const int _channelId = 0;
private const int _price = 1;
private const int _count = 2;
private const int _amount = 3;

/// <summary>
/// L2Update Message constructor
/// </summary>
/// <param name="values"></param>
public L2Update(string[] values)
: base(values)
{
ChannelId = GetInt(_channelId);
Price = TryGetDecimal(_price);
Count = GetInt(_count);
Amount = TryGetDecimal(_amount);
}

/// <summary>
/// Channel Id
/// </summary>
public int ChannelId { get; set; }

/// <summary>
/// Price
/// </summary>
public decimal Price { get; set; }

/// <summary>
/// Count
/// </summary>
public decimal Count { get; set; }

/// <summary>
/// Amount
/// </summary>
public decimal Amount { get; set; }
}
}
Loading