Moved alert system to new repo: lambdaclass/alert

This commit is contained in:
Juan Pablo Amoroso
2020-03-20 15:44:09 -03:00
parent 7586609a08
commit be20c56478
18 changed files with 0 additions and 2401 deletions
-2
View File
@@ -1,2 +0,0 @@
.env
target/
-1865
View File
File diff suppressed because it is too large Load Diff
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "alert"
version = "0.1.0"
authors = ["Amin Arria <arria.amin@gmail.com>"]
edition = "2018"
[dependencies]
fantoccini = "0.11.6"
futures = "0.1.25"
tokio = "0.1.17"
webdriver = "0.39.0"
serde_json = "1.0.39"
select = "0.4.2"
reqwest = "0.9.12"
diesel = { version = "1.4.2", features = ["sqlite", "chrono"] }
dotenv = "0.10"
chrono = "0.4"
-26
View File
@@ -1,26 +0,0 @@
FROM debian:latest
MAINTAINER Amin Arria <amin.arria@lambdaclass.com>
WORKDIR /root
RUN apt-get update
RUN apt-get install -y curl make build-essential pkg-config openssl libssl-dev
RUN apt-get -y install firefox-esr
RUN curl -L -O 'https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz'
RUN tar -vxf geckodriver-v0.24.0-linux64.tar.gz
RUN apt-get install -y sqlite3 libsqlite3-dev
RUN curl https://sh.rustup.rs -sSf > rustup
RUN sh rustup -y
ENV PATH=/root/.cargo/bin:$PATH
COPY . watchdog/
WORKDIR watchdog
RUN make install_diesel_cli
RUN make migration
RUN make release
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
CMD (/root/geckodriver &) && ./target/release/alert
-13
View File
@@ -1,13 +0,0 @@
.PHONY: migration run install_diesel_cli release
migration:
diesel migration run
run:
cargo run
install_diesel_cli:
cargo install diesel_cli --no-default-features --features sqlite chrono
release:
cargo build --release
-34
View File
@@ -1,34 +0,0 @@
# Financial Watchdog
## Requirements
- [WebDriver](https://www.w3.org/TR/webdriver1/) process running on port 4444 (e.g., [geckodriver](https://github.com/mozilla/geckodriver/releases), [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/))
- SQLite 3.19.3
- Rust 1.33
- [Diesel CLI 1.4](https://crates.io/crates/diesel_cli)
When installing `diesel_cli` if you run into any errors try installing with our needed features only:
```
$> make install_diesel_cli
```
## Setup
Create a `.env` with the following values:
- `DATABASE_URL`: The filepath where the DB will be stored (SQLite)
- `SLACK_WEBHOOK_URL`: URL for Slack's webhook
Create the database by running the migrations:
```
$> make migration
```
## Running
Run it
```
$> make run
```
-5
View File
@@ -1,5 +0,0 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
View File
@@ -1 +0,0 @@
DROP TABLE dollar;
@@ -1,16 +0,0 @@
CREATE TABLE dollar (
id INTEGER PRIMARY KEY,
buy DOUBLE NOT NULL,
buy_amount INTEGER NOT NULL,
sell DOUBLE NOT NULL,
sell_amount INTEGER NOT NULL,
last DOUBLE NOT NULL,
var DOUBLE NOT NULL,
varper DOUBLE NOT NULL,
volume INTEGER NOT NULL,
adjustment DOUBLE NOT NULL,
min DOUBLE NOT NULL,
max DOUBLE NOT NULL,
oin INTEGER NOT NULL,
created_at DATETIME NOT NULL
);
@@ -1 +0,0 @@
DROP TABLE alerts;
@@ -1,8 +0,0 @@
CREATE TABLE alerts (
id INTEGER PRIMARY KEY,
created_at DATETIME NOT NULL,
asset VARCHAR(20) NOT NULL,
previous_value DOUBLE NOT NULL,
current_value DOUBLE NOT NULL,
active BOOLEAN NOT NULL
);
-99
View File
@@ -1,99 +0,0 @@
extern crate reqwest;
extern crate serde_json;
use diesel::sqlite::SqliteConnection;
use serde_json::json;
use std::env;
use crate::models::{Dollar, Alert};
use crate::storage;
pub fn check_dollar(conn: &SqliteConnection, dollar: &Dollar) {
let dollar_close = storage::get_dollar_on_close(conn);
let closing_prc_change =
match &dollar_close {
Some(dollar_close) => {
(dollar.last - dollar_close.last)/dollar_close.last * 100.0
},
None => 0.0
};
match storage::get_dollar_alert(conn) {
Some(dollar_alert) => {
let alert_prc_change = (dollar.last - dollar_alert.current_value)/dollar_alert.current_value * 100.0;
// TODO: This value should be set by arguments to binary or by config files
if alert_prc_change.abs() > 2.0 {
if closing_prc_change.abs() > 3.0 {
// If we couldn't unwrap closing_prc_change == 0.0
let dollar_close = dollar_close.unwrap();
let new_alert = Alert::new("dollar".to_string(), dollar_alert.previous_value, dollar.last);
storage::replace_alert(conn, &dollar_alert, &new_alert);
send_updated_alert(dollar_close.last, dollar_alert.current_value, dollar.last, alert_prc_change, closing_prc_change);
} else {
storage::deactivate_alert(conn, &dollar_alert);
send_resolved_alert();
}
}
}
None => {
// TODO: This value should be set by arguments to binary or by config files
if closing_prc_change.abs() > 3.0 {
// If we couldn't unwrap closing_prc_change == 0.0
let dollar_close = dollar_close.unwrap();
let alert = Alert::new(String::from("dollar"), dollar_close.last, dollar.last);
storage::store_alert(conn, &alert);
send_new_alert(dollar_close.last, dollar.last, closing_prc_change);
}
}
};
}
// payload = {
// "username": "Financial Watchdog",
// "icon_emoji": ":money_with_wings:",
// "attachments": [{
// "color": color,
// "title": title,
// "text": text,
// "fallback": text
// "footer": "Financial Watchdog",
// }]
// }
fn send_new_alert(price_close: f64, price_current: f64, prc_change: f64) {
let msg = format!("Closing price: {:.2}\nCurrent price: {:.2}\nChanged: {:+.2}%", price_close, price_current, prc_change);
send_alert_slack("#B22222", "Price jump from closing", &msg);
// TODO: send_alert_mail(prc_change);
}
fn send_resolved_alert() {
let msg = format!("Dollar price from closing price is back below threshold");
send_alert_slack("#3873AD", "Price back to normal", &msg);
// TODO: send_alert_mail(prc_change);
}
fn send_updated_alert(price_close: f64, price_previous: f64, price_current: f64, prc_change: f64, prc_change_close: f64) {
let msg = format!("Closing price: {:.2}\nPrevious price: {:.2}\nCurrent price: {:.2}\nChange from previous: *{:+.2}%*\nChange from close: {:+.2}%", price_close, price_previous, price_current, prc_change, prc_change_close);
send_alert_slack("#f1a031", "[UPDATE] Price jump", &msg);
// TODO: send_alert_mail(prc_change);
}
fn send_alert_slack(color: &str, title: &str, msg: &str) {
let payload = json!({
"username": "Financial Watchdog",
"icon_emoji": ":money_with_wings:",
"attachments": [{
"color": color,
"title": title,
"text": msg,
"fallback": msg,
"footer": "Financial Watchdog"
}]
})
.to_string();
// We can unwrap safely because env variables are checked in main.rs::init_env()
let slack_webhook_url = env::var("SLACK_WEBHOOK_URL").unwrap();
let client = reqwest::Client::new();
client.post(&slack_webhook_url)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(payload)
.send()
.expect("Failed sending slack alert");
}
-37
View File
@@ -1,37 +0,0 @@
#[macro_use]
extern crate diesel;
extern crate dotenv;
extern crate chrono;
pub mod schema;
pub mod models;
mod scrape;
mod storage;
mod alert;
use dotenv::dotenv;
use std::{thread, time, env};
fn main() {
init_env();
loop {
std::thread::spawn(|| {
let dbconn = crate::storage::establish_connection();
let value = crate::scrape::scrape();
crate::alert::check_dollar(&dbconn, &value);
crate::storage::store(&dbconn, &value);
});
thread::sleep(time::Duration::from_secs(60));
}
}
fn init_env() {
dotenv().ok();
env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
env::var("SLACK_WEBHOOK_URL")
.expect("SLACK_WEBHOOK_URL must be set");
}
-71
View File
@@ -1,71 +0,0 @@
use chrono::NaiveDateTime;
use chrono::offset::Utc;
use crate::schema::{dollar, alerts};
#[derive(Queryable, Insertable, Debug)]
#[table_name="dollar"]
pub struct Dollar {
pub id: Option<i32>,
pub buy: f64,
pub buy_amount: i32,
pub sell: f64,
pub sell_amount: i32,
pub last: f64,
pub var: f64,
pub varper: f64,
pub volume: i32,
pub adjustment: f64,
pub min: f64,
pub max: f64,
pub oin: i32,
pub created_at: NaiveDateTime
}
impl Dollar {
pub fn new() -> Dollar {
let created_at = Utc::now().naive_utc();
Dollar {
id: None,
buy: 0.0,
buy_amount: 0,
sell: 0.0,
sell_amount: 0,
last: 0.0,
var: 0.0,
varper: 0.0,
volume: 0,
adjustment: 0.0,
min: 0.0,
max: 0.0,
oin: 0,
created_at: created_at
}
}
}
#[derive(Queryable, Insertable, Debug)]
#[table_name="alerts"]
pub struct Alert {
pub id: Option<i32>,
pub created_at: NaiveDateTime,
pub asset: String,
pub previous_value: f64,
pub current_value: f64,
pub active: bool
}
impl Alert {
pub fn new(asset: String, previous_value: f64, current_value: f64) -> Alert {
let created_at = Utc::now().naive_utc();
Alert {
id: None,
created_at: created_at,
asset: asset,
previous_value: previous_value,
current_value: current_value,
active: true
}
}
}
-34
View File
@@ -1,34 +0,0 @@
table! {
alerts (id) {
id -> Nullable<Integer>,
created_at -> Timestamp,
asset -> Text,
previous_value -> Double,
current_value -> Double,
active -> Bool,
}
}
table! {
dollar (id) {
id -> Nullable<Integer>,
buy -> Double,
buy_amount -> Integer,
sell -> Double,
sell_amount -> Integer,
last -> Double,
var -> Double,
varper -> Double,
volume -> Integer,
adjustment -> Double,
min -> Double,
max -> Double,
oin -> Integer,
created_at -> Timestamp,
}
}
allow_tables_to_appear_in_same_query!(
alerts,
dollar,
);
-102
View File
@@ -1,102 +0,0 @@
extern crate fantoccini;
extern crate futures;
extern crate select;
extern crate serde_json;
extern crate tokio;
extern crate webdriver;
use fantoccini::{Client, Locator};
use futures::future::Future;
use futures::sync::oneshot;
use select::document::Document;
use select::predicate::Class;
use serde_json::json;
use webdriver::capabilities::Capabilities;
use crate::models::Dollar;
pub fn scrape() -> Dollar {
let html = fetch_site();
let document = Document::from(html.as_str());
let mut value = Dollar::new();
for node in document.find(Class("PriceCell")) {
let full_class =
node.attr("class")
.unwrap_or_else(|| panic!("Node without class attribute"));
let class = full_class.split_whitespace().last();
match class {
Some("bsz") => value.buy_amount = parse_i32(node.text()),
Some("bid") => value.buy = parse_f64(node.text()),
Some("ask") => value.sell = parse_f64(node.text()),
Some("asz") => value.sell_amount = parse_i32(node.text()),
Some("lst") => value.last = parse_f64(node.text()),
Some("variation") => value.var = parse_f64(node.text()),
Some("change") => {
value.varper = parse_f64(node.text().trim_end_matches("%").to_string())
}
Some("von") => value.volume = parse_i32(node.text()),
Some("settlementPrice") => value.adjustment = parse_f64(node.text()),
Some("low") => value.min = parse_f64(node.text()),
Some("hgh") => value.max = parse_f64(node.text()),
Some("oin") => value.oin = parse_i32(node.text()),
Some("futureImpliedRate") => {}
Some(class) => {
panic!("Non-matching class: {}", class);
}
None => {
panic!("Node without empty class attribute");
}
}
}
value
}
fn fetch_site() -> String {
// TODO: Set headless capabilities for chromedriver.
let mut cap = Capabilities::new();
let arg = json!({"args": ["-headless"]});
cap.insert("moz:firefoxOptions".to_string(), arg);
let c = Client::with_capabilities("http://localhost:4444", cap);
let (sender, receiver) = oneshot::channel::<String>();
tokio::run(
c.map_err(|e| unimplemented!("failed to connect to WebDriver: {:?}", e))
.and_then(|c| c.goto("https://rofex.primary.ventures/rofex/futuros"))
.and_then(|mut c| c.current_url().map(move |url| (c, url)))
.and_then(|(c, _url)| {
c.wait_for_find(Locator::XPath("((//div[@class='PricePanelRow-row'])[1]//div[@class='PriceCell lst'])[not(contains(., '-'))]/.."))
})
.and_then(|mut e| {
e.html(true)
})
.and_then(|e| match sender.send(e) {
Err(err) => panic!("Error sending fetched site: {}", err),
Ok(()) => Ok(()),
})
.map_err(|e| {
panic!("a WebDriver command failed: {:?}", e);
}),
);
receiver.wait().unwrap()
}
fn parse_i32(text: String) -> i32 {
text.replace(".", "")
.trim()
.parse()
.unwrap_or_else(|err| panic!("Error parsing \"{}\": {}", text, err))
}
fn parse_f64(text: String) -> f64 {
text.replace(".", "")
.replace(",", ".")
.trim()
.parse()
.unwrap_or_else(|err| panic!("Error parsing \"{}\": {}", text, err))
}
-70
View File
@@ -1,70 +0,0 @@
extern crate diesel;
extern crate dotenv;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use diesel::result::Error::NotFound;
use chrono::{Utc, NaiveDateTime, NaiveTime};
use std::env;
use crate::models::{Dollar, Alert};
use crate::schema::{dollar, alerts};
pub fn establish_connection() -> SqliteConnection {
// We can unwrap safely because env variables are checked in main.rs::init_env()
let database_url = env::var("DATABASE_URL").unwrap();
SqliteConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}
pub fn store(conn: &SqliteConnection, new_dollar: &Dollar) {
diesel::insert_into(dollar::table)
.values(new_dollar)
.execute(conn)
.expect("Error saving new dollar");
}
pub fn get_dollar_on_close(conn: &SqliteConnection) -> Option<Dollar> {
let yesterday = Utc::today().naive_utc().pred();
let timestamp = NaiveDateTime::new(yesterday, NaiveTime::from_hms(23, 59, 59));
let result = dollar::table.filter(dollar::created_at.lt(timestamp))
.order(dollar::created_at.desc())
.first::<Dollar>(conn);
match result {
Ok(previous) => Some(previous),
Err(NotFound) => None,
Err(err) => panic!("Error getting previous dollar: \n{}", err)
}
}
pub fn store_alert(conn: &SqliteConnection, new_alert: &Alert) {
diesel::insert_into(alerts::table)
.values(new_alert)
.execute(conn)
.expect("Error saving new alert");
}
pub fn get_dollar_alert(conn: &SqliteConnection) -> Option<Alert> {
let result = alerts::table.filter(alerts::asset.eq("dollar").and(alerts::active.eq(true)))
.first::<Alert>(conn);
match result {
Ok(alert) => Some(alert),
Err(NotFound) => None,
Err(err) => panic!("Error getting current alert: \n{}", err)
}
}
pub fn deactivate_alert(conn: &SqliteConnection, alert: &Alert) {
diesel::update(alerts::table.filter(alerts::id.eq(alert.id)))
.set(alerts::active.eq(false))
.execute(conn)
.expect("Error deactivating alert");
}
pub fn replace_alert(conn: &SqliteConnection, old_alert: &Alert, new_alert: &Alert) {
// TODO: Do this operations inside a transaction for atomicity
store_alert(conn, new_alert);
deactivate_alert(conn, old_alert);
}