Add Ollama news sentiment analysis
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
98
Cargo.lock
generated
98
Cargo.lock
generated
@@ -233,7 +233,6 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.52.0",
|
"windows-targets 0.52.0",
|
||||||
]
|
]
|
||||||
@@ -326,41 +325,6 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling"
|
|
||||||
version = "0.20.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"darling_macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_core"
|
|
||||||
version = "0.20.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
|
|
||||||
dependencies = [
|
|
||||||
"fnv",
|
|
||||||
"ident_case",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"strsim",
|
|
||||||
"syn 2.0.48",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_macro"
|
|
||||||
version = "0.20.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.48",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.3"
|
version = "5.5.3"
|
||||||
@@ -689,12 +653,6 @@ version = "0.3.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f"
|
checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hex"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html-escape"
|
name = "html-escape"
|
||||||
version = "0.2.13"
|
version = "0.2.13"
|
||||||
@@ -875,12 +833,6 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ident_case"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -899,7 +851,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"hashbrown 0.12.3",
|
"hashbrown 0.12.3",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -910,7 +861,6 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.3",
|
"hashbrown 0.14.3",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1309,7 +1259,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"serde_with",
|
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
@@ -1539,9 +1488,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.195"
|
version = "1.0.196"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
|
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
@@ -1558,9 +1507,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.195"
|
version = "1.0.196"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
|
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1580,9 +1529,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.111"
|
version = "1.0.112"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
|
checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -1622,35 +1571,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_with"
|
|
||||||
version = "3.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f5c9fdb6b00a489875b22efd4b78fe2b363b72265cc5f6eb2e2b9ee270e6140c"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"chrono",
|
|
||||||
"hex",
|
|
||||||
"indexmap 1.9.3",
|
|
||||||
"indexmap 2.1.0",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_with_macros",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_with_macros"
|
|
||||||
version = "3.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dbff351eb4b33600a2e138dfa0b10b65a238ea8ff8fb2387c422c5022a3e8298"
|
|
||||||
dependencies = [
|
|
||||||
"darling",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.48",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_yaml"
|
name = "serde_yaml"
|
||||||
version = "0.8.26"
|
version = "0.8.26"
|
||||||
@@ -1716,12 +1636,6 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
|
@@ -27,7 +27,6 @@ log4rs = "1.2.0"
|
|||||||
serde = "1.0.188"
|
serde = "1.0.188"
|
||||||
serde_json = "1.0.105"
|
serde_json = "1.0.105"
|
||||||
serde_repr = "0.1.18"
|
serde_repr = "0.1.18"
|
||||||
serde_with = "3.5.1"
|
|
||||||
futures-util = "0.3.28"
|
futures-util = "0.3.28"
|
||||||
reqwest = { version = "0.11.20", features = [
|
reqwest = { version = "0.11.20", features = [
|
||||||
"json",
|
"json",
|
||||||
|
@@ -4,6 +4,11 @@ services:
|
|||||||
file: support/clickhouse/docker-compose.yml
|
file: support/clickhouse/docker-compose.yml
|
||||||
service: clickhouse
|
service: clickhouse
|
||||||
|
|
||||||
|
ollama:
|
||||||
|
extends:
|
||||||
|
file: support/ollama/docker-compose.yml
|
||||||
|
service: ollama
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
extends:
|
extends:
|
||||||
file: support/grafana/docker-compose.yml
|
file: support/grafana/docker-compose.yml
|
||||||
@@ -19,10 +24,12 @@ services:
|
|||||||
- 7878:7878
|
- 7878:7878
|
||||||
depends_on:
|
depends_on:
|
||||||
- clickhouse
|
- clickhouse
|
||||||
|
- ollama
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker
|
- .env.docker
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
clickhouse-lib:
|
clickhouse-lib:
|
||||||
clickhouse-log:
|
clickhouse-log:
|
||||||
|
ollama:
|
||||||
grafana-lib:
|
grafana-lib:
|
||||||
|
@@ -4,7 +4,7 @@ use reqwest::{
|
|||||||
header::{HeaderMap, HeaderName, HeaderValue},
|
header::{HeaderMap, HeaderName, HeaderValue},
|
||||||
Client,
|
Client,
|
||||||
};
|
};
|
||||||
use std::{env, num::NonZeroU32, sync::Arc};
|
use std::{env, num::NonZeroU32, sync::Arc, time::Duration};
|
||||||
|
|
||||||
pub const ALPACA_ASSET_API_URL: &str = "https://api.alpaca.markets/v2/assets";
|
pub const ALPACA_ASSET_API_URL: &str = "https://api.alpaca.markets/v2/assets";
|
||||||
pub const ALPACA_CLOCK_API_URL: &str = "https://api.alpaca.markets/v2/clock";
|
pub const ALPACA_CLOCK_API_URL: &str = "https://api.alpaca.markets/v2/clock";
|
||||||
@@ -19,9 +19,12 @@ pub const ALPACA_NEWS_WEBSOCKET_URL: &str = "wss://stream.data.alpaca.markets/v1
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub alpaca_api_key: String,
|
pub alpaca_api_key: String,
|
||||||
pub alpaca_api_secret: String,
|
pub alpaca_api_secret: String,
|
||||||
pub alpaca_client: Client,
|
|
||||||
pub alpaca_rate_limit: DefaultDirectRateLimiter,
|
pub alpaca_rate_limit: DefaultDirectRateLimiter,
|
||||||
pub alpaca_source: Source,
|
pub alpaca_source: Source,
|
||||||
|
pub alpaca_client: Client,
|
||||||
|
pub ollama_url: String,
|
||||||
|
pub ollama_model: String,
|
||||||
|
pub ollama_client: Client,
|
||||||
pub clickhouse_client: clickhouse::Client,
|
pub clickhouse_client: clickhouse::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +38,9 @@ impl Config {
|
|||||||
.parse()
|
.parse()
|
||||||
.expect("ALPACA_SOURCE must be a either 'iex' or 'sip'.");
|
.expect("ALPACA_SOURCE must be a either 'iex' or 'sip'.");
|
||||||
|
|
||||||
|
let ollama_url = env::var("OLLAMA_URL").expect("OLLAMA_URL must be set.");
|
||||||
|
let ollama_model = env::var("OLLAMA_MODEL").expect("OLLAMA_MODEL must be set.");
|
||||||
|
|
||||||
let clickhouse_url = env::var("CLICKHOUSE_URL").expect("CLICKHOUSE_URL must be set.");
|
let clickhouse_url = env::var("CLICKHOUSE_URL").expect("CLICKHOUSE_URL must be set.");
|
||||||
let clickhouse_user = env::var("CLICKHOUSE_USER").expect("CLICKHOUSE_USER must be set.");
|
let clickhouse_user = env::var("CLICKHOUSE_USER").expect("CLICKHOUSE_USER must be set.");
|
||||||
let clickhouse_password =
|
let clickhouse_password =
|
||||||
@@ -55,20 +61,27 @@ impl Config {
|
|||||||
.expect("Alpaca API secret must not contain invalid characters."),
|
.expect("Alpaca API secret must not contain invalid characters."),
|
||||||
),
|
),
|
||||||
]))
|
]))
|
||||||
|
.timeout(Duration::from_secs(60))
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
alpaca_rate_limit: RateLimiter::direct(Quota::per_minute(match alpaca_source {
|
alpaca_rate_limit: RateLimiter::direct(Quota::per_minute(match alpaca_source {
|
||||||
Source::Iex => unsafe { NonZeroU32::new_unchecked(200) },
|
Source::Iex => unsafe { NonZeroU32::new_unchecked(190) },
|
||||||
Source::Sip => unsafe { NonZeroU32::new_unchecked(10000) },
|
Source::Sip => unsafe { NonZeroU32::new_unchecked(9990) },
|
||||||
})),
|
})),
|
||||||
alpaca_source,
|
alpaca_source,
|
||||||
|
alpaca_api_key,
|
||||||
|
alpaca_api_secret,
|
||||||
|
ollama_url,
|
||||||
|
ollama_model,
|
||||||
|
ollama_client: Client::builder()
|
||||||
|
.timeout(Duration::from_secs(15))
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
clickhouse_client: clickhouse::Client::default()
|
clickhouse_client: clickhouse::Client::default()
|
||||||
.with_url(clickhouse_url)
|
.with_url(clickhouse_url)
|
||||||
.with_user(clickhouse_user)
|
.with_user(clickhouse_user)
|
||||||
.with_password(clickhouse_password)
|
.with_password(clickhouse_password)
|
||||||
.with_database(clickhouse_db),
|
.with_database(clickhouse_db),
|
||||||
alpaca_api_key,
|
|
||||||
alpaca_api_secret,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,11 +9,11 @@ mod threads;
|
|||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use crate::utils::cleanup;
|
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use log4rs::config::Deserializers;
|
use log4rs::config::Deserializers;
|
||||||
use tokio::{spawn, sync::mpsc};
|
use tokio::{spawn, sync::mpsc};
|
||||||
|
use utils::{cleanup::cleanup, init};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -22,9 +22,10 @@ async fn main() {
|
|||||||
let app_config = Config::arc_from_env();
|
let app_config = Config::arc_from_env();
|
||||||
|
|
||||||
cleanup(&app_config.clickhouse_client).await;
|
cleanup(&app_config.clickhouse_client).await;
|
||||||
|
init::ollama(&app_config).await;
|
||||||
|
|
||||||
let (asset_status_sender, asset_status_receiver) =
|
let (asset_status_sender, asset_status_receiver) =
|
||||||
mpsc::channel::<threads::data::asset_status::Message>(100);
|
mpsc::channel::<threads::data::asset_status::Message>(10);
|
||||||
let (clock_sender, clock_receiver) = mpsc::channel::<threads::clock::Message>(1);
|
let (clock_sender, clock_receiver) = mpsc::channel::<threads::clock::Message>(1);
|
||||||
|
|
||||||
spawn(threads::data::run(
|
spawn(threads::data::run(
|
||||||
|
@@ -56,10 +56,7 @@ pub async fn add(
|
|||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()
|
.error_for_status()
|
||||||
.map_err(|e| match e.status() {
|
.map_err(backoff::Error::Permanent)?
|
||||||
Some(reqwest::StatusCode::NOT_FOUND) => backoff::Error::Permanent(e),
|
|
||||||
_ => e.into(),
|
|
||||||
})?
|
|
||||||
.json::<incoming::asset::Asset>()
|
.json::<incoming::asset::Asset>()
|
||||||
.await
|
.await
|
||||||
.map_err(backoff::Error::Permanent)
|
.map_err(backoff::Error::Permanent)
|
||||||
|
@@ -44,7 +44,8 @@ pub async fn run(app_config: Arc<Config>, clock_sender: mpsc::Sender<Message>) {
|
|||||||
.get(ALPACA_CLOCK_API_URL)
|
.get(ALPACA_CLOCK_API_URL)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?
|
.error_for_status()
|
||||||
|
.map_err(backoff::Error::Permanent)?
|
||||||
.json::<alpaca::api::incoming::clock::Clock>()
|
.json::<alpaca::api::incoming::clock::Clock>()
|
||||||
.await
|
.await
|
||||||
.map_err(backoff::Error::Permanent)
|
.map_err(backoff::Error::Permanent)
|
||||||
|
@@ -3,13 +3,14 @@ use crate::{
|
|||||||
config::{Config, ALPACA_CRYPTO_DATA_URL, ALPACA_NEWS_DATA_URL, ALPACA_STOCK_DATA_URL},
|
config::{Config, ALPACA_CRYPTO_DATA_URL, ALPACA_NEWS_DATA_URL, ALPACA_STOCK_DATA_URL},
|
||||||
database,
|
database,
|
||||||
types::{
|
types::{
|
||||||
alpaca::{api, Source},
|
alpaca::{self, Source},
|
||||||
Asset, Bar, Class, News, Subset,
|
ollama, Asset, Bar, Class, News, Subset,
|
||||||
},
|
},
|
||||||
utils::{duration_until, last_minute, FIFTEEN_MINUTES, ONE_MINUTE},
|
utils::{duration_until, last_minute, FIFTEEN_MINUTES, ONE_MINUTE},
|
||||||
};
|
};
|
||||||
use backoff::{future::retry, ExponentialBackoff};
|
use backoff::{future::retry, ExponentialBackoff};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
use serde_json::{from_str, to_string};
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
@@ -244,18 +245,19 @@ async fn execute_backfill_bars(
|
|||||||
app_config
|
app_config
|
||||||
.alpaca_client
|
.alpaca_client
|
||||||
.get(&data_url)
|
.get(&data_url)
|
||||||
.query(&api::outgoing::bar::Bar::new(
|
.query(&alpaca::api::outgoing::bar::Bar {
|
||||||
vec![symbol.clone()],
|
symbols: vec![symbol.clone()],
|
||||||
ONE_MINUTE,
|
timeframe: ONE_MINUTE,
|
||||||
fetch_from,
|
start: fetch_from,
|
||||||
fetch_to,
|
end: fetch_to,
|
||||||
10000,
|
limit: 10000,
|
||||||
next_page_token.clone(),
|
page_token: next_page_token.clone(),
|
||||||
))
|
})
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?
|
.error_for_status()
|
||||||
.json::<api::incoming::bar::Message>()
|
.map_err(backoff::Error::Permanent)?
|
||||||
|
.json::<alpaca::api::incoming::bar::Message>()
|
||||||
.await
|
.await
|
||||||
.map_err(backoff::Error::Permanent)
|
.map_err(backoff::Error::Permanent)
|
||||||
})
|
})
|
||||||
@@ -318,19 +320,20 @@ async fn execute_backfill_news(
|
|||||||
app_config
|
app_config
|
||||||
.alpaca_client
|
.alpaca_client
|
||||||
.get(&data_url)
|
.get(&data_url)
|
||||||
.query(&api::outgoing::news::News::new(
|
.query(&alpaca::api::outgoing::news::News {
|
||||||
vec![symbol.clone()],
|
symbols: vec![symbol.clone()],
|
||||||
fetch_from,
|
start: fetch_from,
|
||||||
fetch_to,
|
end: fetch_to,
|
||||||
50,
|
limit: 50,
|
||||||
true,
|
include_content: true,
|
||||||
false,
|
exclude_contentless: false,
|
||||||
next_page_token.clone(),
|
page_token: next_page_token.clone(),
|
||||||
))
|
})
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?
|
.error_for_status()
|
||||||
.json::<api::incoming::news::Message>()
|
.map_err(backoff::Error::Permanent)?
|
||||||
|
.json::<alpaca::api::incoming::news::Message>()
|
||||||
.await
|
.await
|
||||||
.map_err(backoff::Error::Permanent)
|
.map_err(backoff::Error::Permanent)
|
||||||
})
|
})
|
||||||
@@ -361,6 +364,53 @@ async fn execute_backfill_news(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for news in &mut news {
|
||||||
|
info!(
|
||||||
|
"{:?} - Getting sentiment for news: {}.",
|
||||||
|
thread_type, news.headline
|
||||||
|
);
|
||||||
|
|
||||||
|
let prediction = retry(ExponentialBackoff::default(), || async {
|
||||||
|
let response = app_config
|
||||||
|
.ollama_client
|
||||||
|
.post(format!("{}/api/chat", app_config.ollama_url))
|
||||||
|
.body(
|
||||||
|
to_string(&ollama::outgoing::sentiment::Sentiment::new(
|
||||||
|
app_config.ollama_model.clone(),
|
||||||
|
&news.clone().into(),
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<ollama::incoming::sentiment::Response>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
from_str::<ollama::incoming::sentiment::Prediction>(&response.message.content)
|
||||||
|
.map_err(Into::into)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match prediction {
|
||||||
|
Ok(prediction) => {
|
||||||
|
info!(
|
||||||
|
"{:?} - Received sentiment for news: {:?}.",
|
||||||
|
thread_type, prediction
|
||||||
|
);
|
||||||
|
news.sentiment = prediction.sentiment.into();
|
||||||
|
news.confidence = prediction.confidence.into();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"{:?} - Failed to get sentiment for news: {:?}.",
|
||||||
|
thread_type, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let backfill = (news.last().unwrap().clone(), symbol.clone()).into();
|
let backfill = (news.last().unwrap().clone(), symbol.clone()).into();
|
||||||
database::news::upsert_batch(&app_config.clickhouse_client, news).await;
|
database::news::upsert_batch(&app_config.clickhouse_client, news).await;
|
||||||
database::backfills::upsert(&app_config.clickhouse_client, &thread_type, &backfill).await;
|
database::backfills::upsert(&app_config.clickhouse_client, &thread_type, &backfill).await;
|
||||||
|
@@ -27,16 +27,6 @@ pub struct Guard {
|
|||||||
pub pending_unsubscriptions: HashMap<String, Asset>,
|
pub pending_unsubscriptions: HashMap<String, Asset>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Guard {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
symbols: HashSet::new(),
|
|
||||||
pending_subscriptions: HashMap::new(),
|
|
||||||
pending_unsubscriptions: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum ThreadType {
|
pub enum ThreadType {
|
||||||
Bars(Class),
|
Bars(Class),
|
||||||
@@ -86,7 +76,11 @@ async fn init_thread(
|
|||||||
mpsc::Sender<asset_status::Message>,
|
mpsc::Sender<asset_status::Message>,
|
||||||
mpsc::Sender<backfill::Message>,
|
mpsc::Sender<backfill::Message>,
|
||||||
) {
|
) {
|
||||||
let guard = Arc::new(RwLock::new(Guard::new()));
|
let guard = Arc::new(RwLock::new(Guard {
|
||||||
|
symbols: HashSet::new(),
|
||||||
|
pending_subscriptions: HashMap::new(),
|
||||||
|
pending_unsubscriptions: HashMap::new(),
|
||||||
|
}));
|
||||||
|
|
||||||
let websocket_url = match thread_type {
|
let websocket_url = match thread_type {
|
||||||
ThreadType::Bars(Class::UsEquity) => format!(
|
ThreadType::Bars(Class::UsEquity) => format!(
|
||||||
@@ -102,7 +96,7 @@ async fn init_thread(
|
|||||||
authenticate(&app_config, &mut websocket_sender, &mut websocket_receiver).await;
|
authenticate(&app_config, &mut websocket_sender, &mut websocket_receiver).await;
|
||||||
let websocket_sender = Arc::new(Mutex::new(websocket_sender));
|
let websocket_sender = Arc::new(Mutex::new(websocket_sender));
|
||||||
|
|
||||||
let (asset_status_sender, asset_status_receiver) = mpsc::channel(100);
|
let (asset_status_sender, asset_status_receiver) = mpsc::channel(10);
|
||||||
spawn(asset_status::run(
|
spawn(asset_status::run(
|
||||||
app_config.clone(),
|
app_config.clone(),
|
||||||
thread_type,
|
thread_type,
|
||||||
@@ -111,7 +105,7 @@ async fn init_thread(
|
|||||||
websocket_sender.clone(),
|
websocket_sender.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
let (backfill_sender, backfill_receiver) = mpsc::channel(100);
|
let (backfill_sender, backfill_receiver) = mpsc::channel(10);
|
||||||
spawn(backfill::run(
|
spawn(backfill::run(
|
||||||
app_config.clone(),
|
app_config.clone(),
|
||||||
thread_type,
|
thread_type,
|
||||||
|
@@ -2,14 +2,15 @@ use super::{backfill, Guard, ThreadType};
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
database,
|
database,
|
||||||
types::{alpaca::websocket, Bar, News, Subset},
|
types::{alpaca::websocket, ollama, Bar, News, Subset},
|
||||||
};
|
};
|
||||||
|
use backoff::{future::retry, ExponentialBackoff};
|
||||||
use futures_util::{
|
use futures_util::{
|
||||||
stream::{SplitSink, SplitStream},
|
stream::{SplitSink, SplitStream},
|
||||||
SinkExt, StreamExt,
|
SinkExt, StreamExt,
|
||||||
};
|
};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use serde_json::from_str;
|
use serde_json::{from_str, to_string};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -93,6 +94,7 @@ async fn handle_websocket_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::significant_drop_tightening)]
|
#[allow(clippy::significant_drop_tightening)]
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn handle_parsed_websocket_message(
|
async fn handle_parsed_websocket_message(
|
||||||
app_config: Arc<Config>,
|
app_config: Arc<Config>,
|
||||||
thread_type: ThreadType,
|
thread_type: ThreadType,
|
||||||
@@ -179,7 +181,7 @@ async fn handle_parsed_websocket_message(
|
|||||||
database::bars::upsert(&app_config.clickhouse_client, &bar).await;
|
database::bars::upsert(&app_config.clickhouse_client, &bar).await;
|
||||||
}
|
}
|
||||||
websocket::incoming::Message::News(message) => {
|
websocket::incoming::Message::News(message) => {
|
||||||
let news = News::from(message);
|
let mut news = News::from(message);
|
||||||
let symbols = news.symbols.clone().into_iter().collect::<HashSet<_>>();
|
let symbols = news.symbols.clone().into_iter().collect::<HashSet<_>>();
|
||||||
|
|
||||||
let guard = guard.read().await;
|
let guard = guard.read().await;
|
||||||
@@ -195,6 +197,52 @@ async fn handle_parsed_websocket_message(
|
|||||||
"{:?} - Received news for {:?}: {}.",
|
"{:?} - Received news for {:?}: {}.",
|
||||||
thread_type, news.symbols, news.time_created
|
thread_type, news.symbols, news.time_created
|
||||||
);
|
);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"{:?} - Getting sentiment for news: {}.",
|
||||||
|
thread_type, news.headline
|
||||||
|
);
|
||||||
|
|
||||||
|
let prediction = retry(ExponentialBackoff::default(), || async {
|
||||||
|
let response = app_config
|
||||||
|
.ollama_client
|
||||||
|
.post(format!("{}/api/chat", app_config.ollama_url))
|
||||||
|
.body(
|
||||||
|
to_string(&ollama::outgoing::sentiment::Sentiment::new(
|
||||||
|
app_config.ollama_model.clone(),
|
||||||
|
&news.clone().into(),
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<ollama::incoming::sentiment::Response>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
from_str::<ollama::incoming::sentiment::Prediction>(&response.message.content)
|
||||||
|
.map_err(Into::into)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match prediction {
|
||||||
|
Ok(prediction) => {
|
||||||
|
info!(
|
||||||
|
"{:?} - Received sentiment for news: {:?}.",
|
||||||
|
thread_type, prediction
|
||||||
|
);
|
||||||
|
news.sentiment = prediction.sentiment.into();
|
||||||
|
news.confidence = prediction.confidence.into();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"{:?} - Failed to get sentiment for news: {:?}.",
|
||||||
|
thread_type, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
database::news::upsert(&app_config.clickhouse_client, &news).await;
|
database::news::upsert(&app_config.clickhouse_client, &news).await;
|
||||||
}
|
}
|
||||||
websocket::incoming::Message::Success(_) => {}
|
websocket::incoming::Message::Success(_) => {}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use crate::types::{self, alpaca::api::impl_from_enum};
|
use crate::types::{self, alpaca::api::impl_from_enum};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Class {
|
pub enum Class {
|
||||||
UsEquity,
|
UsEquity,
|
||||||
@@ -10,7 +10,7 @@ pub enum Class {
|
|||||||
|
|
||||||
impl_from_enum!(types::Class, Class, UsEquity, Crypto);
|
impl_from_enum!(types::Class, Class, UsEquity, Crypto);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "UPPERCASE")]
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
pub enum Exchange {
|
pub enum Exchange {
|
||||||
Amex,
|
Amex,
|
||||||
@@ -36,7 +36,7 @@ impl_from_enum!(
|
|||||||
Crypto
|
Crypto
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Active,
|
Active,
|
||||||
@@ -44,7 +44,7 @@ pub enum Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||||
pub struct Asset {
|
pub struct Asset {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub class: Class,
|
pub class: Class,
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
use crate::types;
|
use crate::types;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||||
pub struct Bar {
|
pub struct Bar {
|
||||||
#[serde(rename = "t")]
|
#[serde(rename = "t")]
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
@@ -40,7 +40,7 @@ impl From<(Bar, String)> for types::Bar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub bars: HashMap<String, Vec<Bar>>,
|
pub bars: HashMap<String, Vec<Bar>>,
|
||||||
pub next_page_token: Option<String>,
|
pub next_page_token: Option<String>,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub struct Clock {
|
pub struct Clock {
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
pub timestamp: OffsetDateTime,
|
pub timestamp: OffsetDateTime,
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
use crate::{types, utils::normalize_news_content};
|
use crate::{
|
||||||
use serde::{Deserialize, Serialize};
|
types::{
|
||||||
use serde_with::serde_as;
|
self,
|
||||||
|
news::{Confidence, Sentiment},
|
||||||
|
},
|
||||||
|
utils::news,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum ImageSize {
|
pub enum ImageSize {
|
||||||
Thumb,
|
Thumb,
|
||||||
@@ -11,14 +16,13 @@ pub enum ImageSize {
|
|||||||
Large,
|
Large,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
pub size: ImageSize,
|
pub size: ImageSize,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde_as]
|
|
||||||
pub struct News {
|
pub struct News {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
@@ -28,17 +32,11 @@ pub struct News {
|
|||||||
#[serde(rename = "updated_at")]
|
#[serde(rename = "updated_at")]
|
||||||
pub time_updated: OffsetDateTime,
|
pub time_updated: OffsetDateTime,
|
||||||
pub symbols: Vec<String>,
|
pub symbols: Vec<String>,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub headline: String,
|
||||||
pub headline: Option<String>,
|
pub author: String,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub source: String,
|
||||||
pub author: Option<String>,
|
pub summary: String,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub content: String,
|
||||||
pub source: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub summary: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub content: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub images: Vec<Image>,
|
pub images: Vec<Image>,
|
||||||
}
|
}
|
||||||
@@ -50,17 +48,16 @@ impl From<News> for types::News {
|
|||||||
time_created: news.time_created,
|
time_created: news.time_created,
|
||||||
time_updated: news.time_updated,
|
time_updated: news.time_updated,
|
||||||
symbols: news.symbols,
|
symbols: news.symbols,
|
||||||
headline: normalize_news_content(news.headline),
|
headline: news::normalize(&news.headline),
|
||||||
author: normalize_news_content(news.author),
|
author: news::normalize(&news.author),
|
||||||
source: normalize_news_content(news.source),
|
content: news::normalize(&news.content),
|
||||||
summary: normalize_news_content(news.summary),
|
sentiment: Sentiment::Neutral,
|
||||||
content: normalize_news_content(news.content),
|
confidence: Confidence::VeryUncertain,
|
||||||
url: news.url,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub news: Vec<News>,
|
pub news: Vec<News>,
|
||||||
pub next_page_token: Option<String>,
|
pub next_page_token: Option<String>,
|
||||||
|
@@ -49,23 +49,3 @@ pub struct Bar {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub page_token: Option<String>,
|
pub page_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bar {
|
|
||||||
pub const fn new(
|
|
||||||
symbols: Vec<String>,
|
|
||||||
timeframe: Duration,
|
|
||||||
start: OffsetDateTime,
|
|
||||||
end: OffsetDateTime,
|
|
||||||
limit: i64,
|
|
||||||
page_token: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
symbols,
|
|
||||||
timeframe,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
limit,
|
|
||||||
page_token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -16,25 +16,3 @@ pub struct News {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub page_token: Option<String>,
|
pub page_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl News {
|
|
||||||
pub const fn new(
|
|
||||||
symbols: Vec<String>,
|
|
||||||
start: OffsetDateTime,
|
|
||||||
end: OffsetDateTime,
|
|
||||||
limit: i64,
|
|
||||||
include_content: bool,
|
|
||||||
exclude_contentless: bool,
|
|
||||||
page_token: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
symbols,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
limit,
|
|
||||||
include_content,
|
|
||||||
exclude_contentless,
|
|
||||||
page_token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
use crate::types;
|
use crate::types;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
#[serde(rename = "t")]
|
#[serde(rename = "t")]
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub code: u16,
|
pub code: u16,
|
||||||
|
@@ -4,9 +4,9 @@ pub mod news;
|
|||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
pub mod success;
|
pub mod success;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||||
#[serde(tag = "T")]
|
#[serde(tag = "T")]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
#[serde(rename = "success")]
|
#[serde(rename = "success")]
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
use crate::{types, utils::normalize_news_content};
|
use crate::{
|
||||||
use serde::{Deserialize, Serialize};
|
types::{
|
||||||
use serde_with::serde_as;
|
self,
|
||||||
|
news::{Confidence, Sentiment},
|
||||||
|
},
|
||||||
|
utils::news,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde_as]
|
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
@@ -14,17 +18,11 @@ pub struct Message {
|
|||||||
#[serde(rename = "updated_at")]
|
#[serde(rename = "updated_at")]
|
||||||
pub time_updated: OffsetDateTime,
|
pub time_updated: OffsetDateTime,
|
||||||
pub symbols: Vec<String>,
|
pub symbols: Vec<String>,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub headline: String,
|
||||||
pub headline: Option<String>,
|
pub author: String,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub source: String,
|
||||||
pub author: Option<String>,
|
pub summary: String,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub content: String,
|
||||||
pub source: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub summary: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub content: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,12 +33,11 @@ impl From<Message> for types::News {
|
|||||||
time_created: news.time_created,
|
time_created: news.time_created,
|
||||||
time_updated: news.time_updated,
|
time_updated: news.time_updated,
|
||||||
symbols: news.symbols,
|
symbols: news.symbols,
|
||||||
headline: normalize_news_content(news.headline),
|
headline: news::normalize(&news.headline),
|
||||||
author: normalize_news_content(news.author),
|
author: news::normalize(&news.author),
|
||||||
source: normalize_news_content(news.source),
|
content: news::normalize(&news.content),
|
||||||
summary: normalize_news_content(news.summary),
|
sentiment: Sentiment::Neutral,
|
||||||
content: normalize_news_content(news.content),
|
confidence: Confidence::VeryUncertain,
|
||||||
url: news.url,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MarketMessage {
|
pub struct MarketMessage {
|
||||||
pub trades: Vec<String>,
|
pub trades: Vec<String>,
|
||||||
@@ -14,13 +14,13 @@ pub struct MarketMessage {
|
|||||||
pub cancel_errors: Option<Vec<String>>,
|
pub cancel_errors: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct NewsMessage {
|
pub struct NewsMessage {
|
||||||
pub news: Vec<String>,
|
pub news: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Market(MarketMessage),
|
Market(MarketMessage),
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(tag = "msg")]
|
#[serde(tag = "msg")]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
|
@@ -2,12 +2,6 @@ use serde::Serialize;
|
|||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
key: String,
|
pub key: String,
|
||||||
secret: String,
|
pub secret: String,
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
pub const fn new(key: String, secret: String) -> Self {
|
|
||||||
Self { key, secret }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -4,14 +4,14 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
|
|||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||||
#[repr(u8)]
|
#[repr(i8)]
|
||||||
pub enum Class {
|
pub enum Class {
|
||||||
UsEquity = 1,
|
UsEquity = 1,
|
||||||
Crypto = 2,
|
Crypto = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||||
#[repr(u8)]
|
#[repr(i8)]
|
||||||
pub enum Exchange {
|
pub enum Exchange {
|
||||||
Amex = 1,
|
Amex = 1,
|
||||||
Arca = 2,
|
Arca = 2,
|
||||||
|
@@ -10,20 +10,20 @@ pub struct Backfill {
|
|||||||
pub time: OffsetDateTime,
|
pub time: OffsetDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backfill {
|
|
||||||
pub const fn new(symbol: String, time: OffsetDateTime) -> Self {
|
|
||||||
Self { symbol, time }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Bar> for Backfill {
|
impl From<Bar> for Backfill {
|
||||||
fn from(bar: Bar) -> Self {
|
fn from(bar: Bar) -> Self {
|
||||||
Self::new(bar.symbol, bar.time)
|
Self {
|
||||||
|
symbol: bar.symbol,
|
||||||
|
time: bar.time,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(News, String)> for Backfill {
|
impl From<(News, String)> for Backfill {
|
||||||
fn from((news, symbol): (News, String)) -> Self {
|
fn from((news, symbol): (News, String)) -> Self {
|
||||||
Self::new(symbol, news.time_created)
|
Self {
|
||||||
|
symbol,
|
||||||
|
time: news.time_created,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ pub mod asset;
|
|||||||
pub mod backfill;
|
pub mod backfill;
|
||||||
pub mod bar;
|
pub mod bar;
|
||||||
pub mod news;
|
pub mod news;
|
||||||
|
pub mod ollama;
|
||||||
|
|
||||||
pub use algebraic::Subset;
|
pub use algebraic::Subset;
|
||||||
pub use asset::{Asset, Class, Exchange};
|
pub use asset::{Asset, Class, Exchange};
|
||||||
|
@@ -1,10 +1,33 @@
|
|||||||
use clickhouse::Row;
|
use clickhouse::Row;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::serde_as;
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||||
|
#[repr(i8)]
|
||||||
|
pub enum Sentiment {
|
||||||
|
VeryNegative = -3,
|
||||||
|
Negative = -2,
|
||||||
|
MildlyNegative = -1,
|
||||||
|
Neutral = 0,
|
||||||
|
MildlyPositive = 1,
|
||||||
|
Positive = 2,
|
||||||
|
VeryPositive = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||||
|
#[repr(i8)]
|
||||||
|
pub enum Confidence {
|
||||||
|
VeryUncertain = -3,
|
||||||
|
Uncertain = -2,
|
||||||
|
MildlyUncertain = -1,
|
||||||
|
Neutral = 0,
|
||||||
|
MildlyCertain = 1,
|
||||||
|
Certain = 2,
|
||||||
|
VeryCertain = 3,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Row)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Row)]
|
||||||
#[serde_as]
|
|
||||||
pub struct News {
|
pub struct News {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
#[serde(with = "clickhouse::serde::time::datetime")]
|
#[serde(with = "clickhouse::serde::time::datetime")]
|
||||||
@@ -12,16 +35,9 @@ pub struct News {
|
|||||||
#[serde(with = "clickhouse::serde::time::datetime")]
|
#[serde(with = "clickhouse::serde::time::datetime")]
|
||||||
pub time_updated: OffsetDateTime,
|
pub time_updated: OffsetDateTime,
|
||||||
pub symbols: Vec<String>,
|
pub symbols: Vec<String>,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub headline: String,
|
||||||
pub headline: Option<String>,
|
pub author: String,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub content: String,
|
||||||
pub author: Option<String>,
|
pub sentiment: Sentiment,
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
pub confidence: Confidence,
|
||||||
pub source: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub summary: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub content: Option<String>,
|
|
||||||
#[serde_as(as = "NoneAsEmptyString")]
|
|
||||||
pub url: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
15
src/types/ollama/chat.rs
Normal file
15
src/types/ollama/chat.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Role {
|
||||||
|
System,
|
||||||
|
User,
|
||||||
|
Assistant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Message {
|
||||||
|
pub role: Role,
|
||||||
|
pub content: String,
|
||||||
|
}
|
2
src/types/ollama/incoming/mod.rs
Normal file
2
src/types/ollama/incoming/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod pull;
|
||||||
|
pub mod sentiment;
|
6
src/types/ollama/incoming/pull.rs
Normal file
6
src/types/ollama/incoming/pull.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub status: String,
|
||||||
|
}
|
75
src/types/ollama/incoming/sentiment.rs
Normal file
75
src/types/ollama/incoming/sentiment.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use crate::types::{self, ollama::chat::Message};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Sentiment {
|
||||||
|
VeryNegative,
|
||||||
|
Negative,
|
||||||
|
MildlyNegative,
|
||||||
|
Neutral,
|
||||||
|
MildlyPositive,
|
||||||
|
Positive,
|
||||||
|
VeryPositive,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Sentiment> for types::news::Sentiment {
|
||||||
|
fn from(sentiment: Sentiment) -> Self {
|
||||||
|
match sentiment {
|
||||||
|
Sentiment::VeryNegative => Self::VeryNegative,
|
||||||
|
Sentiment::Negative => Self::Negative,
|
||||||
|
Sentiment::MildlyNegative => Self::MildlyNegative,
|
||||||
|
Sentiment::Neutral => Self::Neutral,
|
||||||
|
Sentiment::MildlyPositive => Self::MildlyPositive,
|
||||||
|
Sentiment::Positive => Self::Positive,
|
||||||
|
Sentiment::VeryPositive => Self::VeryPositive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Confidence {
|
||||||
|
VeryUncertain,
|
||||||
|
Uncertain,
|
||||||
|
MildlyUncertain,
|
||||||
|
Neutral,
|
||||||
|
MildlyCertain,
|
||||||
|
Certain,
|
||||||
|
VeryCertain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Confidence> for types::news::Confidence {
|
||||||
|
fn from(confidence: Confidence) -> Self {
|
||||||
|
match confidence {
|
||||||
|
Confidence::VeryUncertain => Self::VeryUncertain,
|
||||||
|
Confidence::Uncertain => Self::Uncertain,
|
||||||
|
Confidence::MildlyUncertain => Self::MildlyUncertain,
|
||||||
|
Confidence::Neutral => Self::Neutral,
|
||||||
|
Confidence::MildlyCertain => Self::MildlyCertain,
|
||||||
|
Confidence::Certain => Self::Certain,
|
||||||
|
Confidence::VeryCertain => Self::VeryCertain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
pub struct Prediction {
|
||||||
|
pub sentiment: Sentiment,
|
||||||
|
pub confidence: Confidence,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub model: String,
|
||||||
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
|
pub created_at: OffsetDateTime,
|
||||||
|
pub message: Message,
|
||||||
|
pub done: bool,
|
||||||
|
pub total_duration: i64,
|
||||||
|
pub load_duration: i64,
|
||||||
|
pub prompt_eval_duration: i64,
|
||||||
|
pub eval_count: i64,
|
||||||
|
pub eval_duration: i64,
|
||||||
|
}
|
3
src/types/ollama/mod.rs
Normal file
3
src/types/ollama/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod chat;
|
||||||
|
pub mod incoming;
|
||||||
|
pub mod outgoing;
|
2
src/types/ollama/outgoing/mod.rs
Normal file
2
src/types/ollama/outgoing/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod pull;
|
||||||
|
pub mod sentiment;
|
16
src/types/ollama/outgoing/pull.rs
Normal file
16
src/types/ollama/outgoing/pull.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Pull {
|
||||||
|
name: String,
|
||||||
|
stream: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pull {
|
||||||
|
pub const fn new(name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
stream: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
src/types/ollama/outgoing/sentiment.rs
Normal file
64
src/types/ollama/outgoing/sentiment.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use crate::types::{
|
||||||
|
self,
|
||||||
|
ollama::chat::{self, Message},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::to_string;
|
||||||
|
|
||||||
|
const PROMPT: &str = r#"You are SentimentLLama, a news classification AI. Users will input a news headline or article, and you will output a sentiment and confidence in one line of JSON.
|
||||||
|
|
||||||
|
For sentiment, pick out of ["very_negative", "negative", "mildly_negative", "neutral", "mildly_positive", "positive", "very_positive"].
|
||||||
|
|
||||||
|
For confidence, pick out of ["very_uncertain", "uncertain", "mildly_uncertain", "neutral", "mildly_certain", "certain", "very_certain"]."#;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ModelOptions {
|
||||||
|
temperature: f64,
|
||||||
|
seed: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct News {
|
||||||
|
headline: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<types::News> for News {
|
||||||
|
fn from(news: types::News) -> Self {
|
||||||
|
Self {
|
||||||
|
headline: news.headline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Sentiment {
|
||||||
|
model: String,
|
||||||
|
messages: Vec<Message>,
|
||||||
|
format: String,
|
||||||
|
stream: bool,
|
||||||
|
options: ModelOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sentiment {
|
||||||
|
pub fn new(model: String, content: &News) -> Self {
|
||||||
|
Self {
|
||||||
|
model,
|
||||||
|
messages: vec![
|
||||||
|
Message {
|
||||||
|
role: chat::Role::System,
|
||||||
|
content: PROMPT.to_string(),
|
||||||
|
},
|
||||||
|
Message {
|
||||||
|
role: chat::Role::User,
|
||||||
|
content: to_string(content).unwrap(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
format: "json".to_string(),
|
||||||
|
stream: false,
|
||||||
|
options: ModelOptions {
|
||||||
|
temperature: 0.0,
|
||||||
|
seed: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
src/utils/init.rs
Normal file
24
src/utils/init.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use crate::{config::Config, types::ollama};
|
||||||
|
use serde_json::to_string;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub async fn ollama(app_config: &Config) {
|
||||||
|
let response = app_config
|
||||||
|
.ollama_client
|
||||||
|
.post(format!("{}/api/pull", app_config.ollama_url))
|
||||||
|
.body(
|
||||||
|
to_string(&ollama::outgoing::pull::Pull::new(
|
||||||
|
app_config.ollama_model.clone(),
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.timeout(Duration::MAX)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<ollama::incoming::pull::Response>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(response.status == "success", "Failed to pull Ollama model.");
|
||||||
|
}
|
@@ -1,9 +1,8 @@
|
|||||||
pub mod cleanup;
|
pub mod cleanup;
|
||||||
|
pub mod init;
|
||||||
pub mod news;
|
pub mod news;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
|
|
||||||
pub use cleanup::cleanup;
|
|
||||||
pub use news::normalize_news_content;
|
|
||||||
pub use time::{duration_until, last_minute, FIFTEEN_MINUTES, ONE_MINUTE};
|
pub use time::{duration_until, last_minute, FIFTEEN_MINUTES, ONE_MINUTE};
|
||||||
pub use websocket::authenticate;
|
pub use websocket::authenticate;
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
use html_escape::decode_html_entities;
|
use html_escape::decode_html_entities;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
pub fn normalize_news_content(content: Option<String>) -> Option<String> {
|
pub fn normalize(content: &str) -> String {
|
||||||
content.as_ref()?;
|
|
||||||
let content = content.unwrap();
|
|
||||||
|
|
||||||
let re_tags = Regex::new("<[^>]+>").unwrap();
|
let re_tags = Regex::new("<[^>]+>").unwrap();
|
||||||
let re_spaces = Regex::new("[\\u00A0\\s]+").unwrap();
|
let re_spaces = Regex::new("[\\u00A0\\s]+").unwrap();
|
||||||
|
|
||||||
@@ -12,11 +9,5 @@ pub fn normalize_news_content(content: Option<String>) -> Option<String> {
|
|||||||
let content = re_tags.replace_all(&content, "");
|
let content = re_tags.replace_all(&content, "");
|
||||||
let content = re_spaces.replace_all(&content, " ");
|
let content = re_spaces.replace_all(&content, " ");
|
||||||
let content = decode_html_entities(&content);
|
let content = decode_html_entities(&content);
|
||||||
let content = content.trim();
|
content.trim().to_string()
|
||||||
|
|
||||||
if content.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(content.to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -28,10 +28,10 @@ pub async fn authenticate(
|
|||||||
sender
|
sender
|
||||||
.send(Message::Text(
|
.send(Message::Text(
|
||||||
to_string(&websocket::outgoing::Message::Auth(
|
to_string(&websocket::outgoing::Message::Auth(
|
||||||
websocket::outgoing::auth::Message::new(
|
websocket::outgoing::auth::Message {
|
||||||
app_config.alpaca_api_key.clone(),
|
key: app_config.alpaca_api_key.clone(),
|
||||||
app_config.alpaca_api_secret.clone(),
|
secret: app_config.alpaca_api_secret.clone(),
|
||||||
),
|
},
|
||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
))
|
))
|
||||||
|
@@ -47,10 +47,25 @@ CREATE TABLE IF NOT EXISTS qrust.news (
|
|||||||
symbols Array(LowCardinality(String)),
|
symbols Array(LowCardinality(String)),
|
||||||
headline String,
|
headline String,
|
||||||
author String,
|
author String,
|
||||||
source String,
|
|
||||||
summary String,
|
|
||||||
content String,
|
content String,
|
||||||
url String,
|
sentiment Enum(
|
||||||
|
'very_negative' = -3,
|
||||||
|
'negative' = -2,
|
||||||
|
'mildly_negative' = -1,
|
||||||
|
'neutral' = 0,
|
||||||
|
'mildly_positive' = 1,
|
||||||
|
'positive' = 2,
|
||||||
|
'very_positive' = 3
|
||||||
|
),
|
||||||
|
confidence Enum(
|
||||||
|
'very_uncertain' = -3,
|
||||||
|
'uncertain' = -2,
|
||||||
|
'mildly_uncertain' = -1,
|
||||||
|
'neutral' = 0,
|
||||||
|
'mildly_certain' = 1,
|
||||||
|
'certain' = 2,
|
||||||
|
'very_certain' = 3
|
||||||
|
),
|
||||||
INDEX index_symbols symbols TYPE bloom_filter()
|
INDEX index_symbols symbols TYPE bloom_filter()
|
||||||
)
|
)
|
||||||
ENGINE = ReplacingMergeTree()
|
ENGINE = ReplacingMergeTree()
|
||||||
|
20
support/ollama/docker-compose.yml
Normal file
20
support/ollama/docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
ollama:
|
||||||
|
image: ollama/ollama
|
||||||
|
hostname: ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ollama:/root/.ollama
|
||||||
|
ports:
|
||||||
|
- target: 11434
|
||||||
|
published: 11434
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ollama:
|
Reference in New Issue
Block a user