From 81d26aaadd1a23c78c7c42573e95e35c5ca8e1ac Mon Sep 17 00:00:00 2001 From: Casey Primozic Date: Wed, 11 Jan 2017 18:02:59 -0600 Subject: [PATCH] Created way to convert HashMaps into SimbrokerSettings - Created a FromHashmap trait - Created a procedural macro to derive it for structs --- backtester/src/sim_broker.rs | 2 - configurator/src/directory.rs | 13 ++++-- configurator/src/main.rs | 1 + configurator/src/misc.rs | 23 ++++++---- dev.sh | 7 +++ run.sh | 4 ++ util/Cargo.toml | 2 + util/from_hashmap/.gitignore | 2 + util/from_hashmap/Cargo.toml | 12 +++++ util/from_hashmap/src/lib.rs | 84 ++++++++++++++++++++++++++++++++++ util/src/lib.rs | 4 +- util/src/trading/broker/mod.rs | 64 -------------------------- util/src/trading/objects.rs | 43 +++++++++++++++++ 13 files changed, 182 insertions(+), 79 deletions(-) create mode 100644 util/from_hashmap/.gitignore create mode 100644 util/from_hashmap/Cargo.toml create mode 100644 util/from_hashmap/src/lib.rs diff --git a/backtester/src/sim_broker.rs b/backtester/src/sim_broker.rs index 381190f..ec7b190 100644 --- a/backtester/src/sim_broker.rs +++ b/backtester/src/sim_broker.rs @@ -6,8 +6,6 @@ use std::collections::hash_map::Entry; use std::sync::atomic::{Ordering, AtomicUsize}; use std::sync::{mpsc, Arc, Mutex}; use std::thread; -#[allow(unused_imports)] -use test; use futures::{oneshot, Oneshot}; use futures::stream::{BoxStream, Stream}; diff --git a/configurator/src/directory.rs b/configurator/src/directory.rs index a2b5c0c..d6821ed 100644 --- a/configurator/src/directory.rs +++ b/configurator/src/directory.rs @@ -67,7 +67,7 @@ pub fn show_directory(s: &mut Cursive, settings: Settings, needs_start: bool) { let last_page = PAGE_LIST[last_page_ix]; let changed = check_changes(s, last_page, settings__.clone()); if changed { - s.add_layer(get_save_dialog(0, 0, settings__.clone(), true)); + s.add_layer(get_save_dialog(last_page_ix, last_page_ix, settings__.clone(), true)); } else { directory_exit(s, settings__.clone()); } @@ -170,8 +170,13 @@ fn check_changes(s: &mut Cursive, page: &SettingsPage, settings: Settings) -> bo for row in page.iter() { let cur_val = get_by_id(row.id, s) .expect(&format!("Unable to get {} by id!", row.id)); - let last_val = settings.get(String::from(row.id)) - .expect(&format!("Unable to get past val in check_changes: {}", row.id)); + let last_val_opt = settings.get(String::from(row.id)); + let last_val; + if last_val_opt.is_none() { + last_val = String::from(row.default.expect(&format!("No past val for {} and no default!", row.id))); + } else { + last_val = last_val_opt.unwrap(); + } if last_val != *cur_val { return true } @@ -182,7 +187,7 @@ fn check_changes(s: &mut Cursive, page: &SettingsPage, settings: Settings) -> bo /// Commits all changes for a page to the internal Settings object and then writes them to all files. fn save_changes(s: &mut Cursive, page: &SettingsPage, settings: Settings) { for row in page.iter() { - let cur_val = get_by_id(row.id, s).unwrap(); + let cur_val = get_by_id(row.id, s).expect(&format!("Couldn't get value by id for: {}", row.id)); settings.set(row.id, &*cur_val); } diff --git a/configurator/src/main.rs b/configurator/src/main.rs index 495c221..d1be57d 100644 --- a/configurator/src/main.rs +++ b/configurator/src/main.rs @@ -187,6 +187,7 @@ fn redis_local(s: &mut Cursive, installed: bool, settings: Settings) { "redis://localhost:{}/", port )); + settings.set("redis_server_binary_path", &which("redis-server")); postgres_config(s, settings.clone()) }); port_box.set_content("6379"); diff --git a/configurator/src/misc.rs b/configurator/src/misc.rs index 20cb985..9031042 100644 --- a/configurator/src/misc.rs +++ b/configurator/src/misc.rs @@ -37,7 +37,7 @@ impl SettingRow { SettingType::String | SettingType::Usize | SettingType::Boolean => format!("\"{}\"", raw_val), SettingType::OptionString => match raw_val.as_str() { "" => String::from("null"), - _ => raw_val, + _ => format!("\"{}\"", raw_val), }, } } @@ -51,7 +51,7 @@ impl SettingRow { SettingType::Boolean => raw_val, // Assume it's in the right format. SettingType::OptionString => match raw_val.as_str() { "" => String::from("None"), - _ => format!("Some({})", raw_val), + _ => format!("Some(\"{}\")", raw_val), }, } } @@ -65,7 +65,7 @@ impl SettingRow { SettingType::Boolean => raw_val, // Assume it's in the right format. SettingType::OptionString => match raw_val.as_str() { "" => String::from("null"), - _ => raw_val, + _ => format!("\"{}\"", raw_val), }, } } @@ -367,7 +367,7 @@ pub const FXCM_SETTINGS: SettingsPage = SettingsPage { name: "URL", default: Some("http://www.fxcorporate.com/Hosts.jsp"), setting_type: SettingType::String, - comment: None, + comment: Some("Path to the `Hosts.jsp` file for the FXCM API."), }, SettingRow { id: "fxcm_pin", @@ -406,14 +406,14 @@ pub const GENERAL_SETTINGS: SettingsPage = SettingsPage { name: "Log Channel", default: Some("log"), setting_type: SettingType::String, - comment: None, + comment: Some("The redis pub/sub channel on which log messages will be sent."), }, SettingRow { id: "data_dir", name: "Data Directory", default: None, setting_type: SettingType::String, - comment: None, + comment: Some("Data directory for the platform where things like historical ticks and settings are stored."), }, SettingRow { id: "websocket_port", @@ -427,14 +427,21 @@ pub const GENERAL_SETTINGS: SettingsPage = SettingsPage { name: "MM Port", default: Some("8002"), setting_type: SettingType::Usize, - comment: Some("The port the MM will listen on."), + comment: Some("The port the MM web GUI will listen on."), }, SettingRow { id: "node_binary_path", name: "NodeJS Binary Path", default: None, setting_type: SettingType::String, - comment: None, + comment: Some("The absolute path to the `node` binary."), + }, + SettingRow { + id: "redis_server_binary_path", + name: "Redis Server Path", + default: Some(""), + setting_type: SettingType::OptionString, + comment: Some("The absolute path to the `redis-server` executable. Empty if Redis is installed remotely."), }, SettingRow { id: "logger_persistance_table", diff --git a/dev.sh b/dev.sh index 951c6e1..d452a60 100755 --- a/dev.sh +++ b/dev.sh @@ -1 +1,8 @@ +#!/bin/bash + +# This script symlinks the mm directory into the dist directory so that changes made to the +# mm directory during development will be reflected when running the platform. +# +# You only need to run this script if you're working on the MM. + make debug && make dev && ./run.sh diff --git a/run.sh b/run.sh index 0877087..78af69f 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,9 @@ #!/bin/bash +# This script starts the whole platform and is the only official way to start the platform. +# It initializes a spawner instance which in turn spawns a Logger and a MM instance. +# Once you run this script, you can view the MM web console in your web browser (default port 8002). + LD_LIBRARY_PATH="$(pwd)/dist/lib" export LD_LIBRARY_PATH cd dist && ./spawner diff --git a/util/Cargo.toml b/util/Cargo.toml index 03fd49d..0bef403 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -2,6 +2,7 @@ name = "algobot-util" version = "0.1.0" authors = ["Casey Primozic "] +description = "Code used in all of TickGrinder's modules." [dependencies] redis = "0.5.3" @@ -12,6 +13,7 @@ serde_json = "0.8.4" serde_derive = "0.8.21" uuid = { version = "0.3.1", features = ["serde", "v4",] } indoc = "^0.1" +from_hashmap = { path = "from_hashmap" } [lib] name = "algobot_util" diff --git a/util/from_hashmap/.gitignore b/util/from_hashmap/.gitignore new file mode 100644 index 0000000..3e2f2a8 --- /dev/null +++ b/util/from_hashmap/.gitignore @@ -0,0 +1,2 @@ +*.lock +target diff --git a/util/from_hashmap/Cargo.toml b/util/from_hashmap/Cargo.toml new file mode 100644 index 0000000..ec960bb --- /dev/null +++ b/util/from_hashmap/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "from_hashmap" +version = "0.1.0" +authors = ["Casey Primozic "] +description = "Defines a procedural macro to build a struct from a HashMap containing values for its fields" + +[dependencies] +syn = "0.10" +quote = "0.3" + +[lib] +proc-macro = true diff --git a/util/from_hashmap/src/lib.rs b/util/from_hashmap/src/lib.rs new file mode 100644 index 0000000..c69ab86 --- /dev/null +++ b/util/from_hashmap/src/lib.rs @@ -0,0 +1,84 @@ +//! Used to take a HashMap containing field:value pairs of a struct +//! and using it to create a new struct containing that data. + +#![recursion_limit = "128"] + +extern crate proc_macro; +extern crate syn; +#[macro_use] +extern crate quote; + +use proc_macro::TokenStream; +use syn::{Ident, VariantData}; + +#[proc_macro_derive(FromHashmap)] +pub fn from_hashmap(input: TokenStream) -> TokenStream { + let source = input.to_string(); + // Parse the string representation into a syntax tree + let ast = syn::parse_macro_input(&source).unwrap(); + + // create a vector containing the names of all fields on the struct + let idents: Vec = match ast.body { + syn::Body::Struct(vdata) => { + match vdata { + VariantData::Struct(fields) => { + let mut idents = Vec::new(); + for ref field in fields.iter() { + match &field.ident { + &Some(ref ident) => idents.push(ident.clone()), + &None => panic!("Your struct is missing a field identity!"), + } + } + idents + }, + VariantData::Tuple(_) | VariantData::Unit => { + panic!("You can only derive this for normal structs!"); + }, + } + }, + syn::Body::Enum(_) => panic!("You can only derive this on structs!"), + }; + + // contains quoted strings containing the struct fields in the same order as + // the vector of idents. + let mut keys = Vec::new(); + for ident in idents.iter() { + keys.push(String::from(ident.as_ref())); + } + + let name = &ast.ident; + let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + + let tokens = quote! { + /// Attempts to convert the given &str into a T, panicing if it's not successful + fn parse_pair(v: &str) -> T where T : ::std::str::FromStr { + let res = v.parse::(); + match res { + Ok(val) => val, + Err(_) => panic!(format!("Unable to convert given input into required type: {}", v)), + } + } + + impl #impl_generics FromHashmap<#name> for #name #ty_generics #where_clause { + fn from_hashmap(mut hm: ::std::collections::HashMap) -> #name { + // start with the default implementation + let mut settings = #name::default(); + #( + match hm.entry(String::from(#keys)) { + ::std::collections::hash_map::Entry::Occupied(occ_ent) => { + // set the corresponding struct field to the value in + // the corresponding hashmap if it contains it + settings.#idents = parse_pair(occ_ent.get().as_str()); + }, + ::std::collections::hash_map::Entry::Vacant(_) => (), + } + )* + + // return the modified struct + settings + } + } + }; + + tokens.parse().unwrap() +} diff --git a/util/src/lib.rs b/util/src/lib.rs index b2132a2..fb95eb4 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -1,6 +1,6 @@ //! Code shared by all modules of the platform -#![feature(custom_derive, plugin, conservative_impl_trait, test)] +#![feature(rustc_attrs, plugin, conservative_impl_trait, test)] extern crate redis; extern crate futures; @@ -11,6 +11,8 @@ extern crate serde_json; extern crate serde_derive; extern crate postgres; extern crate test; +#[macro_use] +extern crate from_hashmap; pub mod transport; pub mod strategies; diff --git a/util/src/trading/broker/mod.rs b/util/src/trading/broker/mod.rs index f40bca7..266b32e 100644 --- a/util/src/trading/broker/mod.rs +++ b/util/src/trading/broker/mod.rs @@ -43,67 +43,3 @@ pub type BrokerResult = Result; /// Utility type for a currently pending broker action pub type PendingResult = Receiver; - -// TODO: Move SimbrokerSettings out of here - -/// Settings for the simulated broker that determine things like trade fees, -/// estimated slippage, etc. -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -pub struct SimBrokerSettings { - pub starting_balance: f64, - /// how many microseconds ahead the broker is to the client. - pub ping_ns: usize, - /// how many us between when the broker receives an order and executes it. - pub execution_delay_us: usize, - /// the minimum size that a trade can be in cents of . - pub lot_size: usize, - pub leverage: usize, -} - -impl SimBrokerSettings { - /// Creates a default SimBrokerSettings used for tests - pub fn default() -> SimBrokerSettings { - SimBrokerSettings { - starting_balance: 1f64, - ping_ns: 0, - execution_delay_us: 0usize, - lot_size: 100000, - leverage: 50, - } - } - - /// Parses a String:String hashmap into a SimBrokerSettings object. - pub fn from_hashmap(hm: HashMap) -> Result { - let mut settings = SimBrokerSettings::default(); - - // TODO: change to use Configurator settings. - - for (k, v) in hm.iter() { - match k.as_str() { - "starting_balance" => { - let res = v.parse::(); - if res.is_err() { - return Err(SimBrokerSettings::kv_parse_error(k, v)) - } - settings.starting_balance = res.unwrap(); - }, - "ping_ms" => { - let res = v.parse::(); - if res.is_err() { - return Err(SimBrokerSettings::kv_parse_error(k, v)) - }; - settings.ping_ns = res.unwrap(); - }, - _ => (), - } - } - - Ok(settings) - } - - fn kv_parse_error(k: &String, v: &String) -> BrokerError { - return BrokerError::Message{ - message: format!("Unable to parse K:V pair: {}:{}", k, v) - } - } -} diff --git a/util/src/trading/objects.rs b/util/src/trading/objects.rs index 62b5065..fc1dda8 100644 --- a/util/src/trading/objects.rs +++ b/util/src/trading/objects.rs @@ -204,3 +204,46 @@ impl Position { None } } + +/// Returns a struct given the struct's field:value pairs in a HashMap. If +/// the provided HashMap doesn't contain a field, then the default is used. +pub trait FromHashmap : Default { + fn from_hashmap(hm: HashMap) -> T; +} + +/// Settings for the simulated broker that determine things like trade fees, +/// estimated slippage, etc. +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +// procedural macro is defined in the `from_hashmap` crate found in the util +// directory's root. +#[derive(FromHashmap)] +pub struct SimBrokerSettings { + pub starting_balance: f64, + /// how many microseconds ahead the broker is to the client. + pub ping_ns: usize, + /// how many us between when the broker receives an order and executes it. + pub execution_delay_us: usize, + /// the minimum size that a trade can be in cents of . + pub lot_size: usize, + pub leverage: usize, +} + +impl Default for SimBrokerSettings { + fn default() -> SimBrokerSettings { + SimBrokerSettings { + starting_balance: 1f64, + ping_ns: 0, + execution_delay_us: 0usize, + lot_size: 1000, + leverage: 50, + } + } +} + +#[test] +fn simbroker_settings_hashmap_population() { + let mut hm = HashMap::new(); + hm.insert(String::from("ping_ns"), String::from("2000")); + let settings = SimBrokerSettings::from_hashmap(hm); + assert_eq!(settings.ping_ns, 2000); +}