-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Arbitrer #3
Arbitrer #3
Changes from all commits
49bcb69
e2e42aa
9a97bc2
a035a07
8efb552
30b3d88
d588c57
9755543
56e4577
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
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)) | ||
} |
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."); | ||
}; | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Тут получается посимвольное сравнение? Оно будет ломаться на There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Да, будем логировать одиночный эвенты и искать эвристики для улучшения. PartialEq можно усложнять до бесконечности, особо на производительность не сыграет (благодаря хешированию). |
||
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")); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kek?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если ты про пароли, то сейчас реально так проще.