mirror of
https://github.com/wassname/options_backtester.git
synced 2026-06-27 16:30:55 +08:00
Moved alert system to new repo: lambdaclass/alert
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
.env
|
||||
target/
|
||||
Generated
-1865
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"
|
||||
@@ -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
|
||||
);
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user