// 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;
        }
Beispiel #2
0
 /// <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"));
 }
Beispiel #3
0
 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;
 }
Beispiel #4
0
 /// <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)));
             }
         }
     }
 }
Beispiel #5
0
 /// <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"));
 }
Beispiel #6
0
        // 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;
        }
Beispiel #7
0
        // 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);
        }
Beispiel #8
0
        public PairNames(TradePair pair)
        {
            if (pair == null)
            {
                throw new ArgumentNullException(nameof(pair));
            }

            Base  = pair.Base.Symbol;
            Quote = pair.Quote.Symbol;
        }
Beispiel #9
0
        /// <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);
        }
Beispiel #12
0
        // 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;
        }
Beispiel #13
0
        /// <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);
            }
        }
Beispiel #15
0
        /// <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);
        }
Beispiel #16
0
        /// <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);
            }
        }
Beispiel #17
0
        /// <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);
        }
Beispiel #18
0
        // 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;
            }
        }
Beispiel #19
0
        /// <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);
            }
        }
Beispiel #20
0
        /// <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));
            }
        }
Beispiel #21
0
        /// <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)));
            }
        }
Beispiel #22
0
 /// <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];
Beispiel #23
0
 public OrderResult(TradePair pair, long order_id, bool filled)
     : this(pair, order_id, filled, null)
 {
 }
Beispiel #24
0
 public OrderResult(TradePair pair, bool filled)
     : this(pair, 0, filled)
 {
 }
Beispiel #25
0
        /// <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);
        }
Beispiel #26
0
 /// <summary>Return the market depth info for 'pair'</summary>
 public MarketDepth MarketDepthInternal(TradePair pair, int depth)
 {
     return(m_depth[pair]);
 }
Beispiel #27
0
 /// <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");
 }