// 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: // - 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>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)); } }