// Notes: // - A TradeCompleted is a completed single trade that is part of an OrderCompleted // - TradeCompleted doesn't really need 'pair' and 'tt' because they are duplicates // of fields in the containing OrderCompleted. However, they allow convenient conversion // to CoinIn/CoinOut etc. public TradeCompleted(long order_id, long trade_id, TradePair pair, ETradeType tt, Unit <decimal> amount_in, Unit <decimal> amount_out, Unit <decimal> commission, Coin commission_coin, DateTimeOffset created, DateTimeOffset updated) { // Check units if (amount_in <= 0m._(tt.CoinIn(pair))) { throw new Exception("Invalid 'in' amount"); } if (amount_out <= 0m._(tt.CoinOut(pair))) { throw new Exception("Invalid 'out' amount"); } if (commission < 0m._(commission_coin)) { throw new Exception("Negative commission"); } if (created < Misc.CryptoCurrencyEpoch) { throw new Exception("Invalid creation time"); } OrderId = order_id; TradeId = trade_id; Pair = pair; TradeType = tt; AmountIn = amount_in; AmountOut = amount_out; Commission = commission; CommissionCoin = commission_coin; Created = created; Updated = updated; }
/// <summary>Return the unit type for a trade of this type on 'pair'</summary> public static string RateUnits(this ETradeType tt, TradePair pair) { return (tt == ETradeType.B2Q ? pair.RateUnits : tt == ETradeType.Q2B ? pair.RateUnitsInv : throw new Exception("Unknown trade type")); }
public OrderResult(TradePair pair, long order_id, bool filled, IEnumerable <Fill> trades) { Pair = pair; OrderId = order_id; Trades = trades?.ToList() ?? new List <Fill>(); Filled = filled; }
/// <summary>Enumerate all candle data and time frames provided by this exchange</summary> protected override IEnumerable <PairAndTF> EnumAvailableCandleDataInternal(TradePair pair) { if (pair != null) { var cp = new CurrencyPair(pair.Base, pair.Quote); if (!Pairs.ContainsKey(pair.UniqueKey)) { yield break; } foreach (var mp in Enum <EMarketPeriod> .Values) { yield return(new PairAndTF(pair, ToTimeFrame(mp))); } } else { foreach (var p in Pairs) { foreach (var mp in Enum <EMarketPeriod> .Values) { yield return(new PairAndTF(p, ToTimeFrame(mp))); } } } }
/// <summary>Return the 'out' coin for a trade on 'pair' in this trade direction</summary> public static Coin CoinOut(this ETradeType tt, TradePair pair) { return (tt == ETradeType.B2Q ? pair.Quote : tt == ETradeType.Q2B ? pair.Base : throw new Exception("Unknown trade type")); }
// Notes: // - A 'Trade' is a description of a trade that *could* be placed. It is different // to an 'Order' which is live on an exchange, waiting to be filled. // - AmountIn * Price does not have to equal AmountOut, because 'Trade' is used // with the order book to calculate the best price for trading a given amount. // The price will represent the best price that covers all of the amount, not // the spot price. // - Don't implicitly change amounts/prices based on order type for the same reason. // Instead, allow any values to be set and use validate to check they're correct. // The 'EditTradeUI' should be used to modify properties and ensure correct behaviour // w.r.t to order type. // Rounding issues: // - Quantisation doesn't help, that just makes the discrepancies larger. // - Instead, maintain separate values for amount in and amount out. These imply the // price and allow control over which side of the trade gets rounded. /// <summary>Create a trade on 'pair' to convert 'amount_in' of 'coin_in' to 'amount_out'</summary> public Trade(Fund fund, TradePair pair, EOrderType order_type, ETradeType trade_type, Unit <decimal> amount_in, Unit <decimal> amount_out, Unit <decimal>?price_q2b = null, string creator = null) { // Check trade amounts and units if (amount_in < 0m._(trade_type.CoinIn(pair))) { throw new Exception("Invalid trade 'in' amount"); } if (amount_out < 0m._(trade_type.CoinOut(pair))) { throw new Exception("Invalid trade 'out' amount"); } if (amount_out != 0 && amount_in != 0 && trade_type.PriceQ2B(amount_out / amount_in) < 0m._(pair.RateUnits)) { throw new Exception("Invalid trade price"); } CreatorName = creator ?? string.Empty; Fund = fund; Pair = pair; OrderType = order_type; TradeType = trade_type; AmountIn = amount_in; AmountOut = amount_out; PriceQ2B = price_q2b != null ? price_q2b.Value : amount_out != 0 && amount_in != 0 ? TradeType.PriceQ2B(amount_out / amount_in) : SpotPriceQ2B; }
// Notes: // - An OrderCompleted is a completed (or partially completed) Order consisting // of one or more 'TradeCompleted's that where made to complete the order. public OrderCompleted(long order_id, Fund fund, TradePair pair, ETradeType tt) { OrderId = order_id; Fund = fund; TradeType = tt; Pair = pair; Trades = new TradeCompletedCollection(this); }
public PairNames(TradePair pair) { if (pair == null) { throw new ArgumentNullException(nameof(pair)); } Base = pair.Base.Symbol; Quote = pair.Quote.Symbol; }
/// <summary>Return the chart data for a given pair, over a given time range</summary> protected override Task <List <Candle> > CandleDataInternal(TradePair pair, ETimeFrame timeframe, UnixSec time_beg, UnixSec time_end, CancellationToken?cancel) // Worker thread context { // Get the chart data var cp = new CurrencyPair(pair.Base, pair.Quote); var data = Api.CandleData[cp, ToMarketPeriod(timeframe), time_beg, time_end, cancel]; //var data = await Api.GetChartData(cp, ToMarketPeriod(timeframe), time_beg, time_end, cancel); // Convert it to candles var candles = data.Select(x => new Candle(x.Time.Ticks, x.Open, x.High, x.Low, x.Close, x.Median, x.Volume)).ToList(); return(Task.FromResult(candles)); }
/// <summary>Cancel an open trade</summary> protected async override Task <bool> CancelOrderInternal(TradePair pair, long order_id, CancellationToken cancel) { try { // Cancel the trade return(await Api.CancelTrade(new CurrencyPair(pair.Base, pair.Quote), order_id)); } catch (Exception ex) { throw new Exception($"Poloniex: Cancel trade (id={order_id}) failed. {ex.Message}", ex); } }
///// <summary>Return the order book for 'pair' to a depth of 'count'</summary> //protected async override Task<MarketDepth> MarketDepthInternal(TradePair pair, int depth) // Worker thread context //{ // var cp = new CurrencyPair(pair.Base, pair.Quote); // var orders = await Api.GetOrderBook(cp, depth, cancel: Shutdown.Token); // // Update the depth of market data // var market_depth = new MarketDepth(pair.Base, pair.Quote); // var buys = orders.BuyOffers.Select(x => new Offer(x.Price._(pair.RateUnits), x.AmountBase._(pair.Base))).ToArray(); // var sells = orders.SellOffers.Select(x => new Offer(x.Price._(pair.RateUnits), x.AmountBase._(pair.Base))).ToArray(); // market_depth.UpdateOrderBooks(buys, sells); // return market_depth; //} /// <summary>Return the chart data for a given pair, over a given time range</summary> protected async override Task <List <Candle> > CandleDataInternal(TradePair pair, ETimeFrame timeframe, UnixSec time_beg, UnixSec time_end, CancellationToken?cancel) // Worker thread context { var cp = new CurrencyPair(pair.Base, pair.Quote); // Get the chart data var data = await Api.GetChartData(cp, ToMarketPeriod(timeframe), time_beg, time_end, cancel); // Convert it to candles (yes, Polo gets the base/quote backwards for 'Volume') var candles = data.Select(x => new Candle(x.Time.Ticks, x.Open, x.High, x.Low, x.Close, x.WeightedAverage, x.VolumeQuote)).ToList(); return(candles); }
// Notes: // - An Order is a request to buy/sell that has been sent to an exchange and // should exist somewhere in their order book. When an Order is filled it // becomes a 'OrderCompleted' public Order(long order_id, Fund fund, TradePair pair, EOrderType ot, ETradeType tt, Unit <decimal> amount_in, Unit <decimal> amount_out, Unit <decimal> remaining_in, DateTimeOffset created, DateTimeOffset updated) : base(fund, pair, ot, tt, amount_in, amount_out) { if (created < Misc.CryptoCurrencyEpoch) { throw new Exception("Invalid creation time"); } OrderId = order_id; UniqueKey = Guid.NewGuid(); RemainingIn = remaining_in; Created = created; Updated = updated; }
/// <summary>Cancel an existing position</summary> public bool CancelOrderInternal(TradePair pair, long order_id) { // Doesn't exist? if (!m_ord.TryGetValue(order_id, out var order)) { return(false); } // Remove 'pos' m_ord.Remove(order_id); // Remove any hold on the balance for this trade var bal = m_bal[order.CoinIn]; bal.Holds.Remove(order_id); return(true); }
/// <summary>Cancel an open trade</summary> protected async override Task <bool> CancelOrderInternal(TradePair pair, long order_id, CancellationToken cancel) { try { // Convert a CoinFlip order id to a Bittrex UUID var uuid = m_order_id_lookup[order_id]; // Cancel the trade var result = await Api.CancelTrade(new CurrencyPair(pair.Base, pair.Quote), uuid); return(result.Id == uuid); } catch (Exception ex) { throw new Exception($"Bittrex: Cancel trade (id={order_id}) failed. {ex.Message}", ex); } }
/// <summary>Check this order against the limits given in 'pair'</summary> public Exception Validate(TradePair pair) { if (PriceQ2B <= 0m) { return(new Exception($"Offer price ({PriceQ2B.ToString(6)}) is <= 0")); } if (!pair.AmountRangeBase.Contains(AmountBase)) { return(new Exception($"Offer amount ({AmountBase.ToString(6)}) is not within the valid range: [{pair.AmountRangeBase.Beg},{pair.AmountRangeBase.End}]")); } if (!pair.AmountRangeQuote.Contains(AmountQuote)) { return(new Exception($"Offer amount ({AmountQuote.ToString(6)}) is not within the valid range: [{pair.AmountRangeQuote.Beg},{pair.AmountRangeQuote.End}]")); } return(null); }
/// <summary>Generate simulated market depth for 'pair', using 'latest' as the reference for the current spot price</summary> private MarketDepth GenerateMarketDepth(TradePair pair, Candle latest, ETimeFrame time_frame) { // Notes: // - This is an expensive call when back testing is running so minimise allocation, resizing, and sorting. // - Do all calculations using double's for speed. // Get the market data for 'pair'. // Market data is maintained independently to the pair's market data instance because the // rest of the application expects the market data to periodically overwrite the pair's order books. var md = m_depth[pair]; // Get the Q2B (bid) spot price from the candle close. (This is the minimum of the Q2B offers) // The B2Q spot price is Q2B - spread, which will be the maximum of the B2Q offers var spread = latest.Close * m_spread_frac; var best_q2b = latest.Close; var best_b2q = latest.Close - spread; var base_value = (double)(decimal)pair.Base.Value; md.Q2B.Offers.Resize(m_orders_per_book); md.B2Q.Offers.Resize(m_orders_per_book); // Generate offers with a normal distribution about 'best' var range = 0.2 * 0.5 * (best_q2b + best_b2q); for (var i = 0; i != m_orders_per_book; ++i) { var p = range * Math_.Sqr((double)i / m_orders_per_book); md.Q2B.Offers[i] = new Offer(((decimal)(best_q2b + p))._(pair.RateUnits), RandomAmountBase()._(pair.Base)); md.B2Q.Offers[i] = new Offer(((decimal)(best_b2q - p))._(pair.RateUnits), RandomAmountBase()._(pair.Base)); } return(md); decimal RandomAmountBase() { // Generate an amount to trade in the application common currency (probably USD). // Then convert that to base currency using the 'live' value. var common_value = Math.Abs(m_rng.Double(m_order_value_range.Beg, m_order_value_range.End)); var amount_base = (decimal)Math_.Div(common_value, base_value, common_value); return(amount_base); } }
/// <summary>Update this exchange's set of trading pairs</summary> protected override Task UpdatePairsInternal(HashSet <string> coins) // Worker thread context { Model.DataUpdates.Add(() => { // Create cross-exchange pairs for each coin of interest foreach (var cd in SettingsData.Settings.Coins.Where(x => x.CreateCrossExchangePairs)) { var sym = cd.Symbol; // Find the exchanges that have this coin var exchanges = Exchanges.Where(x => x.Coins.ContainsKey(sym)).ToArray(); // Create trading pairs between the same currencies on different exchanges for (int j = 0; j < exchanges.Length - 1; ++j) { for (int i = j + 1; i < exchanges.Length; ++i) { // Check whether the pair already exists var exch0 = exchanges[j]; var exch1 = exchanges[i]; var pair = Pairs[sym, exch0, exch1]; if (pair != null) { continue; } // If not, add it pair = new TradePair(exch0.Coins[sym], exch1.Coins[sym], this); Pairs.Add(pair); // Add the coins Coins.Add(pair.Base.SymbolWithExchange, pair.Base); Coins.Add(pair.Quote.SymbolWithExchange, pair.Quote); } } } }); return(Task.CompletedTask); }
// Notes: // - PriceData represents a single pair and TimeFrame on an exchange. // - Handles adding new data to the DB. // - Serves data from the DB to the Instruments. // - Use an 'Instrument' to view these data public PriceData(TradePair pair, ETimeFrame time_frame, CancellationToken shutdown) { try { Pair = pair; TimeFrame = time_frame; DataAvailable = pair.CandleDataAvailable.Contains(time_frame); UpdatePollRate = TimeSpan.FromMilliseconds(SettingsData.Settings.PriceDataUpdatePeriodMS); MainShutdownToken = shutdown; // Set up the ref count m_ref = new List <object>(); // Load the database of historic price data var db_filepath = DBFilePath(pair.Exchange.Name, pair.Name); DB = new SQLiteConnection($"Data Source={db_filepath};Version=3;journal mode=Memory;synchronous=Off"); // Ensure a table exists for the time frame if (DataAvailable) { DB.Execute( $"create table if not exists {TimeFrame} (\n" + $" [{nameof(Candle.Timestamp)}] integer unique primary key,\n" + $" [{nameof(Candle.Open)}] real not null,\n" + $" [{nameof(Candle.High)}] real not null,\n" + $" [{nameof(Candle.Low)}] real not null,\n" + $" [{nameof(Candle.Close)}] real not null,\n" + $" [{nameof(Candle.Median)}] real not null,\n" + $" [{nameof(Candle.Volume)}] real not null\n" + $")"); } } catch { Dispose(); throw; } }
/// <summary>Cancel an open trade</summary> protected override async Task <bool> CancelOrderInternal(TradePair pair, long order_id, CancellationToken cancel) { try { var cp = new CurrencyPair(pair.Base, pair.Quote); // Look up the Binance ClientOrderId for 'order_id' var cid = Api.UserData.Orders[cp].FirstOrDefault(x => x.OrderId == order_id)?.ClientOrderId; if (cid == null) { throw new Exception($"Could not find an order with this order id ({order_id})"); } // Cancel the order var res = await Api.CancelTrade(cp, cid, cancel); return(res.Status == EOrderStatus.CANCELED); } catch (Exception ex) { throw new Exception($"Binance: Cancel trade failed. {ex.Message}\nOrder Id: {order_id } Pair: {pair.Name}", ex); } }
/// <summary>Attempt to make a trade on 'pair' for the given 'price' and base 'amount'</summary> private void TryFillOrder(TradePair pair, Fund fund, long order_id, ETradeType tt, EOrderType ot, Unit <decimal> amount_in, Unit <decimal> amount_out, Unit <decimal> remaining_in, out Order ord, out OrderCompleted his) { // The order can be filled immediately, filled partially, or not filled and remain as an 'Order'. // Also, exchanges use the base currency as the amount to fill, so for Q2B trades it's possible // that 'amount_in' is less than the trade asked for. var market = m_depth[pair]; // Consume orders var price_q2b = tt.PriceQ2B(amount_out / amount_in); var amount_base = tt.AmountBase(price_q2b, amount_in: remaining_in); var filled = market.Consume(pair, tt, ot, price_q2b, amount_base, out var remaining_base); // The order is partially or completely filled... Debug.Assert(Misc.EqlAmount(amount_base, filled.Sum(x => x.AmountBase) + remaining_base)); ord = remaining_base != 0 ? new Order(order_id, fund, pair, ot, tt, amount_in, amount_out, tt.AmountIn(remaining_base, price_q2b), Model.UtcNow, Model.UtcNow) : null; his = remaining_base != amount_base ? new OrderCompleted(order_id, fund, pair, tt) : null; // Add 'TradeCompleted' entries for each order book offer that was filled foreach (var fill in filled) { his.Trades.AddOrUpdate(new TradeCompleted(his.OrderId, ++m_history_id, pair, tt, fill.AmountIn(tt), fill.AmountOut(tt), Exchange.Fee * fill.AmountOut(tt), tt.CoinOut(pair), Model.UtcNow, Model.UtcNow)); } }
/// <summary>Return the price data for 'pair' and 'time_frame'</summary> public PriceData this[TradePair pair, ETimeFrame time_frame] { get { if (pair == null) { throw new ArgumentNullException(nameof(pair)); } if (time_frame == ETimeFrame.None) { throw new Exception("TimeFrame is None. No price data available"); } if (!pair.Exchange.Enabled) { throw new Exception("Requesting a trading pair on an inactive exchange"); } // Get the TimeFrame to PriceData map for the given pair var tf_map = Pairs.TryGetValue(pair, out var tf) ? tf : Pairs.Add2(pair, new TFMap()); // Get the price data for the given time frame return(tf_map.TryGetValue(time_frame, out var pd) ? pd : tf_map.Add2(time_frame, new PriceData(pair, time_frame, Shutdown))); } }
/// <summary>Get the indicators associated with 'pair'</summary> public IReadOnlyList <IIndicator> this[TradePair pair] { get => Indicators.TryGetValue(pair.Name, out var indy) ? indy : (IReadOnlyList <IIndicator>) new IIndicator[0];
public OrderResult(TradePair pair, long order_id, bool filled) : this(pair, order_id, filled, null) { }
public OrderResult(TradePair pair, bool filled) : this(pair, 0, filled) { }
/// <summary> /// Consume offers up to 'price_q2b' or 'amount_base' (based on order type). /// 'pair' is the trade pair that this market depth data is associated with. /// Returns the offers that were consumed. 'amount_remaining' is what remains unfilled</summary> public IList <Offer> Consume(TradePair pair, ETradeType tt, EOrderType ot, Unit <decimal> price_q2b, Unit <decimal> amount_base, out Unit <decimal> remaining_base) { // Notes: // - 'remaining_base' should only be non zero if the order book is empty // - Handling 'dust' amounts: // If the amount to fill almost matches an offer, where the difference is an amount too small to trade, // the offer amount is adjusted to exactly match. The small difference is absorbed by the exchange. // - This function cannot use 'amount_in' + 'amount_out' parameters because the price to consume up to // is not necessarity 'amount_out/amount_in'. 'amount_in' may be the partial remaining amount of a trade. var order_book = this[tt]; remaining_base = amount_base; // Stop orders become market orders when the price reaches the stop level if (ot == EOrderType.Stop) { if (order_book.Count != 0 && tt.Sign() * price_q2b.CompareTo(order_book[0].PriceQ2B) <= 0) { ot = EOrderType.Market; } else { return(new List <Offer>()); } } var count = 0; var offers = order_book.Offers; foreach (var offer in offers) { // Price is too high/low to fill 'offer', stop. if (ot != EOrderType.Market && tt.Sign() * price_q2b.CompareTo(offer.PriceQ2B) < 0) { break; } var rem = remaining_base - offer.AmountBase; // The remaining amount is large enough to consider the next offer if (rem < pair.AmountRangeBase.Beg || rem * offer.PriceQ2B < pair.AmountRangeQuote.Beg) { break; } remaining_base = rem; ++count; } // Remove the orders that have been filled var consumed = offers.GetRange(0, count); offers.RemoveRange(0, count); // Remove any remaining amount from the top remaining offer (if the price is right) if (remaining_base != 0 && offers.Count != 0 && (ot == EOrderType.Market || tt.Sign() * price_q2b.CompareTo(offers[0].PriceQ2B) >= 0)) { var offer = offers[0]; var rem = offer.AmountBase - remaining_base; if (pair.AmountRangeBase.Contains(rem) && pair.AmountRangeQuote.Contains(rem * offer.PriceQ2B)) { offers[0] = new Offer(offers[0].PriceQ2B, rem); } else { offers.RemoveAt(0); } consumed.Add(new Offer(offer.PriceQ2B, remaining_base)); remaining_base -= remaining_base; } return(consumed); }
/// <summary>Return the market depth info for 'pair'</summary> public MarketDepth MarketDepthInternal(TradePair pair, int depth) { return(m_depth[pair]); }
/// <summary>Cancel an open trade</summary> protected override Task <bool> CancelOrderInternal(TradePair pair, long order_id, CancellationToken cancel) { throw new Exception("Cannot cancel trades on the CrossExchange"); }