You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

239 lines
7.3KB

  1. #![allow(unused_imports)]
  2. #[macro_use]
  3. extern crate slog;
  4. #[macro_use]
  5. extern crate markets;
  6. use std::path::PathBuf;
  7. use std::time::*;
  8. use std::io::{self, prelude::*};
  9. use std::fs;
  10. use structopt::StructOpt;
  11. use serde::{Serialize, Deserialize};
  12. use slog::Drain;
  13. use pretty_toa::ThousandsSep;
  14. use markets::crypto::{Exchange, Ticker, Side};
  15. // equivalent to panic! but without the ugly 'thread main panicked' yada yada
  16. macro_rules! fatal { ($fmt:expr, $($args:tt)*) => {{
  17. eprintln!($fmt, $($args)*);
  18. std::process::exit(1);
  19. }}}
  20. const PROGRESS_EVERY: usize = 1024 * 1024;
  21. #[derive(Debug, StructOpt)]
  22. struct Opt {
  23. /// Path to CSV file with trades data
  24. #[structopt(short = "f", long = "trades-csv")]
  25. #[structopt(parse(from_os_str))]
  26. trades_csv: PathBuf,
  27. /// Where to save the query results (CSV output)
  28. #[structopt(short = "o", long = "output-path")]
  29. #[structopt(parse(from_os_str))]
  30. output_path: PathBuf,
  31. }
  32. #[derive(Deserialize)]
  33. struct Trade {
  34. /// Unix nanoseconds
  35. pub time: u64,
  36. pub exch: Exchange,
  37. pub ticker: Ticker,
  38. //pub side: Option<Side>,
  39. pub price: f64,
  40. pub amount: f64,
  41. }
  42. /*
  43. struct HourSummary {
  44. pub n_trades: usize,
  45. pub
  46. */
  47. fn per_sec(n: usize, span: Duration) -> f64 {
  48. if n == 0 || span < Duration::from_micros(1) { return 0.0 }
  49. let s: f64 = span.as_nanos() as f64 / 1e9f64;
  50. n as f64 / s
  51. }
  52. #[inline(always)]
  53. fn manual_deserialize_bytes(row: &csv::ByteRecord) -> Result<Trade, &'static str> {
  54. let time: u64 = atoi::atoi(row.get(0).ok_or("no time")?)
  55. .ok_or("parsing time failed")?;
  56. let amount: f64 = lexical::parse(row.get(1).ok_or("no amount")?)
  57. .map_err(|_| "parsing amount failed")?;
  58. let exch = match row.get(2).ok_or("no exch")? {
  59. b"bmex" => e!(bmex),
  60. b"bnce" => e!(bnce),
  61. b"btfx" => e!(btfx),
  62. b"gdax" => e!(gdax),
  63. b"okex" => e!(okex),
  64. b"bits" => e!(bits),
  65. b"plnx" => e!(plnx),
  66. b"krkn" => e!(krkn),
  67. _ => return Err("illegal exch"),
  68. };
  69. let price: f64 = lexical::parse(row.get(3).ok_or("no price")?)
  70. .map_err(|_| "parsing price failed")?;
  71. let ticker = match row.get(6).ok_or("no ticker")? {
  72. b"btc_usd" => t!(btc-usd),
  73. b"eth_usd" => t!(eth-usd),
  74. b"ltc_usd" => t!(ltc-usd),
  75. b"etc_usd" => t!(etc-usd),
  76. b"bch_usd" => t!(bch-usd),
  77. b"xmr_usd" => t!(xmr-usd),
  78. b"usdt_usd" => t!(usdt-usd),
  79. _ => return Err("illegal ticker"),
  80. };
  81. Ok(Trade { time, amount, exch, price, ticker })
  82. }
  83. #[inline(always)]
  84. fn manual_deserialize_str(row: &csv::StringRecord) -> Result<Trade, &'static str> {
  85. let time: u64 = atoi::atoi(row.get(0).ok_or("no time")?.as_bytes())
  86. .ok_or("parsing time failed")?;
  87. let amount: f64 = lexical::parse(row.get(1).ok_or("no amount")?)
  88. .map_err(|_| "parsing amount failed")?;
  89. let exch = match row.get(2).ok_or("no exch")? {
  90. "bmex" => e!(bmex),
  91. "bnce" => e!(bnce),
  92. "btfx" => e!(btfx),
  93. "gdax" => e!(gdax),
  94. "okex" => e!(okex),
  95. "bits" => e!(bits),
  96. "plnx" => e!(plnx),
  97. "krkn" => e!(krkn),
  98. _ => return Err("illegal exch"),
  99. };
  100. let price: f64 = lexical::parse(row.get(3).ok_or("no price")?)
  101. .map_err(|_| "parsing price failed")?;
  102. let ticker = match row.get(6).ok_or("no ticker")? {
  103. "btc_usd" => t!(btc-usd),
  104. "eth_usd" => t!(eth-usd),
  105. "ltc_usd" => t!(ltc-usd),
  106. "etc_usd" => t!(etc-usd),
  107. "bch_usd" => t!(bch-usd),
  108. "xmr_usd" => t!(xmr-usd),
  109. "usdt_usd" => t!(usdt-usd),
  110. _ => return Err("illegal ticker"),
  111. };
  112. Ok(Trade { time, amount, exch, price, ticker })
  113. }
  114. fn run(start: Instant, logger: &slog::Logger) -> Result<usize, String> {
  115. let opt = Opt::from_args();
  116. info!(logger, "initializing...";
  117. "trades-csv" => %opt.trades_csv.display(),
  118. "output-path" => %opt.output_path.display()
  119. );
  120. if ! opt.trades_csv.exists() {
  121. error!(logger, "path does not exist: {}", opt.trades_csv.display());
  122. fatal!("Error: path does not exist: {}", opt.trades_csv.display());
  123. }
  124. debug!(logger, "verified csv path exists"; "trades_csv" => %opt.trades_csv.display());
  125. let rdr = fs::File::open(&opt.trades_csv)
  126. .map_err(|e| format!("opening trades csv file failed: {} (tried to open {})", e, opt.trades_csv.display()))?;
  127. let rdr = io::BufReader::new(rdr);
  128. let mut rdr = csv::Reader::from_reader(rdr);
  129. // our data is ascii, so parsing with the slightly faster ByteRecord is fine
  130. //let headers: csv::ByteRecord = rdr.byte_headers().map_err(|e| format!("failed to parse CSV headers: {}", e))?.clone();
  131. //let mut row = csv::ByteRecord::new();
  132. //assert_eq!(headers.get(0), Some(&b"time"[..]));
  133. //assert_eq!(headers.get(1), Some(&b"amount"[..]));
  134. //assert_eq!(headers.get(2), Some(&b"exch"[..]));
  135. //assert_eq!(headers.get(3), Some(&b"price"[..]));
  136. //assert_eq!(headers.get(6), Some(&b"ticker"[..]));
  137. //let headers: csv::StringRecord = rdr.headers().map_err(|e| format!("failed to parse CSV headers: {}", e))?.clone();
  138. let mut row = csv::StringRecord::new();
  139. let mut n = 0;
  140. let mut last_time = 0;
  141. //while rdr.read_byte_record(&mut row)
  142. while rdr.read_record(&mut row)
  143. .map_err(|e| {
  144. format!("reading row {} failed: {}", (n+1).thousands_sep(), e)
  145. })?
  146. {
  147. //let trade: Trade = row.deserialize(Some(&headers))
  148. //let trade: Trade = manual_deserialize_bytes(&row)
  149. let trade: Trade = manual_deserialize_str(&row)
  150. .map_err(|e| {
  151. format!("deserializing row failed: {}\n\nFailing row:\n{:?}", e, row)
  152. })?;
  153. n += 1;
  154. // verify data is sorted by time
  155. debug_assert!(trade.time >= last_time);
  156. last_time = trade.time;
  157. if n % PROGRESS_EVERY == 0 || (cfg!(debug_assertions) && n % (1024 * 96) == 0) {
  158. info!(logger, "parsing csv file";
  159. "n rows" => %n.thousands_sep(),
  160. "elapsed" => ?(Instant::now() - start),
  161. );
  162. }
  163. if cfg!(debug_assertions) && n > PROGRESS_EVERY {
  164. warn!(logger, "debug mode: exiting early";
  165. "n rows" => %n.thousands_sep(),
  166. "elapsed" => ?(Instant::now() - start),
  167. );
  168. break
  169. }
  170. }
  171. Ok(n)
  172. }
  173. fn main() {
  174. let start = Instant::now();
  175. let decorator = slog_term::TermDecorator::new().stdout().force_color().build();
  176. let drain = slog_term::FullFormat::new(decorator).use_utc_timestamp().build().fuse();
  177. let drain = slog_async::Async::new(drain).chan_size(1024 * 64).thread_name("recv".into()).build().fuse();
  178. let logger = slog::Logger::root(drain, o!("version" => structopt::clap::crate_version!()));
  179. match run(start, &logger) {
  180. Ok(n) => {
  181. let took = Instant::now() - start;
  182. info!(logger, "finished in {:?}", took;
  183. "n rows" => %n.thousands_sep(),
  184. "rows/sec" => &((per_sec(n, took) * 100.0).round() / 10.0).thousands_sep(),
  185. );
  186. }
  187. Err(e) => {
  188. crit!(logger, "run failed: {:?}", e);
  189. eprintln!("\n\nError: {}", e);
  190. std::thread::sleep(Duration::from_millis(100));
  191. std::process::exit(1);
  192. }
  193. }
  194. }