From f7bfcea355b77cd07d0eee0963e8dea0935d3dbc Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Thu, 7 Jan 2016 14:10:04 -0500 Subject: [PATCH] Implement new market API #503 --- libraries/app/api.cpp | 7 +- libraries/app/database_api.cpp | 282 +++++++++++++++++- .../app/include/graphene/app/database_api.hpp | 88 +++++- 3 files changed, 361 insertions(+), 16 deletions(-) diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index dfc43420..875edb7c 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -339,13 +339,12 @@ namespace graphene { namespace app { uint32_t count = 0; auto itr = history_idx.lower_bound( hkey ); vector result; - while( itr != history_idx.end() ) + while( itr != history_idx.end() && count < limit) { if( itr->key.base != a || itr->key.quote != b ) break; result.push_back( *itr ); ++itr; ++count; - if( count > limit ) break; } return result; @@ -386,7 +385,7 @@ namespace graphene { namespace app { FC_ASSERT(_app.chain_database()); const auto& db = *_app.chain_database(); vector result; - result.reserve(100); + result.reserve(200); if( a > b ) std::swap(a,b); @@ -394,7 +393,7 @@ namespace graphene { namespace app { const auto& by_key_idx = bidx.indices().get(); auto itr = by_key_idx.lower_bound( bucket_key( a, b, bucket_seconds, start ) ); - while( itr != by_key_idx.end() && itr->key.open <= end && result.size() < 100 ) + while( itr != by_key_idx.end() && itr->key.open <= end && result.size() < 200 ) { if( !(itr->key.base == a && itr->key.quote == b && itr->key.seconds == bucket_seconds) ) { diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index 3d668fa1..0eb493b7 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -31,18 +31,22 @@ #include +#include +#include + #define GET_REQUIRED_FEES_MAX_RECURSION 4 namespace graphene { namespace app { class database_api_impl; + class database_api_impl : public std::enable_shared_from_this { public: database_api_impl( graphene::chain::database& db ); ~database_api_impl(); - + // Objects fc::variants get_objects(const vector& ids)const; @@ -85,16 +89,20 @@ class database_api_impl : public std::enable_shared_from_this // Assets vector> get_assets(const vector& asset_ids)const; - vector list_assets(const string& lower_bound_symbol, uint32_t limit)const; + vector list_assets(const string& lower_bound_symbol, uint32_t limit)const; vector> lookup_asset_symbols(const vector& symbols_or_ids)const; // Markets / feeds - vector get_limit_orders(asset_id_type a, asset_id_type b, uint32_t limit)const; - vector get_call_orders(asset_id_type a, uint32_t limit)const; - vector get_settle_orders(asset_id_type a, uint32_t limit)const; - vector get_margin_positions( const account_id_type& id )const; + vector get_limit_orders(asset_id_type a, asset_id_type b, uint32_t limit)const; + vector get_call_orders(asset_id_type a, uint32_t limit)const; + vector get_settle_orders(asset_id_type a, uint32_t limit)const; + vector get_margin_positions( const account_id_type& id )const; void subscribe_to_market(std::function callback, asset_id_type a, asset_id_type b); void unsubscribe_from_market(asset_id_type a, asset_id_type b); + market_ticker get_ticker( const string& base, const string& quote )const; + market_volume get_24_volume( const string& base, const string& quote )const; + order_book get_order_book( const string& base, const string& quote, unsigned limit = 50 )const; + vector get_trade_history( const string& base, const string& quote, fc::time_point_sec start, fc::time_point_sec stop, unsigned limit = 100 )const; // Witnesses vector> get_witnesses(const vector& witness_ids)const; @@ -192,7 +200,7 @@ database_api_impl::database_api_impl( graphene::chain::database& db ):_db(db) }); _applied_block_connection = _db.applied_block.connect([this](const signed_block&){ on_applied_block(); }); - _pending_trx_connection = _db.on_pending_transaction.connect([this](const signed_transaction& trx ){ + _pending_trx_connection = _db.on_pending_transaction.connect([this](const signed_transaction& trx ){ if( _pending_trx_callback ) _pending_trx_callback( fc::variant(trx) ); }); } @@ -458,7 +466,7 @@ vector> database_api_impl::get_key_references( vectorget_ticker( base, quote ); +} + +market_ticker database_api_impl::get_ticker( const string& base, const string& quote )const +{ + auto assets = lookup_asset_symbols( {base, quote} ); + FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); + FC_ASSERT( assets[1], "Invalid quote asset symbol: ${s}", ("s",quote) ); + + auto base_id = assets[0]->id; + auto quote_id = assets[1]->id; + + market_ticker result; + + result.base = base; + result.quote = quote; + result.base_volume = 0; + result.quote_volume = 0; + result.percent_change = 0; + result.lowest_ask = 0; + result.highest_bid = 0; + + auto price_to_real = [&]( const long a, int p ) { return double( a ) / pow( 10, p ); }; + + try { + if( base_id > quote_id ) std::swap(base_id, quote_id); + + const auto& bidx = _db.get_index_type(); + const auto& by_key_idx = bidx.indices().get(); + uint32_t bucket_size = 86400; + auto now = fc::time_point_sec( fc::time_point::now() ); + + auto itr = by_key_idx.lower_bound( bucket_key( base_id, quote_id, bucket_size, + now - bucket_size ) ); + + if( itr != by_key_idx.end() && itr->key.base == base_id && itr->key.quote == quote_id && itr->key.seconds == bucket_size ) + { + auto trades = get_trade_history( base, quote, now, fc::time_point_sec( now.sec_since_epoch() - bucket_size ), 100 ); + + if (assets[0]->id == base_id) + { + result.percent_change = ( ( price_to_real( itr->close_quote.value, assets[1]->precision ) / price_to_real( itr->close_base.value, assets[0]->precision ) ) + / ( price_to_real( itr->open_quote.value, assets[1]->precision ) / price_to_real( itr->open_base.value, assets[0]->precision ) ) - 1 ) * 100; + result.lowest_ask = (double) itr->low_quote.value / (double) itr->low_base.value; + result.highest_bid = (double) itr->high_quote.value / (double) itr->high_base.value; + result.latest = trades[0].price; + } + else + { + result.percent_change = ( ( price_to_real( itr->close_base.value, assets[1]->precision ) / price_to_real( itr->close_quote.value, assets[0]->precision ) ) + / ( price_to_real( itr->open_base.value, assets[1]->precision ) / price_to_real( itr->open_quote.value, assets[0]->precision ) ) - 1) * 100; + result.lowest_ask = (double) itr->low_base.value / (double) itr->low_quote.value; + result.highest_bid = (double) itr->high_base.value / (double) itr->high_quote.value; + result.latest = trades[0].price; + } + + for ( market_trade t: trades ) + { + result.base_volume += t.amount; + result.quote_volume += t.value; + } + + while (trades.size() == 100) + { + for ( market_trade t: trades ) + { + result.base_volume += t.amount; + result.quote_volume += t.value; + } + + trades = get_trade_history( base, quote, trades[99].date, fc::time_point_sec( now.sec_since_epoch() - bucket_size ), 100 ); + } + } + + return result; + } FC_CAPTURE_AND_RETHROW( (base)(quote) ) +} + +market_volume database_api::get_24_volume( const string& base, const string& quote )const +{ + return my->get_24_volume( base, quote ); +} + +market_volume database_api_impl::get_24_volume( const string& base, const string& quote )const +{ + auto assets = lookup_asset_symbols( {base, quote} ); + FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); + FC_ASSERT( assets[1], "Invalid quote asset symbol: ${s}", ("s",quote) ); + + auto base_id = assets[0]->id; + auto quote_id = assets[1]->id; + + market_volume result; + result.base = base; + result.quote = quote; + result.base_volume = 0; + result.quote_volume = 0; + + try { + if( base_id > quote_id ) std::swap(base_id, quote_id); + + uint32_t bucket_size = 86400; + auto now = fc::time_point_sec( fc::time_point::now() ); + + auto trades = get_trade_history( base, quote, now, fc::time_point_sec( now.sec_since_epoch() - bucket_size ), 100 ); + + for ( market_trade t: trades ) + { + result.base_volume += t.amount; + result.quote_volume += t.value; + } + + while (trades.size() == 100) + { + for ( market_trade t: trades ) + { + result.base_volume += t.amount; + result.quote_volume += t.value; + } + + trades = get_trade_history( base, quote, trades[99].date, fc::time_point_sec( now.sec_since_epoch() - bucket_size ), 100 ); + } + + return result; + } FC_CAPTURE_AND_RETHROW( (base)(quote) ) +} + +order_book database_api::get_order_book( const string& base, const string& quote, unsigned limit )const +{ + return my->get_order_book( base, quote, limit); +} + +order_book database_api_impl::get_order_book( const string& base, const string& quote, unsigned limit )const +{ + FC_ASSERT( limit <= 50 ); + + order_book result; + result.base = base; + result.quote = quote; + + auto assets = lookup_asset_symbols( {base, quote} ); + FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); + FC_ASSERT( assets[1], "Invalid quote asset symbol: ${s}", ("s",quote) ); + + auto base_id = assets[0]->id; + auto quote_id = assets[1]->id; + auto orders = get_limit_orders( base_id, quote_id, limit ); + + + auto asset_to_real = [&]( const asset& a, int p ) { return double(a.amount.value)/pow( 10, p ); }; + auto price_to_real = [&]( const price& p ) + { + if( p.base.asset_id == base_id ) + return asset_to_real( p.quote, assets[1]->precision ) / asset_to_real( p.base, assets[0]->precision ); + else + return asset_to_real( p.base, assets[1]->precision ) / asset_to_real( p.quote, assets[0]->precision ); + }; + + for( const auto& o : orders ) { + if( o.sell_price.base.asset_id == base_id ) + { + result.asks.push_back( std::make_pair( price_to_real(o.sell_price), + asset_to_real(o.sell_price.base, assets[0]->precision)) ); + } + else + { + result.bids.push_back( std::make_pair( price_to_real(o.sell_price), + asset_to_real(o.sell_price.quote, assets[0]->precision ) ) ); + } + } + + return result; +} + +vector database_api::get_trade_history( const string& base, + const string& quote, + fc::time_point_sec start, + fc::time_point_sec stop, + unsigned limit )const +{ + return my->get_trade_history( base, quote, start, stop, limit ); +} + +vector database_api_impl::get_trade_history( const string& base, + const string& quote, + fc::time_point_sec start, + fc::time_point_sec stop, + unsigned limit )const +{ + FC_ASSERT( limit <= 100 ); + + auto assets = lookup_asset_symbols( {base, quote} ); + FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); + FC_ASSERT( assets[1], "Invalid quote asset symbol: ${s}", ("s",quote) ); + + auto base_id = assets[0]->id; + auto quote_id = assets[1]->id; + + if( base_id > quote_id ) std::swap( base_id, quote_id ); + const auto& history_idx = _db.get_index_type().indices().get(); + history_key hkey; + hkey.base = base_id; + hkey.quote = quote_id; + hkey.sequence = std::numeric_limits::min(); + + auto price_to_real = [&]( const long long a, int p ) { return double( a ) / pow( 10, p ); }; + + if ( start.sec_since_epoch() == 0 ) + start = fc::time_point_sec( fc::time_point::now() ); + + uint32_t count = 0; + auto itr = history_idx.lower_bound( hkey ); + vector result; + + while( itr != history_idx.end() && count < limit && !( itr->key.base != base_id || itr->key.quote != quote_id || itr->time < stop ) ) + { + if( itr->time < start ) + { + market_trade trade; + + if( base_id == itr->op.receives.asset_id ) + { + trade.amount = price_to_real( itr->op.receives.amount.value, assets[0]->precision ); + trade.value = price_to_real( itr->op.pays.amount.value, assets[1]->precision ); + } + else + { + trade.amount = price_to_real( itr->op.pays.amount.value, assets[0]->precision ); + trade.value = price_to_real( itr->op.receives.amount.value, assets[1]->precision ); + } + + trade.date = itr->time; + trade.price = trade.value / trade.amount; + + result.push_back( trade ); + ++count; + } + + // Trades are tracked in each direction. + ++itr; + ++itr; + } + + return result; +} + ////////////////////////////////////////////////////////////////////// // // // Witnesses // @@ -1497,7 +1753,7 @@ vector database_api_impl::get_blinded_balances( const fl ////////////////////////////////////////////////////////////////////// // // // Private methods // -// // +// // ////////////////////////////////////////////////////////////////////// void database_api_impl::broadcast_updates( const vector& updates ) @@ -1574,7 +1830,7 @@ void database_api_impl::on_objects_changed(const vector& ids) if( _market_subscriptions.size() ) { - if( !_subscribe_callback ) + if( !_subscribe_callback ) obj = _db.find_object( id ); if( obj ) { @@ -1625,8 +1881,12 @@ void database_api_impl::on_applied_block() const auto& ops = _db.get_applied_operations(); map< std::pair, vector> > subscribed_markets_ops; - for(const auto& op : ops) + for(const optional< operation_history_object >& o_op : ops) { + if( !o_op.valid() ) + continue; + const operation_history_object& op = *o_op; + std::pair market; switch(op.op.which()) { diff --git a/libraries/app/include/graphene/app/database_api.hpp b/libraries/app/include/graphene/app/database_api.hpp index e543819a..6baff9db 100644 --- a/libraries/app/include/graphene/app/database_api.hpp +++ b/libraries/app/include/graphene/app/database_api.hpp @@ -38,6 +38,8 @@ #include #include +#include + #include #include #include @@ -54,10 +56,47 @@ namespace graphene { namespace app { using namespace graphene::chain; +using namespace graphene::market_history; using namespace std; class database_api_impl; +struct order_book +{ + string base; + string quote; + vector< pair > bids; + vector< pair > asks; +}; + +struct market_ticker +{ + string base; + string quote; + double latest; + double lowest_ask; + double highest_bid; + double percent_change; + double base_volume; + double quote_volume; +}; + +struct market_volume +{ + string base; + string quote; + double base_volume; + double quote_volume; +}; + +struct market_trade +{ + fc::time_point_sec date; + double price; + double amount; + double value; +}; + /** * @brief The database_api class implements the RPC API for the chain database. * @@ -321,7 +360,46 @@ class database_api * @param a First asset ID * @param b Second asset ID */ - void unsubscribe_from_market(asset_id_type a, asset_id_type b); + void unsubscribe_from_market( asset_id_type a, asset_id_type b ); + + /** + * @brief Returns the ticker for the market assetA:assetB + * @param a String name of the first asset + * @param b String name of the second asset + * @return The market ticker for the past 24 hours. + */ + market_ticker get_ticker( const string& base, const string& quote )const; + + /** + * @brief Returns the 24 hour volume for the market assetA:assetB + * @param a String name of the first asset + * @param b String name of the second asset + * @return The market volume over the past 24 hours + */ + market_volume get_24_volume( const string& base, const string& quote )const; + + /** + * @brief Returns the order book for the market base:quote + * @param base String name of the first asset + * @param quote String name of the second asset + * @param depth of the order book. Up to depth of each asks and bids, capped at 50. Prioritizes most moderate of each + * @return Order book of the market + */ + order_book get_order_book( const string& base, const string& quote, unsigned limit = 50 )const; + + /** + * @brief Returns recent trades for the market assetA:assetB + * Note: Currentlt, timezone offsets are not supported. The time must be UTC. + * @param a String name of the first asset + * @param b String name of the second asset + * @param stop Stop time as a UNIX timestamp + * @param limit Number of trasactions to retrieve, capped at 100 + * @param start Start time as a UNIX timestamp + * @return Recent transactions in the market + */ + vector get_trade_history( const string& base, const string& quote, fc::time_point_sec start, fc::time_point_sec stop, unsigned limit = 100 )const; + + /////////////// // Witnesses // @@ -472,6 +550,10 @@ class database_api }; } } +FC_REFLECT( graphene::app::order_book, (base)(quote)(bids)(asks) ); +FC_REFLECT( graphene::app::market_ticker, (base)(quote)(latest)(lowest_ask)(highest_bid)(percent_change)(base_volume)(quote_volume) ); +FC_REFLECT( graphene::app::market_volume, (base)(quote)(base_volume)(quote_volume) ); +FC_REFLECT( graphene::app::market_trade, (date)(price)(amount)(value) ); FC_API(graphene::app::database_api, // Objects @@ -521,12 +603,16 @@ FC_API(graphene::app::database_api, (lookup_asset_symbols) // Markets / feeds + (get_order_book) (get_limit_orders) (get_call_orders) (get_settle_orders) (get_margin_positions) (subscribe_to_market) (unsubscribe_from_market) + (get_ticker) + (get_24_volume) + (get_trade_history) // Witnesses (get_witnesses)