diff --git a/libraries/app/include/graphene/app/plugin.hpp b/libraries/app/include/graphene/app/plugin.hpp index 5065679d..103cd441 100644 --- a/libraries/app/include/graphene/app/plugin.hpp +++ b/libraries/app/include/graphene/app/plugin.hpp @@ -104,9 +104,9 @@ class plugin : public abstract_plugin bpo::options_description& config_file_options ) override; - protected: chain::database& database() { return *app().chain_database(); } application& app()const { assert(_app); return *_app; } + protected: net::node& p2p_node() { return *app().p2p_node(); } private: diff --git a/libraries/plugins/CMakeLists.txt b/libraries/plugins/CMakeLists.txt index 5f55e32b..a8bf2c23 100644 --- a/libraries/plugins/CMakeLists.txt +++ b/libraries/plugins/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory( witness ) add_subdirectory( account_history ) +add_subdirectory( market_history ) diff --git a/libraries/plugins/account_history/include/graphene/account_history/account_history_plugin.hpp b/libraries/plugins/account_history/include/graphene/account_history/account_history_plugin.hpp index 43b5c1f2..f0bdc464 100644 --- a/libraries/plugins/account_history/include/graphene/account_history/account_history_plugin.hpp +++ b/libraries/plugins/account_history/include/graphene/account_history/account_history_plugin.hpp @@ -42,7 +42,8 @@ namespace bpo = boost::program_options; enum account_history_object_type { - key_account_object_type + key_account_object_type = 0, + bucket_object_type = 1 ///< used in market_history_plugin }; class key_account_object : public abstract_object diff --git a/libraries/plugins/market_history/CMakeLists.txt b/libraries/plugins/market_history/CMakeLists.txt new file mode 100644 index 00000000..6394ba66 --- /dev/null +++ b/libraries/plugins/market_history/CMakeLists.txt @@ -0,0 +1,13 @@ +file(GLOB HEADERS "include/graphene/market_history/*.hpp") + +add_library( graphene_market_history + market_history_plugin.cpp + ) + +target_link_libraries( graphene_market_history graphene_chain graphene_app ) +target_include_directories( graphene_market_history + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) + +if(MSVC) + set_source_files_properties( market_history_plugin.cpp PROPERTIES COMPILE_FLAGS "/bigobj" ) +endif(MSVC) diff --git a/libraries/plugins/market_history/include/graphene/market_history/market_history_plugin.hpp b/libraries/plugins/market_history/include/graphene/market_history/market_history_plugin.hpp new file mode 100644 index 00000000..3c146444 --- /dev/null +++ b/libraries/plugins/market_history/include/graphene/market_history/market_history_plugin.hpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2015, Cryptonomex, Inc. + * All rights reserved. + * + * This source code is provided for evaluation in private test networks only, until September 8, 2015. After this date, this license expires and + * the code may not be used, modified or distributed for any purpose. Redistribution and use in source and binary forms, with or without modification, + * are permitted until September 8, 2015, provided that the following conditions are met: + * + * 1. The code and/or derivative works are used only for private test networks consisting of no more than 10 P2P nodes. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#pragma once + +#include +#include + +#include + +namespace graphene { namespace market_history { +using namespace chain; +namespace bpo = boost::program_options; + +// +// Plugins should #define their SPACE_ID's so plugins with +// conflicting SPACE_ID assignments can be compiled into the +// same binary (by simply re-assigning some of the conflicting #defined +// SPACE_ID's in a build script). +// +// Assignment of SPACE_ID's cannot be done at run-time because +// various template automagic depends on them being known at compile +// time. +// +#ifndef ACCOUNT_HISTORY_SPACE_ID +#define ACCOUNT_HISTORY_SPACE_ID 5 +#endif + +struct bucket_key +{ + asset_id_type base; + asset_id_type quote; + uint32_t seconds = 0; + fc::time_point_sec open; + + friend bool operator < ( const bucket_key& a, const bucket_key& b ) + { + return std::tie( a.base, a.quote, b.seconds, a.open ) < std::tie( b.base, b.quote, b.seconds, b.open ); + } + friend bool operator == ( const bucket_key& a, const bucket_key& b ) + { + return std::tie( a.base, a.quote, b.seconds, a.open ) == std::tie( b.base, b.quote, b.seconds, b.open ); + } +}; + +struct bucket_object : public abstract_object +{ + static const uint8_t space_id = ACCOUNT_HISTORY_SPACE_ID; + static const uint8_t type_id = 1; // market_history_plugin type, referenced from account_history_plugin.hpp + + price high()const { return asset( high_base, key.base ) / asset( high_quote, key.quote ); } + price low()const { return asset( low_base, key.base ) / asset( low_quote, key.quote ); } + + bucket_key key; + share_type high_base; + share_type high_quote; + share_type low_base; + share_type low_quote; + share_type open_base; + share_type open_quote; + share_type close_base; + share_type close_quote; + share_type base_volume; + share_type quote_volume; +}; + +struct by_key; +typedef multi_index_container< + bucket_object, + indexed_by< + hashed_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, member< bucket_object, bucket_key, &bucket_object::key > > + > +> bucket_object_multi_index_type; + +typedef generic_index bucket_index; + + +namespace detail +{ + class market_history_plugin_impl; +} + +/** + * The market history plugin can be configured to track any number of intervals via its configuration. Once per block it + * will scan the virtual operations and look for fill_order_operations and then adjust the appropriate bucket objects for + * each fill order. + */ +class market_history_plugin : public graphene::app::plugin +{ + public: + market_history_plugin(); + virtual ~market_history_plugin(); + + std::string plugin_name()const override; + virtual void plugin_set_program_options(bpo::options_description& cli, bpo::options_description& cfg) override; + virtual void plugin_initialize(const bpo::variables_map& options) override; + virtual void plugin_startup() override; + + vector get_history( const bucket_key& start, const bucket_key& end )const; + const flat_set& tracked_buckets()const; + + private: + friend class detail::market_history_plugin_impl; + std::unique_ptr my; +}; + +} } //graphene::market_history + +FC_REFLECT( graphene::market_history::bucket_key, (base)(quote)(seconds)(open) ) +FC_REFLECT( graphene::market_history::bucket_object, (key) + (high_base)(high_quote) + (low_base)(low_quote) + (open_base)(open_quote) + (close_base)(close_quote) + (base_volume)(quote_volume) ) + diff --git a/libraries/plugins/market_history/market_history_plugin.cpp b/libraries/plugins/market_history/market_history_plugin.cpp new file mode 100644 index 00000000..d6dbd168 --- /dev/null +++ b/libraries/plugins/market_history/market_history_plugin.cpp @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2015, Cryptonomex, Inc. + * All rights reserved. + * + * This source code is provided for evaluation in private test networks only, until September 8, 2015. After this date, this license expires and + * the code may not be used, modified or distributed for any purpose. Redistribution and use in source and binary forms, with or without modification, + * are permitted until September 8, 2015, provided that the following conditions are met: + * + * 1. The code and/or derivative works are used only for private test networks consisting of no more than 10 P2P nodes. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace graphene { namespace market_history { + +namespace detail +{ + +class market_history_plugin_impl +{ + public: + market_history_plugin_impl(market_history_plugin& _plugin) + :_self( _plugin ) {} + virtual ~market_history_plugin_impl(); + + /** this method is called as a callback after a block is applied + * and will process/index all operations that were applied in the block. + */ + void update_market_histories( const signed_block& b ); + + graphene::chain::database& database() + { + return _self.database(); + } + + market_history_plugin& _self; + flat_set _tracked_buckets; +}; + + +struct operation_process_fill_order +{ + market_history_plugin& _plugin; + fc::time_point_sec _now; + + operation_process_fill_order( market_history_plugin& mhp, fc::time_point_sec n ) + :_plugin(mhp),_now(n) {} + + typedef void result_type; + + /** do nothing for other operation types */ + template + void operator()( const T& )const{} + + void operator()( const fill_order_operation& o )const + { + const auto& buckets = _plugin.tracked_buckets(); + auto& db = _plugin.database(); + const auto& bucket_idx = db.get_index_type(); + + for( auto bucket : buckets ) + { + bucket_key key; + key.base = o.pays.asset_id; + key.quote = o.receives.asset_id; + price trade_price = o.pays / o.receives; + if( key.base > key.quote ) + { + std::swap( key.base,key.quote ); + trade_price = ~trade_price; + } + + key.seconds = bucket; + key.open = fc::time_point() + fc::seconds((_now.sec_since_epoch() / key.seconds) * key.seconds); + + const auto& by_key_idx = bucket_idx.indices().get(); + auto itr = by_key_idx.find( key ); + if( itr == by_key_idx.end() ) + { // create new bucket + db.create( [&]( bucket_object& b ){ + b.key = key; + b.quote_volume += trade_price.quote.amount; + b.open_base = trade_price.base.amount; + b.open_quote = trade_price.quote.amount; + b.close_base = trade_price.base.amount; + b.close_quote = trade_price.quote.amount; + b.high_base = b.close_base; + b.high_quote = b.close_quote; + b.low_base = b.close_base; + b.low_quote = b.close_quote; + }); + } + else + { // update existing bucket + db.modify( *itr, [&]( bucket_object& b ){ + b.base_volume += trade_price.base.amount; + b.quote_volume += trade_price.quote.amount; + b.close_base = trade_price.base.amount; + b.close_quote = trade_price.quote.amount; + if( b.high() < trade_price ) + { + b.high_base = b.close_base; + b.high_quote = b.close_quote; + } + if( b.low() > trade_price ) + { + b.low_base = b.close_base; + b.low_quote = b.close_quote; + } + }); + } + } + } +}; + +market_history_plugin_impl::~market_history_plugin_impl() +{} + +void market_history_plugin_impl::update_market_histories( const signed_block& b ) +{ + graphene::chain::database& db = database(); + const vector& hist = db.get_applied_operations(); + for( auto op : hist ) + op.op.visit( operation_process_fill_order( _self, b.timestamp ) ); +} + +} // end namespace detail + + + + + + +market_history_plugin::market_history_plugin() : + my( new detail::market_history_plugin_impl(*this) ) +{ +} + +market_history_plugin::~market_history_plugin() +{ +} + +std::string market_history_plugin::plugin_name()const +{ + return "market_history"; +} + +void market_history_plugin::plugin_set_program_options( + boost::program_options::options_description& cli, + boost::program_options::options_description& cfg + ) +{ + cli.add_options() + ("bucket-size", bpo::value>()->composing()->multitoken(), "Bucket size in seconds to track history for (may specify multiple times)") + ; + cfg.add(cli); +} + +void market_history_plugin::plugin_initialize(const boost::program_options::variables_map& options) +{ + database().applied_block.connect( [&]( const signed_block& b){ my->update_market_histories(b); } ); + database().add_index< primary_index< bucket_index > >(); + + LOAD_VALUE_SET(options, "bucket-size", my->_tracked_buckets, uint32_t); +} + +void market_history_plugin::plugin_startup() +{ +} + +const flat_set& market_history_plugin::tracked_buckets() const +{ + return my->_tracked_buckets; +} + +} }