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