/// <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>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 }