Skip to content

Commit

Permalink
Merge pull request #3 from universome/arbitrer
Browse files Browse the repository at this point in the history
Arbitrer
  • Loading branch information
loyd authored Aug 26, 2016
2 parents 0634126 + 56e4577 commit aa100be
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 35 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ lazy_static = "^0.2.1"
url = "^1.2"
mime = "^0.2.2"
rustc-serialize = "^0.3.19"
toml = "^0.2"
crossbeam = "^0.2.10"
13 changes: 13 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[arbitrer]
min-delay = 2
max-delay = 60

[[gamblers]]
host = "egamingbets.com"
username = "shmaladdin"
password = "aladdin"

[[gamblers]]
host = "vitalbet.com"
username = "Алладин"
password = "aladdin"
161 changes: 161 additions & 0 deletions src/arbitrer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use std::collections::HashMap;
use std::cmp;
use std::thread;
use chrono::{UTC, Duration};
use crossbeam;
use crossbeam::sync::MsQueue;

use base::CONFIG;
use events::Offer;
use gamblers::{self, Gambler};
use opportunity::{self, Strategy};

struct GamblerInfo {
host: String,
gambler: Box<Gambler + Sync>
}

struct MarkedOffer(usize, Offer);
type Event = Vec<MarkedOffer>;

pub fn run() {
let gamblers = init_gamblers();
let (min_delay, max_delay) = get_delay_window();

loop {
let events = fetch_events(&gamblers);
realize_events(&gamblers, &events);

let delay = clamp(min_delay, find_delay(&events), max_delay);
println!("Sleep for {}m", delay.num_minutes());
thread::sleep(delay.to_std().unwrap());
}
}

fn init_gamblers() -> Vec<GamblerInfo> {
let mut info = vec![];
let array = CONFIG.lookup("gamblers").unwrap().as_slice().unwrap();

for item in array {
let enabled = item.lookup("enabled").map_or(true, |x| x.as_bool().unwrap());

if !enabled {
continue;
}

let host = item.lookup("host").unwrap().as_str().unwrap();
let username = item.lookup("username").unwrap().as_str().unwrap();
let password = item.lookup("password").unwrap().as_str().unwrap();

let gambler = gamblers::new(host);

// TODO(loyd): parallel authorization.
if let Err(error) = gambler.authorize(username, password) {
println!("Error during auth {}: {}", host, error);
continue;
}

println!("Authorized: {}", host);

info.push(GamblerInfo {
host: host.to_owned(),
gambler: gambler
});
}

info
}

fn get_delay_window() -> (Duration, Duration) {
let min = CONFIG.lookup("arbitrer.min-delay").unwrap().as_integer().unwrap();
let max = CONFIG.lookup("arbitrer.max-delay").unwrap().as_integer().unwrap();

(Duration::minutes(min), Duration::minutes(max))
}

fn fetch_events(gamblers: &[GamblerInfo]) -> Vec<Event> {
let queue = &MsQueue::new();

let events = crossbeam::scope(|scope| {
for (idx, info) in gamblers.iter().enumerate() {
scope.spawn(move || {
let result = info.gambler.fetch_offers();

if let Err(ref error) = result {
println!("Error during fetching from {}: {}", info.host, error);
queue.push(None);
return;
}

let result = result.unwrap();
if result.is_empty() {
println!("There is no offers from {}", info.host);
queue.push(None);
return;
}

for offer in result {
queue.push(Some(MarkedOffer(idx, offer)))
}

queue.push(None);
});
}

group_offers(queue, gamblers.len())
});

events.into_iter().map(|(_, e)| e).collect()
}

fn group_offers(queue: &MsQueue<Option<MarkedOffer>>, mut count: usize) -> HashMap<Offer, Event> {
let mut events: HashMap<_, Event> = HashMap::new();

while count > 0 {
let marked = queue.pop();

if marked.is_none() {
count -= 1;
continue;
}

let marked = marked.unwrap();

if events.contains_key(&marked.1) {
events.get_mut(&marked.1).unwrap().push(marked);
} else {
// TODO(loyd): how to avoid copying?
events.insert(marked.1.clone(), vec![marked]);
}
}

events
}

fn realize_events(gamblers: &[GamblerInfo], events: &[Event]) {
for (i, event) in events.iter().enumerate() {
println!("[#{}] {}, {:?}:", i, event[0].1.date, event[0].1.kind);

for offer in event {
println!(" {}: {:?}", gamblers[offer.0].host, offer.1.outcomes);
}

let outcomes = event.into_iter().map(|o| o.1.outcomes.as_slice());
let opp = opportunity::find_best(outcomes, Strategy::Unbiased);

if let Some(opp) = opp {
println!(" There is an opportunity: {:?}", opp);
}
}
}

fn find_delay(events: &[Event]) -> Duration {
let nearest = events.iter().map(|b| b[0].1.date).min().unwrap();
let now = UTC::now();

nearest - now
}

fn clamp<T: cmp::Ord>(min: T, val: T, max: T) -> T {
cmp::max(min, cmp::min(val, max))
}
18 changes: 18 additions & 0 deletions src/base/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use toml::{Parser, Value};

lazy_static! {
pub static ref CONFIG: Value = {
let config = include_str!("../../config.toml");
let mut parser = Parser::new(config);

if let Some(table) = parser.parse() {
return Value::Table(table);
}

for error in parser.errors {
println!("{}", error);
}

panic!("The config is invalid.");
};
}
2 changes: 2 additions & 0 deletions src/base/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::result;
use std::error;

pub use self::config::CONFIG;
pub use self::session::Session;
pub use self::currency::Currency;
pub use self::parsing::{NodeRefExt, ElementDataExt};

mod config;
mod session;
mod currency;
mod parsing;
Expand Down
19 changes: 13 additions & 6 deletions src/base/session.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::time::Duration;
use std::io::Read;
use std::cell::RefCell;
use std::sync::Mutex;
use url::form_urlencoded::Serializer;
use hyper::client::{Client, Response};
use hyper::header::{Headers, SetCookie, Cookie, UserAgent, ContentLength, Accept, ContentType, qitem};
Expand All @@ -18,15 +19,19 @@ const USER_AGENT: &'static str = concat!("Mozilla/5.0 (Macintosh; Intel Mac OS X
pub struct Session {
client: Client,
base_url: String,
cookie: RefCell<Cookie>
cookie: Mutex<Cookie>
}

impl Session {
pub fn new(base_url: &str) -> Session {
let mut client = Client::new();
client.set_read_timeout(Some(Duration::from_secs(25)));
client.set_write_timeout(Some(Duration::from_secs(25)));

Session {
client: Client::new(),
client: client,
base_url: base_url.to_string(),
cookie: RefCell::new(Cookie(vec![]))
cookie: Mutex::new(Cookie(vec![]))
}
}

Expand Down Expand Up @@ -97,7 +102,9 @@ impl Session {
None => self.client.get(&url)
};

headers.set(self.cookie.borrow().clone());
let mut cookie = self.cookie.lock().unwrap();

headers.set(cookie.clone());
headers.set(UserAgent(USER_AGENT.to_owned()));

if let Some(body) = body {
Expand All @@ -113,7 +120,7 @@ impl Session {
let cookies = response.headers.get::<SetCookie>()
.map_or_else(Vec::new, |c| c.0.clone());

*self.cookie.borrow_mut() = Cookie(cookies);
*cookie = Cookie(cookies);

Ok(response)
}
Expand Down
67 changes: 62 additions & 5 deletions src/events.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,77 @@
use std::cmp::{PartialEq, Eq};
use std::hash::{Hash, Hasher};
use chrono::{DateTime, UTC};

#[derive(Debug, PartialEq)]
pub struct Event {
#[derive(Debug, Clone)]
pub struct Offer {
pub date: DateTime<UTC>,
pub kind: Kind,
pub outcomes: Vec<Outcome>,
pub inner_id: u64
}

#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Outcome(pub String, pub f64);

#[derive(Debug, PartialEq)]
pub static DRAW: &'static str = "(draw)";

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Kind {
Dota2(Dota2)
}

#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Dota2 { Series, Map(u32), FirstBlood(u32), First10Kills(u32) }

impl PartialEq for Offer {
fn eq(&self, other: &Offer) -> bool {
if self.date != other.date || self.kind != other.kind {
return false;
}

// Search at least one match (except draw of course).
for fst in &self.outcomes {
if fst.0 == DRAW { continue; }

for snd in &other.outcomes {
if fuzzy_eq(&fst.0, &snd.0) {
return true;
}
}
}

false
}
}

impl Eq for Offer {}

impl Hash for Offer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.date.hash(state);
self.kind.hash(state);
}
}

fn fuzzy_eq(lhs: &str, rhs: &str) -> bool {
let left = lhs.chars().filter(|c| c.is_alphabetic());
let right = rhs.chars().filter(|c| c.is_alphabetic());

for (l, r) in left.zip(right) {
if l.to_lowercase().zip(r.to_lowercase()).any(|(l, r)| l != r) {
return false;
}
}

true
}

#[test]
fn test_fuzzy_eq() {
assert!(fuzzy_eq("rb", "rb"));
assert!(fuzzy_eq("rb ", "rb"));
assert!(fuzzy_eq("RB", "rb"));
assert!(fuzzy_eq("r.b", "rb"));
assert!(fuzzy_eq(" r.b", "rb"));
assert!(fuzzy_eq(" R.8B ", "rb"));
}
18 changes: 9 additions & 9 deletions src/gamblers/egamingbets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use base::Prime;
use base::{NodeRefExt, ElementDataExt};
use base::{Session, Currency};
use gamblers::Gambler;
use events::{Event, Outcome, Kind, Dota2};
use events::{Offer, Outcome, DRAW, Kind, Dota2};

pub struct EGB {
session: Session
Expand Down Expand Up @@ -40,13 +40,13 @@ impl Gambler for EGB {
Ok(Currency::from(money))
}

fn get_events(&self) -> Prime<Vec<Event>> {
fn fetch_offers(&self) -> Prime<Vec<Offer>> {
let table = try!(self.session.get_json::<Table>("/bets?st=0&ut=0&f="));
let events = table.bets.into_iter().filter_map(Into::into).collect();
Ok(events)
let offers = table.bets.into_iter().filter_map(Into::into).collect();
Ok(offers)
}

fn make_bet(&self, event: Event, outcome: Outcome, bet: Currency) -> Prime<()> {
fn place_bet(&self, offer: Offer, outcome: Outcome, bet: Currency) -> Prime<()> {
unimplemented!();
}
}
Expand Down Expand Up @@ -75,8 +75,8 @@ struct Bet {
winner: i32
}

impl Into<Option<Event>> for Bet {
fn into(self) -> Option<Event> {
impl Into<Option<Offer>> for Bet {
fn into(self) -> Option<Offer> {
// Irrelevant by date.
if self.winner > 0 {
return None;
Expand Down Expand Up @@ -107,10 +107,10 @@ impl Into<Option<Event>> for Bet {
let coef_draw = coef_draw.unwrap();

if coef_draw > 0. {
outcomes.push(Outcome("Draw".to_owned(), coef_draw));
outcomes.push(Outcome(DRAW.to_owned(), coef_draw));
}

Some(Event {
Some(Offer {
date: DateTime::from_utc(NaiveDateTime::from_timestamp(self.date as i64, 0), UTC),
kind: kind,
outcomes: outcomes,
Expand Down
Loading

0 comments on commit aa100be

Please sign in to comment.