From e675cb243acaacf9db537f53ac1c64faadee3b35 Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Thu, 23 Nov 2017 13:03:36 +0100 Subject: [PATCH] WIP --- Balance.cs | 19 ++ Currency.cs | 11 ++ HistoricTrade.cs | 35 ++++ Market.cs | 31 ++++ Order.cs | 27 ++- OrderBook.cs | 106 ++++++++++- OrderState.cs | 8 + OrderTarget.cs | 8 + OrderType.cs | 8 + OrderbookVolume.cs | 24 +++ Tick.cs | 20 ++ TradingConnection.cs | 51 +++++- TradingEnvironment.cs | 31 ++++ VolumeRate.cs | 15 +- bittrex/BittrexBalance.cs | 17 ++ bittrex/BittrexConnector.cs | 315 ++++++++++++++++++++++++++++++++ bittrex/BittrexCurrency.cs | 18 ++ bittrex/BittrexException.cs | 17 ++ bittrex/BittrexHistoricTrade.cs | 26 +++ bittrex/BittrexMarket.cs | 75 ++++++++ bittrex/BittrexOrder.cs | 108 +++++++++++ bittrex/BittrexOrderbook.cs | 38 ++++ bittrex/BittrexTick.cs | 17 ++ bittrex/JSONMarket.cs | 19 ++ cache/HistoryCache.cs | 160 ++++++++++++++++ cache/HistoryService.cs | 41 +++++ cache/WeightedAverage.cs | 34 ++++ services/ITradingService.cs | 12 ++ services/ServiceManager.cs | 182 ++++++++++++++++++ sharp.trading.csproj | 42 ++++- 30 files changed, 1502 insertions(+), 13 deletions(-) create mode 100644 Balance.cs create mode 100644 Currency.cs create mode 100644 HistoricTrade.cs create mode 100644 Market.cs create mode 100644 OrderState.cs create mode 100644 OrderTarget.cs create mode 100644 OrderType.cs create mode 100644 OrderbookVolume.cs create mode 100644 Tick.cs create mode 100644 TradingEnvironment.cs create mode 100644 bittrex/BittrexBalance.cs create mode 100644 bittrex/BittrexConnector.cs create mode 100644 bittrex/BittrexCurrency.cs create mode 100644 bittrex/BittrexException.cs create mode 100644 bittrex/BittrexHistoricTrade.cs create mode 100644 bittrex/BittrexMarket.cs create mode 100644 bittrex/BittrexOrder.cs create mode 100644 bittrex/BittrexOrderbook.cs create mode 100644 bittrex/BittrexTick.cs create mode 100644 bittrex/JSONMarket.cs create mode 100644 cache/HistoryCache.cs create mode 100644 cache/HistoryService.cs create mode 100644 cache/WeightedAverage.cs create mode 100644 services/ITradingService.cs create mode 100644 services/ServiceManager.cs diff --git a/Balance.cs b/Balance.cs new file mode 100644 index 0000000..ee618f8 --- /dev/null +++ b/Balance.cs @@ -0,0 +1,19 @@ +using System; +namespace sharp.trading +{ + public class Balance : MarshalByRefObject + { + public virtual string Currency { get; set; } + public virtual Double CurrentBalance { get; set; } + public virtual Double AvailableBalance { get; set; } + public virtual Double PendingBalance { get; set; } + public virtual string WalletAddress { get; set; } + + public override string ToString() + { + return string.Format("[Balance: Currency={0}, CurrentBalance={1}, AvailableBalance={2}, PendingBalance={3}, WalletAddress={4}]", Currency, CurrentBalance, AvailableBalance, PendingBalance, WalletAddress); + } + + + } +} diff --git a/Currency.cs b/Currency.cs new file mode 100644 index 0000000..fda6799 --- /dev/null +++ b/Currency.cs @@ -0,0 +1,11 @@ +using System; +namespace sharp.trading +{ + public class Currency : MarshalByRefObject + { + public String Symbol { get; protected set; } + public String Name { get; protected set; } + public double WithdrawalFee { get; protected set; } + public string BaseAddress { get; protected set; } + } +} diff --git a/HistoricTrade.cs b/HistoricTrade.cs new file mode 100644 index 0000000..0c72269 --- /dev/null +++ b/HistoricTrade.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +namespace sharp.trading +{ + public class HistoricTrade : MarshalByRefObject, IComparable + { + public Int64 UniqueID; + public DateTime TimeStamp; + + public Double Volume; + public Double Price; + public Double TotalPrice; + + public OrderType OrderType; + + public HistoricTrade() + { + } + + public override string ToString() + { + return string.Format("[HistoricTrade: UniqueID={0}, TimeStamp={1}, Volume={2,14:#0.00000000}, Price={3,14:#0.00000000}, TotalPrice={4,14:#0.00000000}, OrderType={5}]", UniqueID, TimeStamp, Volume, Price, TotalPrice, OrderType); + } + + public override bool Equals(object obj) + { + return this.UniqueID.Equals(((HistoricTrade)obj).UniqueID); + } + + public int CompareTo(object obj) + { + return -this.UniqueID.CompareTo(((HistoricTrade)obj).UniqueID); + } + } +} diff --git a/Market.cs b/Market.cs new file mode 100644 index 0000000..789a3ec --- /dev/null +++ b/Market.cs @@ -0,0 +1,31 @@ +using System; +using System.Net.WebSockets; +namespace sharp.trading +{ + public abstract class Market : MarshalByRefObject + { + public string TradedSymbol { get; private set; } + public string PayingSymbol { get; private set; } + + public TradingConnection Connection { get; private set; } + + public double MinimumTradeVolume { get; protected set; } + + public Market(TradingConnection c,string tradedSymbol,string payingSymbol) + { + this.Connection = c; + this.PayingSymbol = payingSymbol; + this.TradedSymbol = tradedSymbol; + } + + public abstract OrderBook getOrderBook(); + public abstract Order[] getOrders(); + + public abstract void Close(); + + public abstract HistoricTrade[] getHistoricTrades(); + + public abstract Tick[] getTicks(); + + } +} diff --git a/Order.cs b/Order.cs index aea7ccc..0540ea9 100644 --- a/Order.cs +++ b/Order.cs @@ -1,12 +1,31 @@ using System; +using sharp.json.attributes; namespace sharp.trading { - public class Order + [JSONClassPolicy( Policy = JSONPolicy.PUBLIC)] + public class Order : MarshalByRefObject { - public Double OrderVolume { get; set; } - public Double FilledVolume { get; set; } - public Double LimitPrice { get; set; } + public OrderType OrderType { get; set; } + public OrderTarget OrderTarget { get; set; } + public OrderState OrderState { get; set; } + public virtual Double OrderVolume { get; set; } + public virtual Double FilledVolume { get; set; } + public virtual Double LimitPrice { get; set; } + public virtual Double PayedPrice { get; set; } + public virtual Double PayedFees { get; set; } + + public bool IsOpen { get; set; } = true; + + public string OrderID { get; set; } + + public override bool Equals(object obj) + { + if (obj == null){ + return false; + } + return OrderID.Equals(((Order)obj).OrderID); + } } } diff --git a/OrderBook.cs b/OrderBook.cs index 397fde7..9e4ef48 100644 --- a/OrderBook.cs +++ b/OrderBook.cs @@ -1,13 +1,111 @@ using System; +using System.ComponentModel; namespace sharp.trading { - public class OrderBook + public abstract class OrderBook : MarshalByRefObject { - public VolumeRate[] Bids { get; set; } - public VolumeRate[] Asks { get; set; } + public VolumeRate[] Bids { get; protected set; } + public VolumeRate[] Asks { get; protected set; } - public OrderBook() + public Market Market { get; private set; } + + public OrderBook(Market market) { + Market = market; } + + public abstract void Refresh(); + + public string MarketName { get; private set; } + public string MarketCurrency { get; private set; } + public string BaseCurrency { get; private set; } + + public double CurrentBid + { + get { return this.Bids[this.Bids.Length - 1].Price; } + } + public double CurrentAsk + { + get { return this.Asks[0].Price; } + } + + + public Tuple getVolumePrice(double volume){ + double volbuy = volume; + double volsell = volume; + + double pricebuy = 0; + double pricesell = 0; + + for (int n = 0; ;n++){ + double avail; + + if (volbuy > 0) + { + avail = volbuy < Asks[n].Volume ? volbuy : Asks[n].Volume; + pricebuy += avail * Asks[n].Price; + volbuy -= avail; + } + if (volsell > 0) + { + avail = volsell < Bids[Bids.Length - 1 - n].Volume ? volsell : Bids[Bids.Length - 1 - n].Volume; + pricesell += avail * Bids[Bids.Length - 1 - n].Price; + volsell -= avail; + } + if ((volbuy <= 0) && (volsell <= 0)) + { + break; + } + } + + return new Tuple(pricebuy, pricesell); + } + + public OrderbookVolume[] calculateVolumes(double[] volumes) + { + OrderbookVolume[] ovl = new OrderbookVolume[volumes.Length]; + int bidend = Bids.Length - 1; + + for (int vn = 0; vn < volumes.Length; vn++){ + ovl[vn] = new OrderbookVolume(); + } + + for (int n = 0; ; n++) + { + bool done = true; + + for (int vn = 0; vn < volumes.Length; vn++) + { + double toBuy = volumes[vn] - ovl[vn].VolumeBuy; + double toSell = volumes[vn] - ovl[vn].VolumeSell; + + if ((toBuy <= 0) && (toSell <= 0)){ + continue; + } + + if (toBuy > Asks[n].Volume){ + toBuy = Asks[n].Volume; + } + if (toSell > Bids[bidend - n].Volume){ + toSell = Bids[bidend - n].Volume; + } + + ovl[vn].PriceBuy += Asks[n].Price * toBuy; + ovl[vn].VolumeBuy += toBuy; + + ovl[vn].PriceSell += Bids[bidend - n].Price * toSell; + ovl[vn].VolumeSell += toSell; + + done = false; + } + + if (done){ + break; + } + } + + return ovl; + } + } } diff --git a/OrderState.cs b/OrderState.cs new file mode 100644 index 0000000..b7a9bb8 --- /dev/null +++ b/OrderState.cs @@ -0,0 +1,8 @@ +using System; +namespace sharp.trading +{ + public enum OrderState + { + INVALID, UNTOUCHED, PARTLYFILLED, FILLED, CANCELED + } +} diff --git a/OrderTarget.cs b/OrderTarget.cs new file mode 100644 index 0000000..650f14f --- /dev/null +++ b/OrderTarget.cs @@ -0,0 +1,8 @@ +using System; +namespace sharp.trading +{ + public enum OrderTarget + { + UNKNOWN, MARKET, LIMIT + } +} diff --git a/OrderType.cs b/OrderType.cs new file mode 100644 index 0000000..5e9093a --- /dev/null +++ b/OrderType.cs @@ -0,0 +1,8 @@ +using System; +namespace sharp.trading +{ + public enum OrderType + { + BUY, SELL, UNKOWN + } +} diff --git a/OrderbookVolume.cs b/OrderbookVolume.cs new file mode 100644 index 0000000..ddd2a03 --- /dev/null +++ b/OrderbookVolume.cs @@ -0,0 +1,24 @@ +using System; +namespace sharp.trading +{ + public class OrderbookVolume : MarshalByRefObject + { + public double VolumeBuy; + public double PriceBuy; + public double VolumeSell; + public double PriceSell; + + public double AverageUnitPriceBuy { + get { return PriceBuy / VolumeBuy; } + } + + public double AverageUnitPriceSell + { + get { return PriceSell / VolumeSell; } + } + + public OrderbookVolume() + { + } + } +} diff --git a/Tick.cs b/Tick.cs new file mode 100644 index 0000000..8ac7005 --- /dev/null +++ b/Tick.cs @@ -0,0 +1,20 @@ +using System; +namespace sharp.trading +{ + public class Tick : MarshalByRefObject + { + public DateTime TimeStamp; + + public double OpenAt; + public double CloseAt; + + public double High; + public double Low; + + public double BaseVolume; + + public Tick() + { + } + } +} diff --git a/TradingConnection.cs b/TradingConnection.cs index 86f1a38..df0a739 100644 --- a/TradingConnection.cs +++ b/TradingConnection.cs @@ -1,15 +1,60 @@ using System; +using sharp.extensions; +using System.CodeDom; +using System.Collections.Generic; namespace sharp.trading { - public abstract class TradingConnection + public abstract class TradingConnection : MarshalByRefObject { - public TradingConnection() + public String UniqueProviderName { get; private set; } + + public TradingConnection(string providerID) { + UniqueProviderName = providerID; } - public abstract OrderBook getOrderBook(); + public abstract object[] getConstructorArguments(); + public abstract Currency[] getCurrencies(); + public abstract Balance[] getBalances(); + public abstract Market openMarket(string tradedSymbol, string payingSymbol); + public abstract Pair[] + listMarkets(); + public abstract Order[] getOpenOrders(); + public abstract Order[] getHistoricOrders(); + + public abstract Order createOrder(OrderType orderType, OrderTarget orderTarget, string marketSymbol, string baseSymbol, double amount, double price); + public abstract Order getOrder(string orderID); + public abstract void cancelOrder(string orderID); + public abstract void refreshOrder(Order order); + + public Currency getCurrency(string symbol){ + foreach (Currency c in getCurrencies()){ + if (c.Symbol.Equals(symbol)){ + return c; + } + } + throw new NotSupportedException(String.Format("Currency '{0}' not supported by Bittrex",symbol)); + } + + public Market openMarket(Pair pair){ + return openMarket(pair.Item2, pair.Item1); + } + + public Pair[] getMarketsForSymbol(string symbol){ + List> r = new List>(); + foreach (Pair p in listMarkets()){ + if (p.Item1.Equals(symbol) || p.Item2.Equals(symbol)){ + r.Add(p); + } + } + return r.ToArray(); + } + + public void cancelOrder(Order order){ + cancelOrder(order.OrderID); + } } } diff --git a/TradingEnvironment.cs b/TradingEnvironment.cs new file mode 100644 index 0000000..8766a63 --- /dev/null +++ b/TradingEnvironment.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using sharp.extensions; +namespace sharp.trading +{ + public static class TradingEnvironment + { + private static string _datadirectory = initialDataDirectory(); + public static string DataDirectory { + get { return _datadirectory; } + set { + _datadirectory = value; + DirectoryExtensions.EnsureDirectoryExists(_datadirectory); + } + } + + public static TradingConnection DefaultConnection { get; set; } + + + + + + private static string initialDataDirectory(){ + string p = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),".sharp.trading"); + DirectoryExtensions.EnsureDirectoryExists(p); + return p; + } + + } +} diff --git a/VolumeRate.cs b/VolumeRate.cs index 1c8e644..6040846 100644 --- a/VolumeRate.cs +++ b/VolumeRate.cs @@ -1,9 +1,14 @@ using System; +using sharp.json; +using sharp.json.attributes; namespace sharp.trading { - public struct VolumeRate + [JSONClassPolicy(Policy = JSONPolicy.ATTRIBUTED)] + public struct VolumeRate : IComparable { + [JSONField(Alias = "Quantity")] public Double Volume { get; set; } + [JSONField(Alias = "Rate")] public Double Price { get; set; } public VolumeRate(Double Volume,Double Price) @@ -12,5 +17,13 @@ namespace sharp.trading this.Price = Price; } + public int CompareTo(object obj) + { + return Price.CompareTo(((VolumeRate)obj).Price); + } + + public double UnityPrice { + get { if (this.Volume != 0) { return this.Price / this.Volume; } return 0; } + } } } diff --git a/bittrex/BittrexBalance.cs b/bittrex/BittrexBalance.cs new file mode 100644 index 0000000..84c04cb --- /dev/null +++ b/bittrex/BittrexBalance.cs @@ -0,0 +1,17 @@ +using System; +using sharp.json; +using sharp.json.attributes; +namespace sharp.trading.bittrex +{ + public class BittrexBalance : Balance + { + [JSONField( Alias = "Balance")] + public override Double CurrentBalance { get; set; } + [JSONField(Alias = "Available")] + public override Double AvailableBalance { get; set; } + [JSONField(Alias = "Pending")] + public override Double PendingBalance { get; set; } + [JSONField(Alias = "CryptoAddress")] + public override string WalletAddress { get; set; } + } +} diff --git a/bittrex/BittrexConnector.cs b/bittrex/BittrexConnector.cs new file mode 100644 index 0000000..a45c408 --- /dev/null +++ b/bittrex/BittrexConnector.cs @@ -0,0 +1,315 @@ +using System; +using sharp.extensions; +using System.Collections.Generic; +using sharp.json; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Globalization; +using sharp.trading.cache; + +/** + * https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=BTC-WAVES&tickInterval=day + * + **/ + +namespace sharp.trading.bittrex +{ + public class BittrexConnector : TradingConnection + { + HMACSHA512 hmac; + ASCIIEncoding ascii = new ASCIIEncoding(); + + public string ApiKey { get; set; } + public string ApiSecret { get; set; } + + JSONMarket[] activeMarkets; + + BittrexCurrency[] currentCurrencies; + + public List orderCache = new List(); + + UInt64 nextNonce = 1; + + public BittrexConnector() + :base("bittrex.com") + { + ApiKey = ""; + ApiSecret = ""; + initialize(); + } + + public BittrexConnector(string apiKey,string apiSecret) + :base("bittrex.com") + { + ApiKey = apiKey; + ApiSecret = apiSecret; + + hmac = new HMACSHA512(Encoding.UTF8.GetBytes(apiSecret)); + + initialize(); + } + + public override object[] getConstructorArguments() + { + return new object[]{ + ApiKey, + ApiSecret + }; + } + + private void initialize(){ + currentCurrencies = _getCurrencies(); + fetchActiveMarkets(); + nextNonce = (ulong)DateTime.UtcNow.Ticks; + nextNonce *= 100; + } + + private JSONMarket jsonMarket(string marketSymbol,string baseSymbol){ + foreach (JSONMarket jm in activeMarkets){ + if (jm.MarketCurrency.Equals(marketSymbol) && jm.BaseCurrency.Equals(baseSymbol)){ + return jm; + } + } + throw new IndexOutOfRangeException(String.Format("Market {0}-{1} not found.",marketSymbol,baseSymbol)); + } + + private void fetchActiveMarkets(){ + List markets = new List(); + + JSON reply = JSONWebRequest.Call("https://bittrex.com/api/v1.1/public/getmarkets"); + + if (reply["success"].Boolean == true){ + + foreach (JSON market in reply["result"]){ + JSONMarket jm = market.To(); + if (jm.IsActive){ + markets.Add(jm); + } + } + + activeMarkets = markets.ToArray(); + } else { + throw new Exception("Failed to read active markets."); + } + } + + public override Pair[] listMarkets() + { + Pair[] pairs = new Pair[activeMarkets.Length]; + + for (int n = 0; n < pairs.Length;n++){ + pairs[n] = new Pair(activeMarkets[n].BaseCurrency,activeMarkets[n].MarketCurrency); + } + + return pairs; + } + + public override Market openMarket(string tradedSymbol, string payingSymbol) + { + JSONMarket jm = jsonMarket(tradedSymbol, payingSymbol); + BittrexMarket m = new BittrexMarket(this, jm); + return m; + } + + public override Balance[] getBalances() + { + JSON balances = authenticatedCall("https://bittrex.com/api/v1.1/account/getbalances"); + return balances.To(); + } + + public override Order[] getOpenOrders() + { + JSON openorders = authenticatedCall("https://bittrex.com/api/v1.1/market/getopenorders"); + return openorders.To(); + } + + public override Order[] getHistoricOrders() + { + JSON historicorders = authenticatedCall("https://bittrex.com/api/v1.1/account/getorderhistory"); + Order[] orders = historicorders.To(); + foreach (Order o in orders){ + o.IsOpen = false; + } + return orders; + } + + public void UpdateOrderCache(){ + orderCache.Clear(); + //orderCache.AddRange((BittrexOrder[])getOpenOrders()); + //orderCache.AddRange((BittrexOrder[])getHistoricOrders()); + } + + public BittrexOrder findCachedOrder(string orderID){ + foreach (BittrexOrder o in orderCache){ + if (o.OrderID.Equals(orderID)){ + return o; + } + } + return null; + } + + public override Order createOrder(OrderType orderType, OrderTarget orderTarget, string marketSymbol, string baseSymbol, double amount, double price) + { + if (orderTarget != OrderTarget.LIMIT){ + throw new NotImplementedException("Only Limit orders are currently supported"); + } + + string marketname = string.Format("{0}-{1}", baseSymbol, marketSymbol); + KeyValuePair[] parms = new KeyValuePair[]{ + new KeyValuePair("market",marketname), + new KeyValuePair("quantity",amount.ToString(CultureInfo.InvariantCulture)), + new KeyValuePair("rate",price.ToString(CultureInfo.InvariantCulture)) + }; + + string url = null; + + switch (orderType) + { + case OrderType.BUY: + url = "https://bittrex.com/api/v1.1/market/buylimit"; + break; + case OrderType.SELL: + url = "https://bittrex.com/api/v1.1/market/selllimit"; + break; + } + + JSON result = authenticatedCall(url, parms); + + return getOrder(result["uuid"].String); + } + + public override Currency[] getCurrencies() + { + return this.currentCurrencies.Segment(0); + } + + private BittrexOrder getOrderOnline(string orderID) + { + KeyValuePair[] parms = new KeyValuePair[]{ + new KeyValuePair("uuid",orderID) + }; + + JSON json = authenticatedCall("https://bittrex.com/api/v1.1/account/getorder", parms); + return json.To(); + } + + public override Order getOrder(string orderID) + { + if (orderCache.Count == 0){ + UpdateOrderCache(); + } + + BittrexOrder cached = findCachedOrder(orderID); + if (cached != null){ + return cached; + } + + return getOrderOnline(orderID); + } + + public override void refreshOrder(Order order){ + BittrexOrder refresh = getOrderOnline(order.OrderID); + order.FilledVolume = refresh.FilledVolume; + order.PayedFees = refresh.PayedFees; + order.PayedPrice = refresh.PayedPrice; + order.OrderState = refresh.OrderState; + order.IsOpen = refresh.IsOpen; + } + + public override void cancelOrder(string orderID) + { + KeyValuePair[] parms = new KeyValuePair[]{ + new KeyValuePair("uuid",orderID) + }; + + authenticatedCall("https://bittrex.com/api/v1.1/market/cancel", parms); + } + + + private BittrexCurrency[] _getCurrencies() + { + JSON jcurrencies = publicCall("https://bittrex.com/api/v1.1/public/getcurrencies"); + return jcurrencies.To(); + } + + + public JSON publicCall(string url){ + KeyValuePair[] parms = new KeyValuePair[0]; + return publicCall(url, parms); + } + public JSON publicCall(string url, KeyValuePair[] parms) + { + StringBuilder sb = new StringBuilder(); + + sb.Append(url); + sb.Append("?"); + + foreach (KeyValuePair parm in parms) + { + sb.Append(Uri.EscapeDataString(parm.Key)); + sb.Append("="); + sb.Append(Uri.EscapeDataString(parm.Value)); + sb.Append("&"); + } + + string finalurl = sb.ToString(); + finalurl = finalurl.Substring(0, finalurl.Length-1); + + + JSON result = JSONWebRequest.Call(finalurl); + + if (!result["success"].Boolean) + { + throw new BittrexException(result); + } + + return result["result"]; + } + + + private JSON authenticatedCall(string url) + { + KeyValuePair[] parms = new KeyValuePair[0]; + return authenticatedCall(url, parms); + } + + private JSON authenticatedCall(string url, KeyValuePair[] parms) + { + StringBuilder sb = new StringBuilder(); + + sb.Append(url); + sb.Append("?"); + + foreach (KeyValuePair parm in parms) + { + sb.Append(Uri.EscapeDataString(parm.Key)); + sb.Append("="); + sb.Append(Uri.EscapeDataString(parm.Value)); + sb.Append("&"); + } + sb.Append("apikey="); + sb.Append(ApiKey); + sb.Append("&nonce="); + sb.Append(nextNonce++); + + string finalurl = sb.ToString(); + string apisign = HexString.toString(hmac.ComputeHash(Encoding.ASCII.GetBytes(finalurl))).ToLower(); + + Dictionary headers = new Dictionary(); + headers["apisign"] = apisign; + + //Console.WriteLine("CALL: {0}", finalurl); + + JSON result = JSONWebRequest.Call(finalurl, null, headers); + + if (!result["success"].Boolean) + { + throw new BittrexException(result); + } + + return result["result"]; + } + + } +} diff --git a/bittrex/BittrexCurrency.cs b/bittrex/BittrexCurrency.cs new file mode 100644 index 0000000..83ae248 --- /dev/null +++ b/bittrex/BittrexCurrency.cs @@ -0,0 +1,18 @@ +using System; +using sharp.json; + +namespace sharp.trading.bittrex +{ + public class BittrexCurrency : Currency + { + public BittrexCurrency(JSON json) + { + this.Symbol = json["Currency"].String; + this.Name = json["CurrencyLong"].String; + this.WithdrawalFee = json["TxFee"].Double; + this.BaseAddress = json["BaseAddress"].String; + } + + + } +} diff --git a/bittrex/BittrexException.cs b/bittrex/BittrexException.cs new file mode 100644 index 0000000..ef264d4 --- /dev/null +++ b/bittrex/BittrexException.cs @@ -0,0 +1,17 @@ +using System; +using sharp.json; + +namespace sharp.trading.bittrex +{ + public class BittrexException : Exception + { + public JSON JSONResult { get; private set; } + + public BittrexException(JSON result) + :base(result["message"].To()) + { + JSONResult = result; + } + + } +} diff --git a/bittrex/BittrexHistoricTrade.cs b/bittrex/BittrexHistoricTrade.cs new file mode 100644 index 0000000..c3218f8 --- /dev/null +++ b/bittrex/BittrexHistoricTrade.cs @@ -0,0 +1,26 @@ +using System; +using sharp.json; +namespace sharp.trading.bittrex +{ + public class BittrexHistoricTrade : HistoricTrade + { + public BittrexHistoricTrade(JSON json) + { + UniqueID = json["Id"].Integer; + TimeStamp = DateTime.Parse(json["TimeStamp"].String); + Price = json["Price"].Double; + TotalPrice = json["Total"].Double; + Volume = json["Quantity"].Double; + + string ot = json["OrderType"].String; + switch (ot){ + case "BUY": + OrderType = OrderType.BUY; + break; + case "SELL": + OrderType = OrderType.SELL; + break; + } + } + } +} diff --git a/bittrex/BittrexMarket.cs b/bittrex/BittrexMarket.cs new file mode 100644 index 0000000..fe58342 --- /dev/null +++ b/bittrex/BittrexMarket.cs @@ -0,0 +1,75 @@ +using System; +using sharp.json; +using System.Collections.Generic; +using System.Runtime.Remoting.Messaging; +namespace sharp.trading.bittrex +{ + public class BittrexMarket : Market + { + BittrexConnector connector; + JSONMarket market; + + OrderBook orderBook; + + public BittrexMarket(BittrexConnector connector, JSONMarket market) + : base(connector, market.MarketCurrency, market.BaseCurrency) + { + this.market = market; + this.connector = connector; + + MinimumTradeVolume = market.MinTradeSize; + + orderBook = new BittrexOrderbook(this); + } + + public override void Close() + { + /** TODO: Nothing to free just now **/ + } + + public override OrderBook getOrderBook() + { + orderBook.Refresh(); + return this.orderBook; + } + + public override Order[] getOrders() + { + throw new NotImplementedException(); + } + + public string MarketName { get { return market.MarketName; } } + public string MarketCurrency { get { return market.MarketCurrency; } } + public string BaseCurrency { get { return market.BaseCurrency; } } + + + public override string ToString() + { + return string.Format("[BittrexMarket: MarketName={0}, MarketCurrency={1}, BaseCurrency={2}]", MarketName, MarketCurrency, BaseCurrency); + } + + public override HistoricTrade[] getHistoricTrades() + { + KeyValuePair[] parms = new KeyValuePair[]{ + new KeyValuePair("market",MarketName) + }; + JSON result = connector.publicCall("https://bittrex.com/api/v1.1/public/getmarkethistory", parms); + return result.To(); + } + + public override Tick[] getTicks() + { + KeyValuePair[] parms = new KeyValuePair[]{ + new KeyValuePair("marketName",MarketName), + new KeyValuePair("tickInterval","day") + }; + JSON jticks = connector.publicCall("https://bittrex.com/Api/v2.0/pub/market/GetTicks",parms); + return jticks.To(); + } + + + } + + + +} diff --git a/bittrex/BittrexOrder.cs b/bittrex/BittrexOrder.cs new file mode 100644 index 0000000..875ee35 --- /dev/null +++ b/bittrex/BittrexOrder.cs @@ -0,0 +1,108 @@ +using System; +using sharp.json; +using sharp.json.attributes; +namespace sharp.trading.bittrex +{ + [JSONClassPolicy( Policy = JSONPolicy.ALL)] + public class BittrexOrder : Order + { + BittrexConnector Connector { get; set; } + + public BittrexOrder(BittrexConnector connector) + { + this.Connector = connector; + } + + public BittrexOrder() + { + } + + public override string ToString() + { + return string.Format("[BittrexOrder: MarketName={0}, Limit={1}, OrderVolume={2}, FilledVolume={3}]", MarketName, LimitPrice, OrderVolume, FilledVolume); + } + + + + [JSONField(Alias = "Exchange")] + string MarketName { get; set; } + + [JSONField(Alias = "Type")] + private string __type + { + get { + return null; + } + set + { + BittrexOrderType = value; + } + } + + [JSONField( Alias = "OrderType")] + private string BittrexOrderType { + set { + if (value.Equals("LIMIT_SELL")){ + OrderType = OrderType.SELL; + OrderTarget = OrderTarget.LIMIT; + } else if (value.Equals("LIMIT_BUY")){ + OrderType = OrderType.BUY; + OrderTarget = OrderTarget.LIMIT; + } + } + } + + [JSONField( Alias = "Quantity")] + private double _volume { + get { return OrderVolume; } + set { + OrderVolume = value; + } + } + + [JSONField(Alias = "QuantityRemaining")] + double OrderVolumeRemaining + { + get; set; + } + + public override double FilledVolume { + get { return this.OrderVolume - this.OrderVolumeRemaining; } + set { this.OrderVolumeRemaining = this.OrderVolume - value; } + } + + [JSONField(Alias = "Limit")] + private double _limit + { + get { return this.LimitPrice; } + set { this.LimitPrice = value; } + } + + [JSONField(Alias = "Price")] + private double _price + { + get { return this.PayedPrice; } + set { this.PayedPrice = value; } + } + + [JSONField(Alias = "Commission")] + private double _fee + { + get { return this.PayedFees; } + set { this.PayedFees = value; } + } + [JSONField(Alias = "CommissionPaid")] + private double _fee2 + { + get { return this.PayedFees; } + set { this.PayedFees = value; } + } + + [JSONField] + private string OrderUuid { + get { return this.OrderID; } + set { this.OrderID = value; } + } + + } +} diff --git a/bittrex/BittrexOrderbook.cs b/bittrex/BittrexOrderbook.cs new file mode 100644 index 0000000..265a520 --- /dev/null +++ b/bittrex/BittrexOrderbook.cs @@ -0,0 +1,38 @@ +using System; +using sharp.json; +using System.Collections.Generic; +using System.IO; + +namespace sharp.trading.bittrex +{ + + public class BittrexOrderbook : OrderBook + { + BittrexMarket bmarket; + + public BittrexOrderbook(BittrexMarket market) + :base(market) + { + bmarket = market; + } + + public override void Refresh() + { + JSON reply = JSONWebRequest.Call(String.Format("https://bittrex.com/api/v1.1/public/getorderbook?market={0}&type=both",bmarket.MarketName)); + + if (!reply["success"].Boolean){ + throw new IOException("Orderbook Refresh failed: " + reply["message"].String); + } + + VolumeRate[] bids = reply["result"]["buy"].To(); + VolumeRate[] asks = reply["result"]["sell"].To(); + + Array.Sort(bids); + Array.Sort(asks); + + Bids = bids; + Asks = asks; + } + } + +} diff --git a/bittrex/BittrexTick.cs b/bittrex/BittrexTick.cs new file mode 100644 index 0000000..1c9a4db --- /dev/null +++ b/bittrex/BittrexTick.cs @@ -0,0 +1,17 @@ +using System; +using sharp.json; +namespace sharp.trading.bittrex +{ + public class BittrexTick : Tick + { + public BittrexTick(JSON json) + { + TimeStamp = json["T"].To(); + OpenAt = json["O"].To(); + CloseAt = json["C"].To(); + High = json["H"].To(); + Low = json["L"].To(); + BaseVolume = json["BV"].To(); + } + } +} diff --git a/bittrex/JSONMarket.cs b/bittrex/JSONMarket.cs new file mode 100644 index 0000000..913d093 --- /dev/null +++ b/bittrex/JSONMarket.cs @@ -0,0 +1,19 @@ +using System; +namespace sharp.trading.bittrex +{ + public struct JSONMarket + { + public string MarketName { get; private set; } + public string MarketCurrency { get; private set; } + public string BaseCurrency { get; private set; } + public string MarketCurrencyLong { get; private set; } + public string BaseCurrencyLong { get; private set; } + public bool IsActive { get; private set; } + public Double MinTradeSize { get; private set; } + + public override string ToString() + { + return string.Format("[JSONMarket: MarketName={0}, MarketCurrency={1}, BaseCurrency={2}, MarketCurrencyLong={3}, BaseCurrencyLong={4}, IsActive={5}, MinTradeSize={6}]", MarketName, MarketCurrency, BaseCurrency, MarketCurrencyLong, BaseCurrencyLong, IsActive, MinTradeSize); + } + } +} diff --git a/cache/HistoryCache.cs b/cache/HistoryCache.cs new file mode 100644 index 0000000..ee3cbe4 --- /dev/null +++ b/cache/HistoryCache.cs @@ -0,0 +1,160 @@ +using System; +using System.IO; +using System.Net; +using sharp.json; +using System.Collections.Generic; +using System.Linq; +namespace sharp.trading.cache +{ + public class HistoryCache + { + public static string BaseDataDirectory { get { return Path.Combine(TradingEnvironment.DataDirectory, "cache", "history"); } } + public static Dictionary caches = new Dictionary(); + + private static string getCacheName(Market market) + { + return Path.Combine(market.Connection.UniqueProviderName, market.PayingSymbol, market.TradedSymbol); + } + private static string getCacheDir(Market market) + { + return Path.Combine(BaseDataDirectory, getCacheName(market)); + } + private static bool createIfNotExists(string directoryName) + { + if (!Directory.Exists(directoryName)) + { + Directory.CreateDirectory(directoryName); + return false; + } + return false; + } + + public static HistoryCache getCache(Market market) + { + string cname = getCacheName(market); + if (!caches.ContainsKey(cname)) + { + caches[cname] = new HistoryCache(market); + } + return caches[cname]; + } + + public string CacheName { get; private set; } + public string DataDirectory { get; private set; } + public string HistoricTradesFileName { get; private set; } + + //Dictionary history = new Dictionary(); + SortedSet history = new SortedSet(); + + public HistoryCache(Market market){ + CacheName = getCacheName(market); + DataDirectory = Path.Combine(BaseDataDirectory, CacheName); + + createIfNotExists(DataDirectory); + + HistoricTradesFileName = Path.Combine(DataDirectory, "history.json"); + + load(); + + // Import Ticks if nothing available + if (this.history.Count == 0){ + Tick[] ticks = market.getTicks(); + int n = 0; + + foreach (Tick tick in ticks){ + HistoricTrade ht = new HistoricTrade(); + ht.TimeStamp = tick.TimeStamp; + ht.Price = (tick.CloseAt + tick.OpenAt) / 2; + ht.Volume = tick.BaseVolume / ht.Price; + ht.TotalPrice = ht.Price * ht.Volume; + ht.UniqueID = n++; + history.Add(ht); + } + + } + + + } + + private void load(){ + if (File.Exists(HistoricTradesFileName)){ + JSON json = JSON.ReadFrom(HistoricTradesFileName); + foreach (HistoricTrade trade in json.To()){ + this.history.Add(trade); + } + }; + } + private void save(){ + lock(this) + { + HistoricTrade[] trades = this.history.ToArray(); + JSON json = JSONConverter.From(trades); + json.WriteTo(HistoricTradesFileName, true); + } + } + + public void Update(HistoricTrade[] trades){ + lock (this) + { + foreach (HistoricTrade trade in trades) + { + if (!history.Contains(trade)) + { + history.Add(trade); + } + } + } + save(); + } + + public WeightedAverage calulateWeightedAveragesPerVolume(double volume) + { + return calulateWeightedAveragesPerVolume(new double[] { volume })[0]; + } + public WeightedAverage[] calulateWeightedAveragesPerVolume(double[] volumes) + { + WeightedAverage[] averages = new WeightedAverage[volumes.Length]; + + for (int n = 0; n 0) + { + double v = volumes[n] > trade.Volume ? trade.Volume : volumes[n]; + averages[n].TotalPrice += trade.Price * v; + averages[n].Volume += v; + volumes[n] -= v; + used++; + } + } + + if (used == 0) + { + break; + } + + } + } + return averages; + } + + + + + private static void checkDataDirectory(){ + if (!Directory.Exists(BaseDataDirectory)){ + Directory.CreateDirectory(BaseDataDirectory); + } + } + + } +} diff --git a/cache/HistoryService.cs b/cache/HistoryService.cs new file mode 100644 index 0000000..84a04b9 --- /dev/null +++ b/cache/HistoryService.cs @@ -0,0 +1,41 @@ +using System; +using sharp.trading.services; +using sharp.extensions; +using System.Net.Sockets; +namespace sharp.trading.cache +{ + public class HistoryService : ITradingService + { + public HistoryService() + { + } + + public void RunService() + { + Console.WriteLine("History Service runs"); + + Pair[] marketPairs = TradingEnvironment.DefaultConnection.listMarkets(); + + foreach (Pair pair in marketPairs){ + try + { + Market market = TradingEnvironment.DefaultConnection.openMarket(pair); + Console.WriteLine("History Update: {0}-{1}",market.TradedSymbol,market.PayingSymbol); + HistoryCache.getCache(market).Update(market.getHistoricTrades()); + Console.WriteLine("History Update: {0}-{1} finished.",market.TradedSymbol,market.PayingSymbol); + } catch (Exception e){ + Console.WriteLine("History service could not update market data for {0}-{1}",pair.Item1,pair.Item2); + } + } + } + + public int SchedulingIntervall() + { + return 120; + } + + public void Shutdown() + { + } + } +} diff --git a/cache/WeightedAverage.cs b/cache/WeightedAverage.cs new file mode 100644 index 0000000..ef613cf --- /dev/null +++ b/cache/WeightedAverage.cs @@ -0,0 +1,34 @@ +using System; +namespace sharp.trading.cache +{ + public class WeightedAverage + { + public string marketSymbol { get; private set; } + public string baseSymbol { get; private set; } + + public double Volume { get; set; } + public double TotalPrice { get; set; } + public double AveragePrice { get { return TotalPrice / Volume; } } + + public WeightedAverage(string msym,string bsym,double vol,double total) + { + marketSymbol = msym; + baseSymbol = bsym; + Volume = vol; + TotalPrice = total; + } + public WeightedAverage(string msym, string bsym) + { + marketSymbol = msym; + baseSymbol = bsym; + Volume = 0; + TotalPrice = 0; + } + + + public override string ToString() + { + return string.Format("[WeightedAverage: Volume={0}, TotalPrice={1}, AveragePrice={2}]", Volume, TotalPrice, AveragePrice); + } + } +} diff --git a/services/ITradingService.cs b/services/ITradingService.cs new file mode 100644 index 0000000..b4f7324 --- /dev/null +++ b/services/ITradingService.cs @@ -0,0 +1,12 @@ +using System; +using System.Runtime.CompilerServices; +namespace sharp.trading.services +{ + public interface ITradingService + { + void RunService(); + int SchedulingIntervall(); + + void Shutdown(); + } +} diff --git a/services/ServiceManager.cs b/services/ServiceManager.cs new file mode 100644 index 0000000..7e63690 --- /dev/null +++ b/services/ServiceManager.cs @@ -0,0 +1,182 @@ +using System; +using System.Threading; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Permissions; +using sharp.extensions; +using sharp.json; +using System.IO; +namespace sharp.trading.services +{ + public class ServiceManager + { + public static ServiceManager Default { get; private set; } = new ServiceManager(); + + public static void ReplaceDefault(ServiceManager newDefault){ + if (Default != null){ + Default.Shutdown(); + } + Default = newDefault; + } + + Thread serviceThread; + bool shutdown = false; + + List scheduledServices = new List(); + + public ServiceManager() + { + serviceThread = new Thread(_scheduler); + serviceThread.Start(); + + Load(); + } + + public void Shutdown(){ + lock (this){ + shutdown = true; + } + + while (serviceThread.IsAlive){ + Thread.Sleep(250); + } + + foreach (ScheduledService service in scheduledServices){ + while (service.IsRunning){ + Thread.Sleep(250); + } + service.TradingService.Shutdown(); + } + + Save(); + } + + private void Load(){ + if (File.Exists(Path.Combine(TradingEnvironment.DataDirectory, "services.json"))){ + JSON json = JSON.ReadFrom(Path.Combine(TradingEnvironment.DataDirectory, "services.json")); + ConstructableObject[] cos = json.To(); + foreach (ConstructableObject co in cos){ + ITradingService its = (ITradingService)co.Construct(); + Schedule(its,true); + } + + } + } + + public void Save(){ + List col = new List(); + lock (this){ + foreach (ScheduledService sservice in scheduledServices){ + ConstructableObject co = new ConstructableObject(sservice.TradingService); + col.Add(co); + } + } + + JSON json = JSONConverter.From(col.ToArray()); + json.WriteTo(Path.Combine(TradingEnvironment.DataDirectory,"services.json"),true); + } + + + public void Schedule(ITradingService service, bool RunImmediate = false){ + lock (this){ + ScheduledService sservice = new ScheduledService(service); + if (RunImmediate){ + sservice.ScheduledRun = DateTime.UtcNow.Ticks; + } + scheduledServices.Add(sservice); + } + } + public void Unschedule(ITradingService service){ + lock (this){ + foreach (ScheduledService sserv in scheduledServices.ToArray()){ + if (sserv.TradingService == service){ + scheduledServices.Remove(sserv); + break; + } + } + } + } + + + + void _scheduler(){ + while (true){ + try { + Thread.Sleep(1000); + } catch (Exception e){ + } + + if (shutdown){ + break; + } + + lock(this){ + + foreach (ScheduledService sservice in scheduledServices){ + if (sservice.HasElapsed){ + //ThreadPool.QueueUserWorkItem((state) => sservice.Run()); + sservice.Run(); + } + } + + } + + } + } + + + class ScheduledService { + public Int64 ScheduledRun { get; set; } + public Int64 ScheduledIntervall { get; private set; } + + public ITradingService TradingService { get; private set; } + + bool isRunning; + public bool IsRunning { + get { lock (this) { return isRunning; } } + private set { lock(this) { this.isRunning = value; } } + } + + public ScheduledService(ITradingService tradingService){ + this.TradingService = tradingService; + this.ScheduledRun = DateTime.UtcNow.Ticks + (tradingService.SchedulingIntervall() * 10000000); + } + + public bool HasElapsed { + get + { + return ScheduledRun <= DateTime.UtcNow.Ticks; + } + } + + + public void Run(){ + lock(this){ + + if (!IsRunning){ + IsRunning = true; + } else { + Console.WriteLine("Service already running, re-scheduling..."); + ScheduledRun += ScheduledIntervall; + return; + //throw new Exception("Tried to run ScheduledService while already running"); + } + + ScheduledIntervall = TradingService.SchedulingIntervall() * 10000000; + ScheduledRun += ScheduledIntervall; + } + + try { + + TradingService.RunService(); + + } catch (Exception e){ + Console.WriteLine("ScheduledService threw exception: {0}",e.ToString()); + } + + IsRunning = false; + } + } + + } +} diff --git a/sharp.trading.csproj b/sharp.trading.csproj index 3926ba3..0356c13 100644 --- a/sharp.trading.csproj +++ b/sharp.trading.csproj @@ -7,7 +7,7 @@ Library sharp.trading sharp.trading - v4.5 + v4.7 true @@ -35,6 +35,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {97CA3CA9-98B3-4492-B072-D7A5995B68E9} + sharp.extensions + + + {D9342117-3249-4D8B-87C9-51A50676B158} + sharp.json + + + + + + \ No newline at end of file