Example #1
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));
            }
        }
Example #2
0
        /// <summary>Step the exchange</summary>
        public void Step()
        {
            // This function emulates the behaviour of the 'Exchange.UpdateThreadEntryPoint'
            // method and the operations that occur on the exchange.

            // Do nothing if not enabled
            if (!Exchange.Enabled)
            {
                return;
            }

            #region Update Market Data
            // Generate and copy the market depth data to each trade pair
            {
                // Update the order book for each pair
                foreach (var pair in Pairs)
                {
                    // Get the data source for 'pair'
                    var src = PriceData[pair, Sim.TimeFrame];
                    if (src == null || src.Count == 0)
                    {
                        continue;
                    }

                    // No market data if the source is empty
                    Debug.Assert(src.TimeFrame == Sim.TimeFrame);
                    var latest = src.Current;
                    if (latest == null)
                    {
                        continue;
                    }

                    // Generate market depth for 'pair', so that the spot price matches 'src.Current'
                    var md = GenerateMarketDepth(pair, latest, Sim.TimeFrame);

                    // Update the spot price and order book
                    pair.MarketDepth.UpdateOrderBooks(md.B2Q.ToArray(), md.Q2B.ToArray());
                    pair.SpotPrice[ETradeType.Q2B] = md.Q2B[0].PriceQ2B;
                    pair.SpotPrice[ETradeType.B2Q] = md.B2Q[0].PriceQ2B;

                    // Q2B => first price is the minimum, B2Q => first price is a maximum
                    Debug.Assert(pair.SpotPrice[ETradeType.Q2B] == ((decimal)latest.Close)._(pair.RateUnits));
                    Debug.Assert(pair.SpotPrice[ETradeType.B2Q] == ((decimal)latest.Close - (decimal)pair.Spread)._(pair.RateUnits));
                }

                // Notify updated
                Pairs.LastUpdated = Model.UtcNow;
            }
            #endregion

            #region Orders / History
            // Fill any orders that can be filled, update 'Orders' from 'm_ord', and 'History' from 'm_his'
            {
                // Try to fill orders
                foreach (var order in m_ord.Values.ToArray())
                {
                    // Try to fill 'position'.
                    // If 'his' is null, then the position can't be filled can remains unchanged
                    // If 'pos' is null, then the position is completely filled.
                    // 'order.OrderType' should have a value because the trade cannot be submitted without knowing the spot price
                    TryFillOrder(order.Pair, order.Fund, order.OrderId, order.TradeType, order.OrderType, order.AmountIn, order.AmountOut, order.RemainingIn, out var pos, out var his);
                    if (his == null)
                    {
                        continue;
                    }

                    // Stop the sim for 'RunToTrade' mode
                    if (Sim.RunMode == Simulation.ERunMode.RunToTrade)
                    {
                        Sim.Pause();
                    }

                    // If 'his' is not null, some or all of the position was filled.
                    ApplyToBalance(pos, his);

                    // Update the position store
                    if (pos != null)
                    {
                        m_ord[order.OrderId] = pos;
                    }
                    else
                    {
                        m_ord.Remove(order.OrderId);
                    }

                    // Update the history store
                    if (his != null)
                    {
                        var existing = m_his[order.OrderId]?.Trades;
                        if (existing != null)
                        {
                            foreach (var h in existing)
                            {
                                his.Trades.AddOrUpdate(h);
                            }
                        }
                        m_his[order.OrderId] = his;
                    }

                    // Sanity check that the partial trade isn't gaining or losing amount
                    Debug.Assert(Misc.EqlAmount(order.AmountBase, (his?.Trades.Sum(x => x.AmountBase) ?? 0m) + (pos?.RemainingBase ?? 0m)));
                }

                // This is equivalent to the 'DataUpdates' code in 'UpdateOrdersAndHistoryInternal'
                {
                    var orders    = new List <Order>();
                    var timestamp = Model.UtcNow;

                    // Update the trade history
                    var history_updates = m_his.Values.Where(x => x.Created.Ticks >= Exchange.HistoryInterval.End);
                    foreach (var exch_order in history_updates.SelectMany(x => x.Trades))
                    {
                        // Update the history of the completed orders
                        var fill = TradeCompletedFrom(exch_order, timestamp);
                        Exchange.AddToTradeHistory(fill);
                    }

                    // Update the collection of existing orders
                    foreach (var exch_order in m_ord.Values)
                    {
                        // Add the order to the collection
                        orders.Add2(OrderFrom(exch_order, timestamp));
                    }
                    Exchange.SynchroniseOrders(orders, timestamp);

                    // Notify updated. Notify history before positions so that orders don't "disappear" temporarily
                    History.LastUpdated = timestamp;
                    Orders.LastUpdated  = timestamp;
                }
            }
            #endregion

            #region Balance
            {
                // This is equivalent to the 'DataUpdates' code in 'UpdateBalancesInternal'
                {
                    var timestamp = Model.UtcNow;

                    foreach (var b in m_bal.Values)
                    {
                        // Find the currency that this balance is for
                        var coin = Coins.GetOrAdd(b.Coin.Symbol);

                        // Update the balance
                        Balance.ExchangeUpdate(coin, b.Total._(coin), b.Held._(coin), timestamp);
                    }

                    // Notify updated
                    Balance.LastUpdated = timestamp;
                }
            }
            #endregion
        }