chore: apply clippy fixes and rustfmt configuration

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
MickyandClaude Opus 5.5 committed 2026-10-11 23:44:20 -04:00
1 parent f86c66c4d9
commit d0e7b33ffd
32 files changed
+803 -266

No files matched your search

+140 -43
View File
@@ -7,8 +7,8 @@ use egui::{Align2, Color32, Event, Key, Modifiers, Rect, RichText, Stroke, Strok
use serde::{Deserialize, Serialize};
use tr_chart::drawings::{Drawing, Tool};
use tr_chart::scene::ChartType;
use tr_congress::db::{Alert, PoliticianSummary};
use tr_congress::Trade;
use tr_congress::db::{Alert, PoliticianSummary};
use tr_core::Timeframe;
use tr_data::Quote;
@@ -124,7 +124,8 @@ fn settings_path() -> std::path::PathBuf {
impl TerminalApp {
pub fn new(ctx: &egui::Context, worker: Worker, provider: String) -> Self {
theme::apply(ctx);
let settings: Option<Settings> = std::fs::read(settings_path()).ok().and_then(|b| serde_json::from_slice(&b).ok());
let settings: Option<Settings> =
std::fs::read(settings_path()).ok().and_then(|b| serde_json::from_slice(&b).ok());
let mut app = Self {
worker,
provider,
@@ -145,7 +146,9 @@ impl TerminalApp {
pol_search: String::new(),
alerts: Vec::new(),
congress_status: String::new(),
watchlist: ["SPY", "QQQ", "AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "TSLA", "JPM"].map(String::from).to_vec(),
watchlist: ["SPY", "QQQ", "AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "TSLA", "JPM"]
.map(String::from)
.to_vec(),
quotes: HashMap::new(),
last_quotes: None,
last_feed: None,
@@ -300,7 +303,9 @@ impl TerminalApp {
}
let disc = |i: usize| std::mem::discriminant(&self.panes[i].kind);
let n = self.panes.len();
if let Some(i) = (0..n).rev().find(|&i| !matches!(self.panes[i].kind, PaneKind::Chart(_)) && (0..n).any(|j| j != i && disc(j) == disc(i))) {
if let Some(i) = (0..n).rev().find(|&i| {
!matches!(self.panes[i].kind, PaneKind::Chart(_)) && (0..n).any(|j| j != i && disc(j) == disc(i))
}) {
return i;
}
let candidates: Vec<usize> = (0..n).filter(|&i| !matches!(self.panes[i].kind, PaneKind::Chart(_))).collect();
@@ -350,7 +355,9 @@ impl TerminalApp {
for a in actions {
match a {
Action::OpenChart(s) => self.open_chart(&s),
Action::OpenTicker(s) => self.open_panel(PaneKind::Ticker { symbol: s, trades: Vec::new(), loading: true }),
Action::OpenTicker(s) => {
self.open_panel(PaneKind::Ticker { symbol: s, trades: Vec::new(), loading: true })
}
Action::OpenProfile(q) => self.open_panel(PaneKind::Profile { query: q, data: None, loading: true }),
Action::Track(q, on) => {
self.worker.send(Request::Track { query: q.clone(), on });
@@ -434,7 +441,8 @@ impl TerminalApp {
Function::ClosePane => {
if self.panes.len() > 1 {
self.panes.remove(self.focus);
let prev = Layout::ALL.into_iter().rev().find(|l| l.slots() <= self.panes.len()).unwrap_or(Layout::One);
let prev =
Layout::ALL.into_iter().rev().find(|l| l.slots() <= self.panes.len()).unwrap_or(Layout::One);
self.layout = prev;
self.fit_panes_to_layout();
}
@@ -492,23 +500,21 @@ impl TerminalApp {
}
Response::Latest(t) => self.latest = t,
Response::TickerTrades { pane, symbol, trades } => {
if let Some(p) = self.panes.iter_mut().find(|p| p.id == pane) {
if let PaneKind::Ticker { symbol: s, trades: t, loading } = &mut p.kind {
if *s == symbol {
*t = trades;
*loading = false;
}
}
if let Some(p) = self.panes.iter_mut().find(|p| p.id == pane)
&& let PaneKind::Ticker { symbol: s, trades: t, loading } = &mut p.kind
&& *s == symbol
{
*t = trades;
*loading = false;
}
}
Response::Profile { pane, profile, query } => {
if let Some(p) = self.panes.iter_mut().find(|p| p.id == pane) {
if let PaneKind::Profile { query: q, data, loading } = &mut p.kind {
if *q == query {
*data = profile;
*loading = false;
}
}
if let Some(p) = self.panes.iter_mut().find(|p| p.id == pane)
&& let PaneKind::Profile { query: q, data, loading } = &mut p.kind
&& *q == query
{
*data = profile;
*loading = false;
}
}
Response::Politicians(list) => self.politicians = list,
@@ -535,7 +541,9 @@ impl TerminalApp {
self.worker.send(Request::SearchPoliticians(String::new()));
for p in &self.panes {
match &p.kind {
PaneKind::Ticker { symbol, .. } => self.worker.send(Request::TickerTrades { pane: p.id, symbol: symbol.clone() }),
PaneKind::Ticker { symbol, .. } => {
self.worker.send(Request::TickerTrades { pane: p.id, symbol: symbol.clone() })
}
PaneKind::Chart(c) => self.worker.send(Request::Markers { pane: p.id, symbol: c.symbol.clone() }),
_ => {}
}
@@ -564,7 +572,9 @@ impl TerminalApp {
let mut out = String::new();
for e in &i.events {
match e {
Event::Key { pressed: true, modifiers, .. } => chord = modifiers.alt || modifiers.ctrl || modifiers.command,
Event::Key { pressed: true, modifiers, .. } => {
chord = modifiers.alt || modifiers.ctrl || modifiers.command
}
Event::Text(t) if !chord => out.push_str(t),
_ => {}
}
@@ -572,7 +582,8 @@ impl TerminalApp {
out
});
if !typed.is_empty() {
let starts = !self.cmd.is_empty() || typed.chars().next().is_some_and(|c| c.is_ascii_alphanumeric() || c == '^' || c == '?');
let starts = !self.cmd.is_empty()
|| typed.chars().next().is_some_and(|c| c.is_ascii_alphanumeric() || c == '^' || c == '?');
if starts {
self.cmd.push_str(&typed.to_ascii_uppercase());
self.focus_cmd = true;
@@ -605,7 +616,9 @@ impl TerminalApp {
if !is_chart {
return;
}
for (k, t) in [(Key::T, Tool::Trendline), (Key::H, Tool::Horizontal), (Key::V, Tool::Vertical), (Key::F, Tool::Fib)] {
for (k, t) in
[(Key::T, Tool::Trendline), (Key::H, Tool::Horizontal), (Key::V, Tool::Vertical), (Key::F, Tool::Fib)]
{
if pressed(Modifiers::ALT, k) {
self.with_chart(|c, _| c.start_tool(t));
}
@@ -635,7 +648,10 @@ impl TerminalApp {
if pressed(Modifiers::NONE, Key::ArrowRight) {
self.with_chart(|c, _| c.pan_bars(5.0));
}
if pressed(Modifiers::COMMAND, Key::ArrowUp) || pressed(Modifiers::NONE, Key::Plus) || pressed(Modifiers::NONE, Key::Equals) {
if pressed(Modifiers::COMMAND, Key::ArrowUp)
|| pressed(Modifiers::NONE, Key::Plus)
|| pressed(Modifiers::NONE, Key::Equals)
{
self.with_chart(|c, _| c.zoom(1.25));
}
if pressed(Modifiers::COMMAND, Key::ArrowDown) || pressed(Modifiers::NONE, Key::Minus) {
@@ -655,19 +671,41 @@ impl TerminalApp {
let clock = format!("{} {} UTC", now.format("%a %d %b %H:%M:%S"), chrono::Utc::now().format("%H:%M"));
p.text(pos2(rect.right() - 8.0, rect.top() + 14.0), Align2::RIGHT_CENTER, clock, mono(12.0), AMBER);
let unread = self.alerts.iter().filter(|a| !a.seen).count();
let right_info = format!("LAYOUT {} DATA {}{}", self.layout.label(), self.provider.to_uppercase(), if unread > 0 { format!(" ALERTS {unread}") } else { String::new() });
p.text(pos2(rect.right() - 8.0, rect.top() + 31.0), Align2::RIGHT_CENTER, right_info, mono(11.0), if unread > 0 { AMBER } else { MUTED });
let right_info = format!(
"LAYOUT {} DATA {}{}",
self.layout.label(),
self.provider.to_uppercase(),
if unread > 0 { format!(" ALERTS {unread}") } else { String::new() }
);
p.text(
pos2(rect.right() - 8.0, rect.top() + 31.0),
Align2::RIGHT_CENTER,
right_info,
mono(11.0),
if unread > 0 { AMBER } else { MUTED },
);
let cmd_rect = Rect::from_min_max(pos2(logo.right() + 8.0, rect.top() + 4.0), pos2(rect.right() - 300.0, rect.top() + 24.0));
let cmd_rect = Rect::from_min_max(
pos2(logo.right() + 8.0, rect.top() + 4.0),
pos2(rect.right() - 300.0, rect.top() + 24.0),
);
let mut child = ui.new_child(UiBuilder::new().max_rect(cmd_rect));
let id = egui::Id::new("command-line");
let te = egui::TextEdit::singleline(&mut self.cmd)
.id(id)
.font(mono(14.0))
.text_color(AMBER)
.hint_text(RichText::new("<TICKER> <GO> e.g. AAPL · NVDA CONG · POL PELOSI · RSI · 5 · LAYOUT 4").color(AMBER_DIM))
.hint_text(
RichText::new("<TICKER> <GO> e.g. AAPL · NVDA CONG · POL PELOSI · RSI · 5 · LAYOUT 4")
.color(AMBER_DIM),
)
.desired_width(cmd_rect.width())
.frame(egui::Frame::new().fill(Color32::from_rgb(20, 16, 0)).stroke(Stroke::new(1.0, AMBER_DIM)).inner_margin(vec2(6.0, 2.0)));
.frame(
egui::Frame::new()
.fill(Color32::from_rgb(20, 16, 0))
.stroke(Stroke::new(1.0, AMBER_DIM))
.inner_margin(vec2(6.0, 2.0)),
);
let resp = child.add(te);
if self.focus_cmd {
resp.request_focus();
@@ -680,7 +718,14 @@ impl TerminalApp {
self.focus_cmd = false;
}
let focused = resp.has_focus();
let (enter, esc, up, down) = ui.input(|i| (i.key_pressed(Key::Enter), i.key_pressed(Key::Escape), i.key_pressed(Key::ArrowUp), i.key_pressed(Key::ArrowDown)));
let (enter, esc, up, down) = ui.input(|i| {
(
i.key_pressed(Key::Enter),
i.key_pressed(Key::Escape),
i.key_pressed(Key::ArrowUp),
i.key_pressed(Key::ArrowDown),
)
});
if resp.lost_focus() && enter {
let input = std::mem::take(&mut self.cmd);
if !input.trim().is_empty() {
@@ -708,14 +753,30 @@ impl TerminalApp {
self.cmd = self.cmd_history[pos].clone();
}
let hint = command::hint(&self.cmd);
p.text(pos2(cmd_rect.left() + 2.0, rect.top() + 32.0), Align2::LEFT_CENTER, hint, mono(11.0), if self.cmd.is_empty() { MUTED } else { TEXT });
p.text(
pos2(cmd_rect.left() + 2.0, rect.top() + 32.0),
Align2::LEFT_CENTER,
hint,
mono(11.0),
if self.cmd.is_empty() { MUTED } else { TEXT },
);
focused
}
fn bottom_bar(&self, ui: &Ui, rect: Rect) {
let p = ui.painter_at(rect);
p.rect_filled(rect, 0.0, theme::PANEL);
let keys = [("F1", "HELP"), ("F2", "CONG"), ("F3", "POL"), ("F4", "ALRT"), ("F5", "WL"), ("F6", "SYNC"), ("TAB", "PANE"), ("ALT+T/H/V/F", "DRAW"), ("ALT+R", "RESET")];
let keys = [
("F1", "HELP"),
("F2", "CONG"),
("F3", "POL"),
("F4", "ALRT"),
("F5", "WL"),
("F6", "SYNC"),
("TAB", "PANE"),
("ALT+T/H/V/F", "DRAW"),
("ALT+R", "RESET"),
];
let mut x = rect.left() + 4.0;
for (k, label) in keys {
let g = p.layout_no_wrap(k.to_string(), mono(11.0), Color32::BLACK);
@@ -744,11 +805,22 @@ impl TerminalApp {
fn watchlist(&mut self, ui: &mut Ui) -> Vec<Action> {
let mut out = Vec::new();
ui.add(egui::Label::new(RichText::new("SYMBOL LAST CHG CHG%").font(mono(11.5)).color(AMBER).background_color(theme::HEADER)).extend());
ui.add(
egui::Label::new(
RichText::new("SYMBOL LAST CHG CHG%")
.font(mono(11.5))
.color(AMBER)
.background_color(theme::HEADER),
)
.extend(),
);
let mut remove = None;
for s in &self.watchlist {
ui.horizontal(|ui| {
let r = ui.add(egui::Label::new(RichText::new(format!("{s:<8}")).font(mono(12.5)).color(AMBER)).sense(egui::Sense::click()));
let r = ui.add(
egui::Label::new(RichText::new(format!("{s:<8}")).font(mono(12.5)).color(AMBER))
.sense(egui::Sense::click()),
);
if r.clicked() {
out.push(Action::OpenChart(s.clone()));
}
@@ -758,7 +830,11 @@ impl TerminalApp {
match self.quotes.get(s) {
Some(q) => {
let c = if q.change() >= 0.0 { theme::UP } else { theme::DOWN };
ui.label(RichText::new(format!("{:>10.2} {:>+9.2} {:>+8.2}%", q.last, q.change(), q.change_pct())).font(mono(12.5)).color(c));
ui.label(
RichText::new(format!("{:>10.2} {:>+9.2} {:>+8.2}%", q.last, q.change(), q.change_pct()))
.font(mono(12.5))
.color(c),
);
}
None => {
ui.label(RichText::new(" …").font(mono(12.5)).color(MUTED));
@@ -769,7 +845,9 @@ impl TerminalApp {
if let Some(s) = remove {
self.watchlist.retain(|x| *x != s);
}
ui.label(RichText::new("WL ADD <TICKER> · right-click to remove · click to chart").font(mono(10.5)).color(MUTED));
ui.label(
RichText::new("WL ADD <TICKER> · right-click to remove · click to chart").font(mono(10.5)).color(MUTED),
);
out
}
@@ -827,7 +905,10 @@ DATA APCA_API_KEY_ID/APCA_API_SECRET_KEY -> Alpaca; otherwise Yahoo; TR_PROVIDE
let screen = ctx.content_rect();
let p = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("toasts")));
for (k, (msg, _)) in self.toasts.iter().enumerate() {
let r = Rect::from_min_size(pos2(screen.right() - 470.0, screen.top() + 48.0 + k as f32 * 30.0), vec2(460.0, 26.0));
let r = Rect::from_min_size(
pos2(screen.right() - 470.0, screen.top() + 48.0 + k as f32 * 30.0),
vec2(460.0, 26.0),
);
p.rect_filled(r, 0.0, Color32::from_rgb(60, 40, 0));
p.rect_stroke(r, 0.0, Stroke::new(1.0, AMBER), StrokeKind::Inside);
p.text(r.left_center() + vec2(8.0, 0.0), Align2::LEFT_CENTER, format!("ALERT {msg}"), mono(12.0), AMBER);
@@ -845,7 +926,10 @@ impl eframe::App for TerminalApp {
ui.painter().rect_filled(screen, 0.0, theme::BG);
let top = Rect::from_min_size(screen.min, vec2(screen.width(), TOP_H));
let bottom = Rect::from_min_max(pos2(screen.left(), screen.bottom() - BOTTOM_H), screen.max);
let area = Rect::from_min_max(pos2(screen.left() + 2.0, top.bottom() + 2.0), pos2(screen.right() - 2.0, bottom.top() - 2.0));
let area = Rect::from_min_max(
pos2(screen.left() + 2.0, top.bottom() + 2.0),
pos2(screen.right() - 2.0, bottom.top() - 2.0),
);
self.hotkeys(&ctx);
self.top_bar(ui, top);
@@ -869,7 +953,12 @@ impl eframe::App for TerminalApp {
let mut child = ui.new_child(UiBuilder::new().max_rect(inner).id_salt(("pane", pane.id)));
child.set_clip_rect(inner);
match kind {
PaneKind::Feed => actions.extend(congress_view::feed(&mut child, &self.latest, &mut self.feed_filter, &self.congress_status)),
PaneKind::Feed => actions.extend(congress_view::feed(
&mut child,
&self.latest,
&mut self.feed_filter,
&self.congress_status,
)),
PaneKind::Ticker { symbol, trades, loading } => {
if *loading {
child.label(RichText::new("Loading…").color(MUTED));
@@ -877,8 +966,14 @@ impl eframe::App for TerminalApp {
actions.extend(congress_view::ticker_trades(&mut child, symbol, trades));
}
}
PaneKind::Profile { query, data, loading } => actions.extend(congress_view::profile(&mut child, query, data.as_deref(), *loading)),
PaneKind::Politicians => actions.extend(congress_view::politicians(&mut child, &self.politicians, &mut self.pol_search)),
PaneKind::Profile { query, data, loading } => {
actions.extend(congress_view::profile(&mut child, query, data.as_deref(), *loading))
}
PaneKind::Politicians => actions.extend(congress_view::politicians(
&mut child,
&self.politicians,
&mut self.pol_search,
)),
PaneKind::Alerts => {
let (a, seen) = congress_view::alerts(&mut child, &self.alerts);
actions.extend(a);
@@ -895,7 +990,9 @@ impl eframe::App for TerminalApp {
let wl = self.watchlist(&mut child);
actions.extend(wl);
}
let clicked = ui.input(|inp| inp.pointer.primary_pressed() && inp.pointer.interact_pos().is_some_and(|p| rect.contains(p)));
let clicked = ui.input(|inp| {
inp.pointer.primary_pressed() && inp.pointer.interact_pos().is_some_and(|p| rect.contains(p))
});
if clicked {
new_focus = Some(i);
}
+206 -41
View File
@@ -104,7 +104,13 @@ impl ChartState {
self.manual_scale = None;
self.compare_line = None;
self.markers.clear();
w.send(Request::History { pane: self.id, generation: self.generation, symbol: self.symbol.clone(), tf: self.tf, limit: initial_limit(self.tf) });
w.send(Request::History {
pane: self.id,
generation: self.generation,
symbol: self.symbol.clone(),
tf: self.tf,
limit: initial_limit(self.tf),
});
w.send(Request::Markers { pane: self.id, symbol: self.symbol.clone() });
}
@@ -215,20 +221,41 @@ impl ChartState {
fn request_indicators(&self, w: &Worker) {
if !self.bars.is_empty() {
w.send(Request::Indicators { pane: self.id, generation: self.generation, bars: self.bars.clone(), tf: self.tf, specs: self.indicators.clone() });
w.send(Request::Indicators {
pane: self.id,
generation: self.generation,
bars: self.bars.clone(),
tf: self.tf,
specs: self.indicators.clone(),
});
}
}
fn request_compare(&self, w: &Worker) {
if let (Some(sym), false) = (&self.compare, self.bars.is_empty()) {
w.send(Request::Compare { pane: self.id, generation: self.generation, symbol: sym.clone(), tf: self.tf, base: self.bars.clone() });
w.send(Request::Compare {
pane: self.id,
generation: self.generation,
symbol: sym.clone(),
tf: self.tf,
base: self.bars.clone(),
});
}
}
// ---- worker responses -------------------------------------------------------------
#[allow(clippy::too_many_arguments)]
pub fn on_history(&mut self, generation: u64, bars: Arc<Vec<Bar>>, gpu: SeriesGpu, prepended: usize, from_cache: bool, exhausted: bool, w: &Worker) {
pub fn on_history(
&mut self,
generation: u64,
bars: Arc<Vec<Bar>>,
gpu: SeriesGpu,
prepended: usize,
from_cache: bool,
exhausted: bool,
w: &Worker,
) {
if generation != self.generation {
return;
}
@@ -334,7 +361,8 @@ impl ChartState {
x += g.size().x + 12.0;
for tf in Timeframe::ALL {
let label = tf.label();
let r = Rect::from_min_size(pos2(x, header.top() + 1.0), vec2(label.len() as f32 * 7.6 + 6.0, HEADER_H - 2.0));
let r =
Rect::from_min_size(pos2(x, header.top() + 1.0), vec2(label.len() as f32 * 7.6 + 6.0, HEADER_H - 2.0));
if r.right() > header.right() - 120.0 {
break;
}
@@ -343,7 +371,13 @@ impl ChartState {
if active {
painter.rect_filled(r, 0.0, Color32::from_rgb(70, 50, 0));
}
let col = if active { AMBER } else if resp.hovered() { TEXT } else { MUTED };
let col = if active {
AMBER
} else if resp.hovered() {
TEXT
} else {
MUTED
};
painter.text(r.center(), Align2::CENTER_CENTER, label, mono(11.5), col);
if resp.clicked() {
self.set_timeframe(tf, w);
@@ -354,12 +388,19 @@ impl ChartState {
x += 8.0;
for ct in ChartType::ALL {
let label = ct.label();
let r = Rect::from_min_size(pos2(x, header.top() + 1.0), vec2(label.len() as f32 * 7.2 + 6.0, HEADER_H - 2.0));
let r =
Rect::from_min_size(pos2(x, header.top() + 1.0), vec2(label.len() as f32 * 7.2 + 6.0, HEADER_H - 2.0));
if r.right() > header.right() - 4.0 {
break;
}
let resp = ui.interact(r, ui.id().with((self.id, "ct", label)), Sense::click());
let col = if ct == self.chart_type { AMBER } else if resp.hovered() { TEXT } else { MUTED };
let col = if ct == self.chart_type {
AMBER
} else if resp.hovered() {
TEXT
} else {
MUTED
};
painter.text(r.center(), Align2::CENTER_CENTER, label, mono(11.0), col);
if resp.clicked() {
self.chart_type = ct;
@@ -374,7 +415,13 @@ impl ChartState {
for r in Range::ALL {
let lr = Rect::from_min_size(pos2(rx, range_row.top()), vec2(r.label().len() as f32 * 7.4 + 8.0, RANGE_H));
let resp = ui.interact(lr, ui.id().with((self.id, "range", r.label())), Sense::click());
painter.text(lr.center(), Align2::CENTER_CENTER, r.label(), mono(11.0), if resp.hovered() { AMBER } else { MUTED });
painter.text(
lr.center(),
Align2::CENTER_CENTER,
r.label(),
mono(11.0),
if resp.hovered() { AMBER } else { MUTED },
);
if resp.clicked() {
self.set_range(r, w);
clicked = true;
@@ -385,9 +432,20 @@ impl ChartState {
(Some(e), _, _) => format!("ERR {e}"),
(_, true, _) => "loading…".into(),
(_, _, true) => "loading history…".into(),
_ => format!("{} bars{}{}", self.bars.len(), if self.log { " LOG" } else { "" }, if self.magnet { " MAGNET" } else { "" }),
_ => format!(
"{} bars{}{}",
self.bars.len(),
if self.log { " LOG" } else { "" },
if self.magnet { " MAGNET" } else { "" }
),
};
painter.text(pos2(range_row.right() - 6.0, range_row.center().y), Align2::RIGHT_CENTER, status, mono(10.5), MUTED);
painter.text(
pos2(range_row.right() - 6.0, range_row.center().y),
Align2::RIGHT_CENTER,
status,
mono(10.5),
MUTED,
);
let body = Rect::from_min_max(pos2(rect.left(), header.bottom()), pos2(rect.right(), range_row.top()));
let plot_all = Rect::from_min_max(body.min, pos2(body.right() - AXIS_W, body.bottom() - TIME_H));
@@ -405,17 +463,21 @@ impl ChartState {
let pane_specs: Vec<usize> = (0..self.built.len()).filter(|&i| self.built[i].spec.is_pane()).collect();
let n_sub = pane_specs.len().min(3);
let sub_h = if n_sub > 0 { (plot_all.height() * 0.2).clamp(50.0, 160.0) } else { 0.0 };
let price_rect = Rect::from_min_max(plot_all.min, pos2(plot_all.right(), plot_all.bottom() - sub_h * n_sub as f32));
let price_rect =
Rect::from_min_max(plot_all.min, pos2(plot_all.right(), plot_all.bottom() - sub_h * n_sub as f32));
let len = self.bars.len();
let v = self.viewport.get_or_insert_with(|| Viewport::fit_last(len, DEFAULT_VISIBLE, price_rect.width() as f64));
let v =
self.viewport.get_or_insert_with(|| Viewport::fit_last(len, DEFAULT_VISIBLE, price_rect.width() as f64));
v.set_width(price_rect.width() as f64);
// ---- input ----
let plot_resp = ui.interact(plot_all, ui.id().with((self.id, "plot")), Sense::click_and_drag());
let axis_rect = Rect::from_min_max(pos2(price_rect.right(), price_rect.top()), pos2(body.right(), price_rect.bottom()));
let axis_rect =
Rect::from_min_max(pos2(price_rect.right(), price_rect.top()), pos2(body.right(), price_rect.bottom()));
let axis_resp = ui.interact(axis_rect, ui.id().with((self.id, "paxis")), Sense::click_and_drag());
let time_rect = Rect::from_min_max(pos2(plot_all.left(), plot_all.bottom()), pos2(plot_all.right(), body.bottom()));
let time_rect =
Rect::from_min_max(pos2(plot_all.left(), plot_all.bottom()), pos2(plot_all.right(), body.bottom()));
let time_resp = ui.interact(time_rect, ui.id().with((self.id, "taxis")), Sense::click_and_drag());
if plot_resp.clicked() || plot_resp.drag_started() || axis_resp.clicked() || time_resp.clicked() {
clicked = true;
@@ -474,12 +536,22 @@ impl ChartState {
}
// ---- price pane GPU frame ----
let mut overlays: Vec<Overlay> = self.built.iter().filter(|b| !b.spec.is_pane()).flat_map(|b| b.overlays.iter().map(|o| o.1.clone())).collect();
let mut overlays: Vec<Overlay> = self
.built
.iter()
.filter(|b| !b.spec.is_pane())
.flat_map(|b| b.overlays.iter().map(|o| o.1.clone()))
.collect();
if let Some(c) = &self.compare_line {
let (lo, hi) = self.viewport.as_ref().map(|v| v.visible_range(len)).unwrap_or((0, len));
let scale = scene::rebase_factor(&gpu.closes, c, lo, hi);
self.compare_scale = scale;
overlays.push(Overlay { line: c.clone(), color: theme::rgba(theme::SERIES[5]), kind: LineKind::Line, scale });
overlays.push(Overlay {
line: c.clone(),
color: theme::rgba(theme::SERIES[5]),
kind: LineKind::Line,
scale,
});
}
let style = Style {
up: theme::rgba(theme::UP),
@@ -542,7 +614,11 @@ impl ChartState {
p.hline(r.x_range(), r.top(), Stroke::new(1.0, theme::BORDER));
if matches!(b.spec, IndicatorSpec::Rsi(_)) {
for lvl in [30.0, 70.0] {
p.hline(r.x_range(), r.top() + so.scale.y_of(lvl) as f32, Stroke::new(1.0, Color32::from_rgb(60, 45, 80)));
p.hline(
r.x_range(),
r.top() + so.scale.y_of(lvl) as f32,
Stroke::new(1.0, Color32::from_rgb(60, 45, 80)),
);
}
}
let frame = Frame { pane: self.id * 8 + 1 + k as u64, frame_no, cmds: so.cmds };
@@ -556,7 +632,13 @@ impl ChartState {
for t in &ticks {
let y = price_rect.top() + scale.y_of(*t) as f32;
if y > price_rect.top() + 6.0 && y < price_rect.bottom() - 6.0 {
axis_painter.text(pos2(plot_all.right() + 6.0, y), Align2::LEFT_CENTER, scale::format_price(*t, dec), mono(11.0), MUTED);
axis_painter.text(
pos2(plot_all.right() + 6.0, y),
Align2::LEFT_CENTER,
scale::format_price(*t, dec),
mono(11.0),
MUTED,
);
}
}
for (r, bi, s) in &sub_rects {
@@ -565,7 +647,13 @@ impl ChartState {
for t in scale::price_ticks(s.lo, s.hi, 3) {
let y = r.top() + s.y_of(t) as f32;
if y > r.top() + 8.0 && y < r.bottom() - 4.0 {
axis_painter.text(pos2(plot_all.right() + 6.0, y), Align2::LEFT_CENTER, format!("{t:.1}"), mono(10.5), MUTED);
axis_painter.text(
pos2(plot_all.right() + 6.0, y),
Align2::LEFT_CENTER,
format!("{t:.1}"),
mono(10.5),
MUTED,
);
}
}
}
@@ -592,12 +680,22 @@ impl ChartState {
let col = if last.is_up() { theme::UP } else { theme::DOWN };
let mut xx = price_rect.left();
while xx < price_rect.right() {
price_painter.hline(xx..=(xx + 3.0).min(price_rect.right()), y, Stroke::new(1.0, col.gamma_multiply(0.6)));
price_painter.hline(
xx..=(xx + 3.0).min(price_rect.right()),
y,
Stroke::new(1.0, col.gamma_multiply(0.6)),
);
xx += 6.0;
}
let tag = Rect::from_min_size(pos2(plot_all.right() + 1.0, y - 8.0), vec2(AXIS_W - 2.0, 16.0));
axis_painter.rect_filled(tag, 0.0, col);
axis_painter.text(tag.left_center() + vec2(5.0, 0.0), Align2::LEFT_CENTER, scale::format_price(last.close, dec.max(2)), mono(11.0), Color32::BLACK);
axis_painter.text(
tag.left_center() + vec2(5.0, 0.0),
Align2::LEFT_CENTER,
scale::format_price(last.close, dec.max(2)),
mono(11.0),
Color32::BLACK,
);
}
}
@@ -622,7 +720,13 @@ impl ChartState {
let price = scale.price_at((p.y - price_rect.top()) as f64);
let tag = Rect::from_min_size(pos2(plot_all.right() + 1.0, p.y - 8.0), vec2(AXIS_W - 2.0, 16.0));
axis_painter.rect_filled(tag, 0.0, Color32::from_gray(70));
axis_painter.text(tag.left_center() + vec2(5.0, 0.0), Align2::LEFT_CENTER, scale::format_price(price, dec.max(2)), mono(11.0), TEXT);
axis_painter.text(
tag.left_center() + vec2(5.0, 0.0),
Align2::LEFT_CENTER,
scale::format_price(price, dec.max(2)),
mono(11.0),
TEXT,
);
}
if let Some(i) = hovered_bar {
let label = scale::crosshair_label(self.bars[i].ts, self.tf);
@@ -668,9 +772,27 @@ impl ChartState {
let mut ly = y + 16.0;
let value_at = |l: &LineRef| l.pyramid.data.get(i).map(|p| p[1] as f64).filter(|v| v.is_finite());
for bi in self.built.iter().filter(|b| !b.spec.is_pane()) {
let vals: Vec<String> = bi.overlays.iter().filter_map(|(_, o)| value_at(&o.line).map(|v| format!("{v:.2}"))).collect();
let c = bi.overlays.get(bi.overlays.len() / 2).map(|o| Color32::from_rgba_unmultiplied((o.1.color[0] * 255.0) as u8, (o.1.color[1] * 255.0) as u8, (o.1.color[2] * 255.0) as u8, 255)).unwrap_or(TEXT);
p.text(pos2(price_rect.left() + 6.0, ly), Align2::LEFT_TOP, format!("{} {}", bi.spec.label(), vals.join(" ")), mono(11.0), c);
let vals: Vec<String> =
bi.overlays.iter().filter_map(|(_, o)| value_at(&o.line).map(|v| format!("{v:.2}"))).collect();
let c = bi
.overlays
.get(bi.overlays.len() / 2)
.map(|o| {
Color32::from_rgba_unmultiplied(
(o.1.color[0] * 255.0) as u8,
(o.1.color[1] * 255.0) as u8,
(o.1.color[2] * 255.0) as u8,
255,
)
})
.unwrap_or(TEXT);
p.text(
pos2(price_rect.left() + 6.0, ly),
Align2::LEFT_TOP,
format!("{} {}", bi.spec.label(), vals.join(" ")),
mono(11.0),
c,
);
ly += 14.0;
}
if let (Some(sym), Some(line)) = (&self.compare, &self.compare_line) {
@@ -685,18 +807,30 @@ impl ChartState {
ly += 14.0;
}
if let Some(tool) = self.draw.tool {
let msg = if self.draw.pending.is_some() { "click second point (Esc cancels)" } else { "click to place (Esc cancels)" };
p.text(pos2(price_rect.left() + 6.0, ly), Align2::LEFT_TOP, format!("{}: {msg}", tool.label()), mono(11.0), AMBER);
let msg = if self.draw.pending.is_some() {
"click second point (Esc cancels)"
} else {
"click to place (Esc cancels)"
};
p.text(
pos2(price_rect.left() + 6.0, ly),
Align2::LEFT_TOP,
format!("{}: {msg}", tool.label()),
mono(11.0),
AMBER,
);
}
for (r, bi, _) in sub_rects {
let b = &self.built[*bi];
let vals: Vec<String> = b.overlays.iter().filter_map(|(n, o)| value_at(&o.line).map(|v| format!("{n} {v:.2}"))).collect();
let vals: Vec<String> =
b.overlays.iter().filter_map(|(n, o)| value_at(&o.line).map(|v| format!("{n} {v:.2}"))).collect();
ui.painter_at(*r).text(r.left_top() + vec2(90.0, 3.0), Align2::LEFT_TOP, vals.join(" "), mono(10.5), TEXT);
}
}
fn drawing_shapes(&self, d: &Drawing, rect: Rect, v: &Viewport, s: &PriceScale) -> Vec<(Shape, Option<String>)> {
let px = |a: &Anchor| [rect.left() as f64 + v.x_of(self.index_of_ts(a.ts)), rect.top() as f64 + s.y_of(a.price)];
let px =
|a: &Anchor| [rect.left() as f64 + v.x_of(self.index_of_ts(a.ts)), rect.top() as f64 + s.y_of(a.price)];
match d.tool {
Tool::Trendline => vec![(Shape::Segment(px(&d.a), px(&d.b)), None)],
Tool::Horizontal => vec![(Shape::HLine(px(&d.a)[1]), Some(format!("{:.2}", d.a.price)))],
@@ -715,15 +849,23 @@ impl ChartState {
}
}
fn paint_drawings(&mut self, ui: &Ui, rect: Rect, v: &Viewport, s: &PriceScale, hover: Option<Pos2>, resp: &egui::Response) {
fn paint_drawings(
&mut self,
ui: &Ui,
rect: Rect,
v: &Viewport,
s: &PriceScale,
hover: Option<Pos2>,
resp: &egui::Response,
) {
let p = ui.painter_at(rect);
let pointer_anchor = |this: &Self, pos: Pos2| -> Anchor {
let idx = v.index_at((pos.x - rect.left()) as f64);
let mut price = s.price_at((pos.y - rect.top()) as f64);
if this.magnet {
if let Some(b) = this.bars.get(idx.floor().max(0.0) as usize) {
price = drawings::magnet(price, [b.open, b.high, b.low, b.close]);
}
if this.magnet
&& let Some(b) = this.bars.get(idx.floor().max(0.0) as usize)
{
price = drawings::magnet(price, [b.open, b.high, b.low, b.close]);
}
Anchor { ts: this.ts_of_index(idx), price }
};
@@ -753,7 +895,8 @@ impl ChartState {
});
}
let mut all: Vec<(Drawing, bool)> = self.drawings.iter().cloned().enumerate().map(|(i, d)| (d, self.draw.selected == Some(i))).collect();
let mut all: Vec<(Drawing, bool)> =
self.drawings.iter().cloned().enumerate().map(|(i, d)| (d, self.draw.selected == Some(i))).collect();
if let (Some(tool), Some(first), Some(h)) = (self.draw.tool, self.draw.pending, hover) {
all.push((Drawing::new(tool, first, pointer_anchor(self, h)), true));
}
@@ -765,7 +908,13 @@ impl ChartState {
Shape::Segment(a, b) => {
p.line_segment([pos2(a[0] as f32, a[1] as f32), pos2(b[0] as f32, b[1] as f32)], stroke);
if let Some(l) = label {
p.text(pos2(a[0] as f32 + 3.0, a[1] as f32 - 1.0), Align2::LEFT_BOTTOM, l, mono(10.0), color);
p.text(
pos2(a[0] as f32 + 3.0, a[1] as f32 - 1.0),
Align2::LEFT_BOTTOM,
l,
mono(10.0),
color,
);
}
if selected && d.tool == Tool::Trendline {
for q in [a, b] {
@@ -776,7 +925,13 @@ impl ChartState {
Shape::HLine(y) => {
p.hline(rect.x_range(), y as f32, stroke);
if let Some(l) = label {
p.text(pos2(rect.right() - 4.0, y as f32 - 1.0), Align2::RIGHT_BOTTOM, l, mono(10.0), color);
p.text(
pos2(rect.right() - 4.0, y as f32 - 1.0),
Align2::RIGHT_BOTTOM,
l,
mono(10.0),
color,
);
}
}
Shape::VLine(x) => {
@@ -810,7 +965,11 @@ impl ChartState {
let n = stack.entry((i, sell)).or_insert(0);
let x = rect.left() + v.x_center(i) as f32;
let off = 6.0 + r + *n as f32 * (2.0 * r + 1.0);
let y = if sell { rect.top() + s.y_of(bar.high) as f32 - off } else { rect.top() + s.y_of(bar.low) as f32 + off };
let y = if sell {
rect.top() + s.y_of(bar.high) as f32 - off
} else {
rect.top() + s.y_of(bar.low) as f32 + off
};
*n += 1;
let c = Pos2::new(x, y);
let col = theme::party_color(t.party);
@@ -859,7 +1018,10 @@ fn dashed_v(p: &egui::Painter, x: f32, y: egui::Rangef, s: Stroke) {
/// Hover card for a congressional trade marker, including the disclosure lag.
fn hover_card(ui: &Ui, at: Pos2, t: &Trade) {
let lines = [
(format!("{} ({}-{})", t.politician, t.party.code(), t.state.as_deref().unwrap_or("??")), theme::party_color(t.party)),
(
format!("{} ({}-{})", t.politician, t.party.code(), t.state.as_deref().unwrap_or("??")),
theme::party_color(t.party),
),
(format!("{} · {}", t.chamber.as_str(), t.owner.as_deref().unwrap_or("Self")), MUTED),
(
format!(
@@ -878,7 +1040,10 @@ fn hover_card(ui: &Ui, at: Pos2, t: &Trade) {
),
(format!("Traded {}", t.tx_date), TEXT),
(format!("Filed {}", t.filed_date), TEXT),
(format!("Disclosure lag {} days", t.disclosure_lag_days()), if t.disclosure_lag_days() > 45 { theme::DOWN } else { AMBER }),
(
format!("Disclosure lag {} days", t.disclosure_lag_days()),
if t.disclosure_lag_days() > 45 { theme::DOWN } else { AMBER },
),
];
let w = lines.iter().map(|(l, _)| l.len()).max().unwrap_or(10) as f32 * 7.2 + 14.0;
let h = lines.len() as f32 * 14.0 + 8.0;
+44 -25
View File
@@ -38,7 +38,9 @@ impl IndicatorSpec {
pub fn same_kind(&self, other: &IndicatorSpec) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
&& match (self, other) {
(IndicatorSpec::Sma(a), IndicatorSpec::Sma(b)) | (IndicatorSpec::Ema(a), IndicatorSpec::Ema(b)) => a == b,
(IndicatorSpec::Sma(a), IndicatorSpec::Sma(b)) | (IndicatorSpec::Ema(a), IndicatorSpec::Ema(b)) => {
a == b
}
_ => true,
}
}
@@ -66,7 +68,8 @@ pub enum Range {
}
impl Range {
pub const ALL: [Range; 9] = [Range::D1, Range::D5, Range::M1, Range::M3, Range::M6, Range::Ytd, Range::Y1, Range::Y5, Range::All];
pub const ALL: [Range; 9] =
[Range::D1, Range::D5, Range::M1, Range::M3, Range::M6, Range::Ytd, Range::Y1, Range::Y5, Range::All];
pub fn label(self) -> &'static str {
match self {
@@ -167,9 +170,10 @@ fn parse_function(head: &str, rest: &[&str]) -> Option<Function> {
"SMA" => Function::Indicator(IndicatorSpec::Sma(num(rest.first()).unwrap_or(20))),
"EMA" => Function::Indicator(IndicatorSpec::Ema(num(rest.first()).unwrap_or(20))),
"VWAP" => Function::Indicator(IndicatorSpec::Vwap),
"BB" | "BOLL" | "BOLLINGER" => {
Function::Indicator(IndicatorSpec::Bollinger(num(rest.first()).unwrap_or(20), num(rest.get(1)).unwrap_or(2.0)))
}
"BB" | "BOLL" | "BOLLINGER" => Function::Indicator(IndicatorSpec::Bollinger(
num(rest.first()).unwrap_or(20),
num(rest.get(1)).unwrap_or(2.0),
)),
"RSI" => Function::Indicator(IndicatorSpec::Rsi(num(rest.first()).unwrap_or(14))),
"MACD" => Function::Indicator(IndicatorSpec::Macd(
num(rest.first()).unwrap_or(12),
@@ -198,13 +202,21 @@ fn parse_function(head: &str, rest: &[&str]) -> Option<Function> {
Some("DEL") | Some("RM") | Some("REMOVE") => Function::WatchRemove(words()[1..].to_vec()),
_ => Function::Watchlist,
},
"LAYOUT" | "GRID" => Function::Layout(rest.first().map(|s| s.to_ascii_uppercase()).unwrap_or_else(|| "1".into())),
"LAYOUT" | "GRID" => {
Function::Layout(rest.first().map(|s| s.to_ascii_uppercase()).unwrap_or_else(|| "1".into()))
}
"NEW" => Function::NewPane,
"CLOSE" => Function::ClosePane,
"HELP" | "?" | "H" => Function::Help,
"RANGE" => Function::Range(Range::parse(rest.first()?)?),
// Timeframes win where spellings overlap (`1D`, `1M`); otherwise try a range (`5D`, `YTD`).
_ => return head.parse::<Timeframe>().ok().map(Function::Timeframe).or_else(|| Range::parse(&up).map(Function::Range)),
_ => {
return head
.parse::<Timeframe>()
.ok()
.map(Function::Timeframe)
.or_else(|| Range::parse(&up).map(Function::Range));
}
})
}
@@ -218,10 +230,7 @@ fn looks_like_ticker(s: &str) -> bool {
/// Parse one command line. Returns `None` for empty or unrecognised input.
pub fn parse(input: &str) -> Option<Command> {
let tokens: Vec<&str> = input
.split_whitespace()
.filter(|t| !t.eq_ignore_ascii_case("<GO>"))
.collect();
let tokens: Vec<&str> = input.split_whitespace().filter(|t| !t.eq_ignore_ascii_case("<GO>")).collect();
let (&head, rest) = tokens.split_first()?;
// A function keyword (or timeframe) wins over a ticker of the same spelling,
// except for single letters that are common tickers when followed by nothing.
@@ -232,7 +241,8 @@ pub fn parse(input: &str) -> Option<Command> {
return None;
}
let symbol = head.to_ascii_uppercase().replace('/', ".");
let rest: Vec<&str> = rest.iter().copied().filter(|t| !MARKET_SECTORS.contains(&t.to_ascii_uppercase().as_str())).collect();
let rest: Vec<&str> =
rest.iter().copied().filter(|t| !MARKET_SECTORS.contains(&t.to_ascii_uppercase().as_str())).collect();
match rest.split_first() {
None => Some(Command::Symbol(symbol)),
Some((&f, args)) => parse_function(f, args).map(|func| Command::SymbolFunction(symbol, func)),
@@ -252,17 +262,20 @@ pub fn hint(input: &str) -> String {
fn describe(f: &Function) -> String {
match f {
Function::Timeframe(tf) => format!("Interval {}", match tf {
Timeframe::M1 => "1 minute",
Timeframe::M5 => "5 minutes",
Timeframe::M15 => "15 minutes",
Timeframe::M30 => "30 minutes",
Timeframe::H1 => "1 hour",
Timeframe::H4 => "4 hours",
Timeframe::D1 => "1 day",
Timeframe::W1 => "1 week",
Timeframe::MN1 => "1 month",
}),
Function::Timeframe(tf) => format!(
"Interval {}",
match tf {
Timeframe::M1 => "1 minute",
Timeframe::M5 => "5 minutes",
Timeframe::M15 => "15 minutes",
Timeframe::M30 => "30 minutes",
Timeframe::H1 => "1 hour",
Timeframe::H4 => "4 hours",
Timeframe::D1 => "1 day",
Timeframe::W1 => "1 week",
Timeframe::MN1 => "1 month",
}
),
Function::ChartType(c) => format!("Chart type {}", c.label()),
Function::Indicator(i) => format!("Toggle {}", i.label()),
Function::ClearIndicators => "Remove all indicators".into(),
@@ -325,14 +338,20 @@ mod tests {
assert_eq!(parse("FIB"), Some(Command::Function(Function::Draw(Tool::Fib))));
assert_eq!(parse("CLEAR DRAW"), Some(Command::Function(Function::ClearDrawings)));
assert_eq!(parse("POL Nancy Pelosi"), Some(Command::Function(Function::Politician("Nancy Pelosi".into()))));
assert_eq!(parse("WL ADD msft nvda"), Some(Command::Function(Function::WatchAdd(vec!["MSFT".into(), "NVDA".into()]))));
assert_eq!(
parse("WL ADD msft nvda"),
Some(Command::Function(Function::WatchAdd(vec!["MSFT".into(), "NVDA".into()])))
);
assert_eq!(parse("LAYOUT 4"), Some(Command::Function(Function::Layout("4".into()))));
}
#[test]
fn symbol_with_function() {
assert_eq!(parse("NVDA CONG <GO>"), Some(Command::SymbolFunction("NVDA".into(), Function::Congress)));
assert_eq!(parse("AAPL US EQUITY 1W"), Some(Command::SymbolFunction("AAPL".into(), Function::Timeframe(Timeframe::W1))));
assert_eq!(
parse("AAPL US EQUITY 1W"),
Some(Command::SymbolFunction("AAPL".into(), Function::Timeframe(Timeframe::W1)))
);
assert_eq!(parse("AAPL WHATEVER"), None);
}
+100 -42
View File
@@ -70,11 +70,20 @@ fn plain(ui: &mut Ui, text: impl Into<String>, color: Color32) {
/// Column header row; never wraps (narrow panes clip instead, like a terminal).
fn header(ui: &mut Ui, text: &str) {
ui.add(egui::Label::new(RichText::new(text).font(mono(11.5)).color(AMBER).background_color(theme::HEADER)).extend());
ui.add(
egui::Label::new(RichText::new(text).font(mono(11.5)).color(AMBER).background_color(theme::HEADER)).extend(),
);
}
/// One trade row. `show_politician` / `show_ticker` drop redundant columns.
fn trade_row(ui: &mut Ui, t: &Trade, show_politician: bool, show_ticker: bool, extra: Option<(String, Color32)>, out: &mut Vec<Action>) {
fn trade_row(
ui: &mut Ui,
t: &Trade,
show_politician: bool,
show_ticker: bool,
extra: Option<(String, Color32)>,
out: &mut Vec<Action>,
) {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
plain(ui, t.filed_date.format("%m/%d/%y").to_string(), MUTED);
@@ -87,7 +96,11 @@ fn trade_row(ui: &mut Ui, t: &Trade, show_politician: bool, show_ticker: bool, e
if r.clicked() {
out.push(Action::OpenProfile(t.politician.clone()));
}
plain(ui, format!("{}-{:<2}", t.party.code(), t.state.as_deref().unwrap_or("")), theme::party_color(t.party));
plain(
ui,
format!("{}-{:<2}", t.party.code(), t.state.as_deref().unwrap_or("")),
theme::party_color(t.party),
);
plain(ui, if t.chamber == Chamber::House { "H" } else { "S" }, MUTED);
}
let (tx, c) = tx_text(t.tx_type);
@@ -119,7 +132,9 @@ fn trade_row(ui: &mut Ui, t: &Trade, show_politician: bool, show_ticker: bool, e
pub fn feed(ui: &mut Ui, trades: &[Trade], filter: &mut FeedFilter, status: &str) -> Vec<Action> {
let mut out = Vec::new();
ui.horizontal(|ui| {
let chip = |ui: &mut Ui, label: &str, on: bool| ui.selectable_label(on, RichText::new(label).font(mono(11.5))).clicked();
let chip = |ui: &mut Ui, label: &str, on: bool| {
ui.selectable_label(on, RichText::new(label).font(mono(11.5))).clicked()
};
if chip(ui, "ALL", *filter == FeedFilter::default()) {
*filter = FeedFilter::default();
}
@@ -164,31 +179,54 @@ pub fn ticker_trades(ui: &mut Ui, symbol: &str, trades: &[Trade]) -> Vec<Action>
let mut people: Vec<&str> = trades.iter().map(|t| t.politician.as_str()).collect();
people.sort();
people.dedup();
let buy_vol: f64 = trades.iter().filter(|t| t.tx_type.is_buy()).filter_map(|t| t.amount.map(|a| a.midpoint())).sum();
let sell_vol: f64 = trades.iter().filter(|t| t.tx_type.is_sell()).filter_map(|t| t.amount.map(|a| a.midpoint())).sum();
let buy_vol: f64 =
trades.iter().filter(|t| t.tx_type.is_buy()).filter_map(|t| t.amount.map(|a| a.midpoint())).sum();
let sell_vol: f64 =
trades.iter().filter(|t| t.tx_type.is_sell()).filter_map(|t| t.amount.map(|a| a.midpoint())).sum();
ui.horizontal(|ui| {
let r = clickable(ui, format!("{symbol} US Equity"), AMBER);
if r.clicked() {
out.push(Action::OpenChart(symbol.to_string()));
}
plain(ui, format!("{} members · {buys} buys / {sells} sells · est. net ${:+.1}M", people.len(), (buy_vol - sell_vol) / 1e6), TEXT);
plain(
ui,
format!(
"{} members · {buys} buys / {sells} sells · est. net ${:+.1}M",
people.len(),
(buy_vol - sell_vol) / 1e6
),
TEXT,
);
});
header(ui, "FILED TRADED LAG POLITICIAN PTY C TYPE AMOUNT OWNER ASSET");
if trades.is_empty() {
plain(ui, format!("No congressional trades disclosed for {symbol}."), MUTED);
}
ScrollArea::both().id_salt(("ticker", symbol)).auto_shrink([false, false]).show_rows(ui, 16.0, trades.len(), |ui, range| {
for t in &trades[range] {
trade_row(ui, t, true, false, None, &mut out);
}
});
ScrollArea::both().id_salt(("ticker", symbol)).auto_shrink([false, false]).show_rows(
ui,
16.0,
trades.len(),
|ui, range| {
for t in &trades[range] {
trade_row(ui, t, true, false, None, &mut out);
}
},
);
out
}
pub fn profile(ui: &mut Ui, query: &str, p: Option<&Profile>, loading: bool) -> Vec<Action> {
let mut out = Vec::new();
let Some(p) = p else {
plain(ui, if loading { format!("Loading profile for '{query}'…") } else { format!("No politician matches '{query}'. Try POL with a last name.") }, MUTED);
plain(
ui,
if loading {
format!("Loading profile for '{query}'…")
} else {
format!("No politician matches '{query}'. Try POL with a last name.")
},
MUTED,
);
return out;
};
let pol = &p.politician;
@@ -211,7 +249,11 @@ pub fn profile(ui: &mut Ui, query: &str, p: Option<&Profile>, loading: bool) ->
TEXT,
);
let label = if p.tracked { "★ TRACKED" } else { "☆ TRACK" };
if ui.button(RichText::new(label).font(mono(12.0)).color(AMBER)).on_hover_text("Alert me when this politician files a new trade").clicked() {
if ui
.button(RichText::new(label).font(mono(12.0)).color(AMBER))
.on_hover_text("Alert me when this politician files a new trade")
.clicked()
{
out.push(Action::Track(pol.id.to_string(), !p.tracked));
}
});
@@ -233,7 +275,7 @@ pub fn profile(ui: &mut Ui, query: &str, p: Option<&Profile>, loading: bool) ->
}
}
let mut top: Vec<(&str, usize)> = counts.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1));
top.sort_by_key(|t| std::cmp::Reverse(t.1));
ui.horizontal_wrapped(|ui| {
plain(ui, "Most traded:", MUTED);
for (tk, n) in top.into_iter().take(12) {
@@ -244,12 +286,20 @@ pub fn profile(ui: &mut Ui, query: &str, p: Option<&Profile>, loading: bool) ->
});
header(ui, "FILED TRADED LAG TYPE TICKER AMOUNT RETURN OWNER ASSET");
let ret_by_id: std::collections::HashMap<i64, f64> = p.returns.iter().map(|r| (r.trade_id, r.ret)).collect();
ScrollArea::both().id_salt(("profile", pol.id)).auto_shrink([false, false]).show_rows(ui, 16.0, p.trades.len(), |ui, range| {
for t in &p.trades[range] {
let extra = ret_by_id.get(&t.id).map(|r| (format!("{:>+6.1}%", r * 100.0), if *r >= 0.0 { theme::UP } else { theme::DOWN })).unwrap_or((" n/a ".into(), MUTED));
trade_row(ui, t, false, true, Some(extra), &mut out);
}
});
ScrollArea::both().id_salt(("profile", pol.id)).auto_shrink([false, false]).show_rows(
ui,
16.0,
p.trades.len(),
|ui, range| {
for t in &p.trades[range] {
let extra = ret_by_id
.get(&t.id)
.map(|r| (format!("{:>+6.1}%", r * 100.0), if *r >= 0.0 { theme::UP } else { theme::DOWN }))
.unwrap_or((" n/a ".into(), MUTED));
trade_row(ui, t, false, true, Some(extra), &mut out);
}
},
);
out
}
@@ -262,26 +312,32 @@ pub fn politicians(ui: &mut Ui, list: &[PoliticianSummary], search: &mut String)
});
header(ui, "TRK POLITICIAN PTY ST CH TRADES LAST FILED");
let q = search.to_ascii_lowercase();
let rows: Vec<&PoliticianSummary> = list.iter().filter(|p| q.is_empty() || p.politician.name.to_ascii_lowercase().contains(&q)).collect();
ScrollArea::both().id_salt("politicians").auto_shrink([false, false]).show_rows(ui, 16.0, rows.len(), |ui, range| {
for s in &rows[range] {
let p = &s.politician;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
if clickable(ui, if s.tracked { " ★ ".into() } else { " ☆ ".into() }, AMBER).clicked() {
out.push(Action::Track(p.id.to_string(), !s.tracked));
}
if clickable(ui, truncate(&p.name, 24), theme::party_color(p.party)).clicked() {
out.push(Action::OpenProfile(p.id.to_string()));
}
plain(ui, format!("{:<4}", p.party.code()), theme::party_color(p.party));
plain(ui, format!("{:<3}", p.state.as_deref().unwrap_or("")), TEXT);
plain(ui, format!("{:<7}", p.chamber.as_str()), MUTED);
plain(ui, format!("{:>6}", s.trades), TEXT);
plain(ui, s.last_filed.map(|d| d.to_string()).unwrap_or_default(), MUTED);
});
}
});
let rows: Vec<&PoliticianSummary> =
list.iter().filter(|p| q.is_empty() || p.politician.name.to_ascii_lowercase().contains(&q)).collect();
ScrollArea::both().id_salt("politicians").auto_shrink([false, false]).show_rows(
ui,
16.0,
rows.len(),
|ui, range| {
for s in &rows[range] {
let p = &s.politician;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
if clickable(ui, if s.tracked { " ★ ".into() } else { " ☆ ".into() }, AMBER).clicked() {
out.push(Action::Track(p.id.to_string(), !s.tracked));
}
if clickable(ui, truncate(&p.name, 24), theme::party_color(p.party)).clicked() {
out.push(Action::OpenProfile(p.id.to_string()));
}
plain(ui, format!("{:<4}", p.party.code()), theme::party_color(p.party));
plain(ui, format!("{:<3}", p.state.as_deref().unwrap_or("")), TEXT);
plain(ui, format!("{:<7}", p.chamber.as_str()), MUTED);
plain(ui, format!("{:>6}", s.trades), TEXT);
plain(ui, s.last_filed.map(|d| d.to_string()).unwrap_or_default(), MUTED);
});
}
},
);
out
}
@@ -300,7 +356,9 @@ pub fn alerts(ui: &mut Ui, alerts: &[Alert]) -> (Vec<Action>, bool) {
ScrollArea::vertical().id_salt("alerts").auto_shrink([false, false]).show(ui, |ui| {
for a in alerts {
ui.horizontal(|ui| {
let ts = chrono::DateTime::from_timestamp(a.created_at, 0).map(|d| d.format("%Y-%m-%d %H:%M").to_string()).unwrap_or_default();
let ts = chrono::DateTime::from_timestamp(a.created_at, 0)
.map(|d| d.format("%Y-%m-%d %H:%M").to_string())
.unwrap_or_default();
plain(ui, ts, if a.seen { MUTED } else { AMBER });
if clickable(ui, truncate(&a.politician, 24), TEXT).clicked() {
out.push(Action::OpenProfile(a.politician_id.to_string()));
+2 -1
View File
@@ -15,7 +15,8 @@ pub enum Layout {
}
impl Layout {
pub const ALL: [Layout; 6] = [Layout::One, Layout::TwoCols, Layout::TwoRows, Layout::MainRight2, Layout::Grid2x2, Layout::Grid3x2];
pub const ALL: [Layout; 6] =
[Layout::One, Layout::TwoCols, Layout::TwoRows, Layout::MainRight2, Layout::Grid2x2, Layout::Grid3x2];
pub fn parse(s: &str) -> Option<Self> {
Some(match s.trim().to_ascii_uppercase().as_str() {
+9 -5
View File
@@ -14,7 +14,10 @@ use std::time::Instant;
fn main() -> eframe::Result {
let t0 = Instant::now();
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info,wgpu_core=warn,wgpu_hal=warn,naga=warn".into()))
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,wgpu_core=warn,wgpu_hal=warn,naga=warn".into()),
)
.init();
let options = eframe::NativeOptions {
@@ -30,10 +33,11 @@ fn main() -> eframe::Result {
options,
Box::new(move |cc| {
let rs = cc.wgpu_render_state.as_ref().ok_or("terminal-rs requires the wgpu renderer")?;
rs.renderer
.write()
.callback_resources
.insert(tr_chart::gpu::ChartRenderer::new(&rs.device, rs.target_format, 1));
rs.renderer.write().callback_resources.insert(tr_chart::gpu::ChartRenderer::new(
&rs.device,
rs.target_format,
1,
));
let provider = tr_data::from_env();
let name = provider.name().to_string();
let worker = worker::Worker::spawn(cc.egui_ctx.clone(), provider)?;
+7 -1
View File
@@ -66,7 +66,13 @@ pub fn apply(ctx: &egui::Context) {
v.widgets.active.bg_fill = Color32::from_rgb(80, 60, 0);
v.window_corner_radius = 0.into();
v.menu_corner_radius = 0.into();
for w in [&mut v.widgets.noninteractive, &mut v.widgets.inactive, &mut v.widgets.hovered, &mut v.widgets.active, &mut v.widgets.open] {
for w in [
&mut v.widgets.noninteractive,
&mut v.widgets.inactive,
&mut v.widgets.hovered,
&mut v.widgets.active,
&mut v.widgets.open,
] {
w.corner_radius = 0.into();
}
ctx.set_visuals(v);
+140 -29
View File
@@ -23,18 +23,57 @@ use crate::theme;
pub type PaneId = u64;
pub enum Request {
History { pane: PaneId, generation: u64, symbol: String, tf: Timeframe, limit: usize },
History {
pane: PaneId,
generation: u64,
symbol: String,
tf: Timeframe,
limit: usize,
},
/// Prepend older history to `existing`.
MoreHistory { pane: PaneId, generation: u64, symbol: String, tf: Timeframe, existing: Arc<Vec<Bar>>, limit: usize },
Indicators { pane: PaneId, generation: u64, bars: Arc<Vec<Bar>>, tf: Timeframe, specs: Vec<IndicatorSpec> },
Compare { pane: PaneId, generation: u64, symbol: String, tf: Timeframe, base: Arc<Vec<Bar>> },
Markers { pane: PaneId, symbol: String },
MoreHistory {
pane: PaneId,
generation: u64,
symbol: String,
tf: Timeframe,
existing: Arc<Vec<Bar>>,
limit: usize,
},
Indicators {
pane: PaneId,
generation: u64,
bars: Arc<Vec<Bar>>,
tf: Timeframe,
specs: Vec<IndicatorSpec>,
},
Compare {
pane: PaneId,
generation: u64,
symbol: String,
tf: Timeframe,
base: Arc<Vec<Bar>>,
},
Markers {
pane: PaneId,
symbol: String,
},
Quotes(Vec<String>),
Latest { limit: usize },
TickerTrades { pane: PaneId, symbol: String },
Profile { pane: PaneId, query: String },
Latest {
limit: usize,
},
TickerTrades {
pane: PaneId,
symbol: String,
},
Profile {
pane: PaneId,
query: String,
},
SearchPoliticians(String),
Track { query: String, on: bool },
Track {
query: String,
on: bool,
},
Alerts,
MarkAlertsSeen,
RefreshCongress,
@@ -66,14 +105,39 @@ pub enum Response {
from_cache: bool,
exhausted: bool,
},
HistoryError { pane: PaneId, generation: u64, error: String },
Indicators { pane: PaneId, generation: u64, built: Vec<BuiltIndicator> },
Compare { pane: PaneId, generation: u64, symbol: String, line: LineRef },
Markers { pane: PaneId, symbol: String, trades: Vec<Trade> },
HistoryError {
pane: PaneId,
generation: u64,
error: String,
},
Indicators {
pane: PaneId,
generation: u64,
built: Vec<BuiltIndicator>,
},
Compare {
pane: PaneId,
generation: u64,
symbol: String,
line: LineRef,
},
Markers {
pane: PaneId,
symbol: String,
trades: Vec<Trade>,
},
Quotes(Vec<Quote>),
Latest(Vec<Trade>),
TickerTrades { pane: PaneId, symbol: String, trades: Vec<Trade> },
Profile { pane: PaneId, profile: Option<Box<Profile>>, query: String },
TickerTrades {
pane: PaneId,
symbol: String,
trades: Vec<Trade>,
},
Profile {
pane: PaneId,
profile: Option<Box<Profile>>,
query: String,
},
Politicians(Vec<PoliticianSummary>),
Alerts(Vec<Alert>),
NewAlerts(Vec<NewTradeAlert>),
@@ -110,7 +174,10 @@ pub fn data_dir() -> PathBuf {
if cfg!(target_os = "macos") {
home.join("Library/Application Support/terminal-rs")
} else {
std::env::var_os("XDG_DATA_HOME").map(PathBuf::from).unwrap_or_else(|| home.join(".local/share")).join("terminal-rs")
std::env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".local/share"))
.join("terminal-rs")
}
}
@@ -122,8 +189,13 @@ impl Worker {
tracing::warn!("congress db unavailable ({e:#}); using in-memory store");
Db::open_in_memory()
})?;
let cache = BarCache::open(dir.join("bars.sqlite3")).map_err(|e| tracing::warn!("bar cache unavailable: {e:#}")).ok();
let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(4).enable_all().thread_name("tr-worker").build()?;
let cache =
BarCache::open(dir.join("bars.sqlite3")).map_err(|e| tracing::warn!("bar cache unavailable: {e:#}")).ok();
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.thread_name("tr-worker")
.build()?;
let (tx, mut req_rx) = tokio::sync::mpsc::unbounded_channel::<Request>();
let (out, rx) = mpsc::channel();
let shared = Arc::new(Shared { provider, cache: std::sync::Mutex::new(cache), db: Mutex::new(db), out, ctx });
@@ -202,7 +274,9 @@ pub fn build_indicators(bars: &[Bar], tf: Timeframe, specs: &[IndicatorSpec]) ->
let overlays = match *spec {
IndicatorSpec::Sma(n) => vec![(spec.label(), overlay(&indicators::sma(&closes, n), c, LineKind::Line))],
IndicatorSpec::Ema(n) => vec![(spec.label(), overlay(&indicators::ema(&closes, n), c, LineKind::Line))],
IndicatorSpec::Vwap => vec![(spec.label(), overlay(&indicators::vwap_by(bars, session_key(tf)), c, LineKind::Line))],
IndicatorSpec::Vwap => {
vec![(spec.label(), overlay(&indicators::vwap_by(bars, session_key(tf)), c, LineKind::Line))]
}
IndicatorSpec::Bollinger(n, k) => {
let b = indicators::bollinger(&closes, n, k);
let dim = c.gamma_multiply(0.7);
@@ -212,7 +286,9 @@ pub fn build_indicators(bars: &[Bar], tf: Timeframe, specs: &[IndicatorSpec]) ->
("BB lower".into(), overlay(&b.lower, dim, LineKind::Line)),
]
}
IndicatorSpec::Rsi(n) => vec![(spec.label(), overlay(&indicators::rsi(&closes, n), theme::IND, LineKind::Line))],
IndicatorSpec::Rsi(n) => {
vec![(spec.label(), overlay(&indicators::rsi(&closes, n), theme::IND, LineKind::Line))]
}
IndicatorSpec::Macd(f, sl, sg) => {
let m = indicators::macd(&closes, f, sl, sg);
vec![
@@ -265,19 +341,39 @@ async fn handle(s: &Shared, req: Request) {
if let Some(bars) = cached.filter(|b| !b.is_empty()) {
let bars = Arc::new(bars);
let gpu = build_gpu(bars.clone()).await;
s.send(Response::History { pane, generation, symbol: symbol.clone(), tf, bars, gpu, prepended: 0, from_cache: true, exhausted: false });
s.send(Response::History {
pane,
generation,
symbol: symbol.clone(),
tf,
bars,
gpu,
prepended: 0,
from_cache: true,
exhausted: false,
});
}
match s.provider.history(&symbol, tf, None, limit).await {
Ok(bars) if !bars.is_empty() => {
if let Ok(mut c) = s.cache.lock() {
if let Some(c) = c.as_mut() {
let _ = c.store(&symbol, tf, &bars);
}
if let Ok(mut c) = s.cache.lock()
&& let Some(c) = c.as_mut()
{
let _ = c.store(&symbol, tf, &bars);
}
let exhausted = bars.len() < limit / 2;
let bars = Arc::new(bars);
let gpu = build_gpu(bars.clone()).await;
s.send(Response::History { pane, generation, symbol, tf, bars, gpu, prepended: 0, from_cache: false, exhausted });
s.send(Response::History {
pane,
generation,
symbol,
tf,
bars,
gpu,
prepended: 0,
from_cache: false,
exhausted,
});
}
Ok(_) => s.send(Response::HistoryError { pane, generation, error: format!("no data for {symbol}") }),
Err(e) => s.send(Response::HistoryError { pane, generation, error: format!("{e:#}") }),
@@ -293,13 +389,24 @@ async fn handle(s: &Shared, req: Request) {
merged.extend_from_slice(&existing);
let bars = Arc::new(merged);
let gpu = build_gpu(bars.clone()).await;
s.send(Response::History { pane, generation, symbol, tf, bars, gpu, prepended, from_cache: false, exhausted: prepended == 0 });
s.send(Response::History {
pane,
generation,
symbol,
tf,
bars,
gpu,
prepended,
from_cache: false,
exhausted: prepended == 0,
});
}
Err(e) => s.send(Response::HistoryError { pane, generation, error: format!("{e:#}") }),
}
}
Request::Indicators { pane, generation, bars, tf, specs } => {
let built = tokio::task::spawn_blocking(move || build_indicators(&bars, tf, &specs)).await.unwrap_or_default();
let built =
tokio::task::spawn_blocking(move || build_indicators(&bars, tf, &specs)).await.unwrap_or_default();
s.send(Response::Indicators { pane, generation, built });
}
Request::Compare { pane, generation, symbol, tf, base } => {
@@ -449,7 +556,11 @@ mod tests {
#[test]
fn indicators_build_expected_overlays() {
let bars = tr_core::sample::generate("X", Timeframe::D1, 300, 1_790_000_000);
let built = build_indicators(&bars, Timeframe::D1, &[IndicatorSpec::Bollinger(20, 2.0), IndicatorSpec::Macd(12, 26, 9), IndicatorSpec::Vwap]);
let built = build_indicators(
&bars,
Timeframe::D1,
&[IndicatorSpec::Bollinger(20, 2.0), IndicatorSpec::Macd(12, 26, 9), IndicatorSpec::Vwap],
);
assert_eq!(built[0].overlays.len(), 3);
assert_eq!(built[1].overlays[0].1.kind, LineKind::Hist);
assert_eq!(built[2].overlays.len(), 1);
+12 -2
View File
@@ -19,8 +19,18 @@ fn bench(c: &mut Criterion) {
let closes: Vec<f64> = bars.iter().map(|b| b.close).collect();
let series = SeriesGpu::build(&bars);
let overlays = [
Overlay { line: line_ref(&indicators::ema(&closes, 20)), color: [0.0, 0.6, 1.0, 1.0], kind: LineKind::Line, scale: 1.0 },
Overlay { line: line_ref(&indicators::sma(&closes, 50)), color: [1.0, 0.0, 1.0, 1.0], kind: LineKind::Line, scale: 1.0 },
Overlay {
line: line_ref(&indicators::ema(&closes, 20)),
color: [0.0, 0.6, 1.0, 1.0],
kind: LineKind::Line,
scale: 1.0,
},
Overlay {
line: line_ref(&indicators::sma(&closes, 50)),
color: [1.0, 0.0, 1.0, 1.0],
kind: LineKind::Line,
scale: 1.0,
},
];
let mut g = c.benchmark_group("frame_1M_bars_1600x900");
g.sample_size(50);
+14 -3
View File
@@ -28,8 +28,18 @@ fn main() {
let closes: Vec<f64> = bars.iter().map(|b| b.close).collect();
let series = SeriesGpu::build(&bars);
let overlays = [
Overlay { line: line_ref(&indicators::ema(&closes, 20)), color: [0.0, 0.6, 1.0, 1.0], kind: LineKind::Line, scale: 1.0 },
Overlay { line: line_ref(&indicators::bollinger(&closes, 20, 2.0).upper), color: [0.8, 0.8, 0.8, 1.0], kind: LineKind::Line, scale: 1.0 },
Overlay {
line: line_ref(&indicators::ema(&closes, 20)),
color: [0.0, 0.6, 1.0, 1.0],
kind: LineKind::Line,
scale: 1.0,
},
Overlay {
line: line_ref(&indicators::bollinger(&closes, 20, 2.0).upper),
color: [0.8, 0.8, 0.8, 1.0],
kind: LineKind::Line,
scale: 1.0,
},
];
let prep_ms = t.elapsed().as_secs_f64() * 1e3;
println!("{n} bars: generate {gen_ms:.0} ms, pyramids+indicators {prep_ms:.0} ms");
@@ -56,7 +66,8 @@ fn main() {
let first = render(&v);
println!("first frame (includes GPU upload): {:.1} ms", first.1.as_secs_f64() * 1e3);
let phases: [(&str, Box<dyn Fn(&mut Viewport, usize)>); 3] = [
type Step = Box<dyn Fn(&mut Viewport, usize)>;
let phases: [(&str, Step); 3] = [
("zoom out to all bars", Box::new(|v: &mut Viewport, _| v.zoom(0.97, 1500.0))),
("pan across history", Box::new(|v: &mut Viewport, i| v.pan_px(if i < 150 { 40.0 } else { -40.0 }))),
("zoom in to 1 bar/8px", Box::new(|v: &mut Viewport, _| v.zoom(1.035, 800.0))),
+9 -2
View File
@@ -275,11 +275,18 @@ impl ChartRenderer {
panes: HashMap::new(),
frame_no: u64::MAX,
next_slot: 0,
max_binding: device.limits().max_storage_buffer_binding_size as u64,
max_binding: device.limits().max_storage_buffer_binding_size,
}
}
fn ensure(&mut self, device: &wgpu::Device, is_line: bool, key: u64, version: u64, bytes: impl FnOnce() -> Vec<u8>) {
fn ensure(
&mut self,
device: &wgpu::Device,
is_line: bool,
key: u64,
version: u64,
bytes: impl FnOnce() -> Vec<u8>,
) {
let frame = self.frame_no;
if let Some(b) = self.buffers.get_mut(&(is_line, key)) {
b.last_used = frame;
+4 -1
View File
@@ -77,7 +77,10 @@ impl Headless {
view: &self.view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
+2 -2
View File
@@ -7,11 +7,11 @@
//! - [`scale`]: axis ticks and labels. [`drawings`]: user drawing model and hit testing.
pub mod drawings;
pub mod headless;
pub mod gpu;
pub mod headless;
pub mod lod;
pub mod scene;
pub mod scale;
pub mod scene;
pub mod viewport;
pub use egui_wgpu;
+4 -1
View File
@@ -203,7 +203,10 @@ mod tests {
assert_eq!(p.levels(), 4); // 5, 3, 2, 1
let l1 = p.level(1);
assert_eq!(l1.len(), 3);
assert_eq!(l1[0], GpuBar { x: 0.0, w: 2.0, open: 0.0, high: 3.0, low: -1.0, close: 2.0, volume: 2.0, _pad: 0.0 });
assert_eq!(
l1[0],
GpuBar { x: 0.0, w: 2.0, open: 0.0, high: 3.0, low: -1.0, close: 2.0, volume: 2.0, _pad: 0.0 }
);
assert_eq!(l1[2].x, 4.0);
assert_eq!(l1[2].w, 1.0);
let top = p.level(3);
+2 -4
View File
@@ -117,10 +117,8 @@ pub fn time_ticks(times: &[i64], lo: usize, hi: usize, min_gap_bars: f64, tf: Ti
return Vec::new();
}
let gap = min_gap_bars.max(1.0).ceil() as usize;
let mut cands: Vec<(Boundary, usize)> = (lo.max(1)..hi)
.map(|i| (boundary(times[i - 1], times[i]), i))
.filter(|(b, _)| *b > Boundary::None)
.collect();
let mut cands: Vec<(Boundary, usize)> =
(lo.max(1)..hi).map(|i| (boundary(times[i - 1], times[i]), i)).filter(|(b, _)| *b > Boundary::None).collect();
// Higher boundaries first; within a level, keep chronological order.
cands.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
let mut chosen: Vec<(usize, Boundary)> = Vec::new();
+24 -4
View File
@@ -126,7 +126,12 @@ fn transform(v: &Viewport, height_px: f64, scale: &PriceScale) -> PaneTransform
}
/// Autoscale range for the visible part of the series and overlays.
pub fn visible_extent(series: &SeriesGpu, overlays: &[Overlay], v: &Viewport, chart_type: ChartType) -> Option<(f64, f64, f64)> {
pub fn visible_extent(
series: &SeriesGpu,
overlays: &[Overlay],
v: &Viewport,
chart_type: ChartType,
) -> Option<(f64, f64, f64)> {
let p = &series.bars.pyramid;
let (lo, hi) = v.visible_range(p.len0);
let k = level_for(v.bar_px, p.levels());
@@ -202,7 +207,12 @@ pub fn price_pane(p: &PricePane<'_>) -> PaneOutput {
}
let mut ls = style;
ls.thickness_px = 2.0;
cmds.push(DrawCmd::Line { kind: LineKind::Line, data: p.series.closes.clone(), range, uniforms: Uniforms::new(&t, &ls) });
cmds.push(DrawCmd::Line {
kind: LineKind::Line,
data: p.series.closes.clone(),
range,
uniforms: Uniforms::new(&t, &ls),
});
}
}
cmds.extend(overlay_cmds(p.overlays, v, &t, &style));
@@ -248,7 +258,13 @@ fn overlay_cmds(overlays: &[Overlay], v: &Viewport, t: &PaneTransform, base: &St
}
/// An indicator sub-pane (RSI, MACD): lines and histograms with their own scale.
pub fn sub_pane(overlays: &[Overlay], v: &Viewport, height_px: f64, fixed: Option<(f64, f64)>, style: Style) -> PaneOutput {
pub fn sub_pane(
overlays: &[Overlay],
v: &Viewport,
height_px: f64,
fixed: Option<(f64, f64)>,
style: Style,
) -> PaneOutput {
let len = overlays.first().map(|o| o.line.pyramid.level_len(0)).unwrap_or(0);
let (lo, hi) = v.visible_range(len);
let (l, h) = fixed.unwrap_or_else(|| {
@@ -263,7 +279,11 @@ pub fn sub_pane(overlays: &[Overlay], v: &Viewport, height_px: f64, fixed: Optio
// Histograms grow from zero, so keep zero in range.
if overlays.iter().any(|o| o.kind == LineKind::Hist) { (a.min(0.0), b.max(0.0)) } else { (a, b) }
});
let scale = if fixed.is_some() { PriceScale::new(l, h, false, height_px) } else { PriceScale::padded(l, h, 0.1, false, height_px) };
let scale = if fixed.is_some() {
PriceScale::new(l, h, false, height_px)
} else {
PriceScale::padded(l, h, 0.1, false, height_px)
};
let t = transform(v, height_px, &scale);
let cmds = overlay_cmds(overlays, v, &t, &style);
PaneOutput { cmds, scale, level: 0, vol_max: 0.0 }
+8 -3
View File
@@ -8,12 +8,12 @@ use tr_core::{Timeframe, indicators, sample};
fn dump_ppm(path: &str, w: u32, h: u32, rgba: &[u8]) {
let mut out = format!("P6 {w} {h} 255\n").into_bytes();
out.extend(rgba.chunks_exact(4).flat_map(|p| [p[0], p[1], p[2]]));
out.extend(rgba.as_chunks::<4>().0.iter().flat_map(|p| [p[0], p[1], p[2]]));
std::fs::write(path, out).unwrap();
}
fn count(px: &[u8], pred: impl Fn(&[u8]) -> bool) -> usize {
px.chunks_exact(4).filter(|p| pred(p)).count()
px.as_chunks::<4>().0.iter().filter(|p| pred(p.as_slice())).count()
}
#[test]
@@ -26,7 +26,12 @@ fn renders_candles_volume_lines_and_hist() {
let bars = sample::generate("AAPL", Timeframe::D1, 2_000, 1_790_000_000);
let series = SeriesGpu::build(&bars);
let closes: Vec<f64> = bars.iter().map(|b| b.close).collect();
let overlays = [Overlay { line: line_ref(&indicators::sma(&closes, 20)), color: [0.0, 0.6, 1.0, 1.0], kind: LineKind::Line, scale: 1.0 }];
let overlays = [Overlay {
line: line_ref(&indicators::sma(&closes, 20)),
color: [0.0, 0.6, 1.0, 1.0],
kind: LineKind::Line,
scale: 1.0,
}];
let v = Viewport::fit_last(bars.len(), 150, 800.0);
for ct in ChartType::ALL {
let out = price_pane(&PricePane {
+9 -2
View File
@@ -25,8 +25,15 @@ async fn main() -> anyhow::Result<()> {
for t in db.latest_trades(15)? {
println!(
"{} {:<24} {} {:<6} {:<5} {:<12} tx {} filed {} lag {}d",
t.chamber.as_str(), t.politician, t.party.code(), t.ticker.as_deref().unwrap_or("-"),
t.tx_type.marker(), t.amount.map(|a| a.to_string()).unwrap_or_default(), t.tx_date, t.filed_date, t.disclosure_lag_days()
t.chamber.as_str(),
t.politician,
t.party.code(),
t.ticker.as_deref().unwrap_or("-"),
t.tx_type.marker(),
t.amount.map(|a| a.to_string()).unwrap_or_default(),
t.tx_date,
t.filed_date,
t.disclosure_lag_days()
);
}
Ok(())
+4 -1
View File
@@ -4,7 +4,10 @@ fn main() {
match tr_congress::house::parse_ptr_pdf(&std::fs::read(&p).unwrap()) {
Ok(r) => {
for t in r.trades {
println!("{:?} | {:?} | {:?} | {} | {} | {:?}", t.owner, t.ticker, t.tx_type, t.tx_date, t.asset_name, t.amount);
println!(
"{:?} | {:?} | {:?} | {} | {} | {:?}",
t.owner, t.ticker, t.tx_type, t.tx_date, t.asset_name, t.amount
);
}
}
Err(e) => println!("{p}: {e}"),
+8 -2
View File
@@ -261,7 +261,10 @@ impl Db {
for &d in dups {
tx.execute("UPDATE trades SET politician_id = ?1 WHERE politician_id = ?2", params![keep, d])?;
tx.execute("UPDATE filings SET politician_id = ?1 WHERE politician_id = ?2", params![keep, d])?;
tx.execute("UPDATE OR IGNORE tracked SET politician_id = ?1 WHERE politician_id = ?2", params![keep, d])?;
tx.execute(
"UPDATE OR IGNORE tracked SET politician_id = ?1 WHERE politician_id = ?2",
params![keep, d],
)?;
tx.execute("DELETE FROM tracked WHERE politician_id = ?1", [d])?;
tx.execute("UPDATE alerts SET politician_id = ?1 WHERE politician_id = ?2", params![keep, d])?;
tx.execute("DELETE FROM politicians WHERE id = ?1", [d])?;
@@ -587,7 +590,10 @@ mod tests {
})
.unwrap();
db.conn().execute("UPDATE politicians SET bioguide = 'D000001' WHERE id = ?1", [dup]).unwrap();
assert_eq!(db.apply_roster(&[("D000001".into(), "Jane Doe".into(), Party::Republican, Some("MO".into()))]).unwrap(), 1);
assert_eq!(
db.apply_roster(&[("D000001".into(), "Jane Doe".into(), Party::Republican, Some("MO".into()))]).unwrap(),
1
);
assert!(db.politician(dup).unwrap().is_none());
let s = db.search_politicians("doe", 10).unwrap();
assert_eq!(s[0].trades, 2);
+4 -1
View File
@@ -331,7 +331,10 @@ mod tests {
#[test]
fn exchange_prefixed_ticker_and_cusip() {
assert_eq!(ticker_from_asset("DIA - SPDR Dow Jones Indust Avg ETF Trust NYSEARCA: DIA").as_deref(), Some("DIA"));
assert_eq!(
ticker_from_asset("DIA - SPDR Dow Jones Indust Avg ETF Trust NYSEARCA: DIA").as_deref(),
Some("DIA")
);
assert_eq!(ticker_from_asset("US Treasury 01/31/28 (91282CGH8)"), None);
assert_eq!(ticker_from_asset("Berkshire Hathaway Inc. New Common Stock (BRK.B)").as_deref(), Some("BRK.B"));
}
+2 -8
View File
@@ -133,10 +133,7 @@ impl AmountRange {
/// Parse `$1,001 - $15,000`, `Over $50,000,000`, `Spouse/DC Over $1,000,000`,
/// and line-wrapped forms such as `$15,001 -\n$50,000`.
pub fn parse(s: &str) -> Option<Self> {
let nums: Vec<u64> = DOLLARS
.captures_iter(s)
.filter_map(|c| c[1].replace(',', "").parse().ok())
.collect();
let nums: Vec<u64> = DOLLARS.captures_iter(s).filter_map(|c| c[1].replace(',', "").parse().ok()).collect();
let over = s.to_ascii_lowercase().contains("over");
match (nums.as_slice(), over) {
([n], true) => Some(Self { min: n + 1, max: None }),
@@ -255,10 +252,7 @@ mod tests {
assert_eq!(AmountRange::parse("$1,001 - $15,000"), Some(AmountRange { min: 1001, max: Some(15000) }));
assert_eq!(AmountRange::parse("$15,001 -\n$50,000"), Some(AmountRange { min: 15001, max: Some(50000) }));
assert_eq!(AmountRange::parse("Over $50,000,000"), Some(AmountRange { min: 50_000_001, max: None }));
assert_eq!(
AmountRange::parse("Spouse/DC Over $1,000,000"),
Some(AmountRange { min: 1_000_001, max: None })
);
assert_eq!(AmountRange::parse("Spouse/DC Over $1,000,000"), Some(AmountRange { min: 1_000_001, max: None }));
assert_eq!(AmountRange::parse("--"), None);
let r = AmountRange::parse("$1,001 - $15,000").unwrap();
assert_eq!(r.to_string(), "$1K-$15K");
+14 -3
View File
@@ -64,7 +64,11 @@ pub fn estimate(
scored: out.len(),
unscored,
weighted_return: if wsum > 0.0 { out.iter().map(|r| r.ret * r.weight).sum::<f64>() / wsum } else { 0.0 },
hit_rate: if out.is_empty() { 0.0 } else { out.iter().filter(|r| r.ret > 0.0).count() as f64 / out.len() as f64 },
hit_rate: if out.is_empty() {
0.0
} else {
out.iter().filter(|r| r.ret > 0.0).count() as f64 / out.len() as f64
},
avg_lag_days: if trades.is_empty() {
0.0
} else {
@@ -109,8 +113,15 @@ mod tests {
];
let (p, rs) = estimate(
&trades,
|tk, _| match tk { "UP" | "DOWN" => Some(100.0), _ => None },
|tk| match tk { "UP" => Some(110.0), "DOWN" => Some(80.0), _ => None },
|tk, _| match tk {
"UP" | "DOWN" => Some(100.0),
_ => None,
},
|tk| match tk {
"UP" => Some(110.0),
"DOWN" => Some(80.0),
_ => None,
},
);
assert_eq!(p.scored, 2);
assert_eq!(p.unscored, 1);
+9 -5
View File
@@ -41,8 +41,7 @@ impl RefreshConfig {
/// Sensible defaults derived from the database's last successful run.
pub fn incremental(db: &Db) -> Self {
let today = Utc::now().date_naive();
let last: Option<NaiveDate> =
db.meta("last_refresh_date").ok().flatten().and_then(|s| s.parse().ok());
let last: Option<NaiveDate> = db.meta("last_refresh_date").ok().flatten().and_then(|s| s.parse().ok());
let (house_years, senate_since) = match last {
// Re-scan a week back: filings can appear in the index late.
Some(d) => {
@@ -177,7 +176,12 @@ async fn store(
t
};
let message = if parsed.trades.is_empty() {
format!("{} filed a new PTR ({}) on {}", politician.name, if rec.paper { "paper" } else { "scanned" }, rec.filed_date)
format!(
"{} filed a new PTR ({}) on {}",
politician.name,
if rec.paper { "paper" } else { "scanned" },
rec.filed_date
)
} else {
format!("{} filed {} trade(s): {}", politician.name, parsed.trades.len(), tickers.join(", "))
};
@@ -207,7 +211,7 @@ async fn refresh_house<S: Source>(
todo.extend(entries.into_iter().filter(|e| e.is_ptr() && !known.contains(&format!("house:{}", e.doc_id))));
}
// Newest first so a capped run still gets the freshest filings.
todo.sort_by(|a, b| b.filing_date.cmp(&a.filing_date));
todo.sort_by_key(|e| std::cmp::Reverse(e.filing_date));
todo.truncate(cfg.max_new_per_chamber);
debug!("house: {} new PTRs to fetch", todo.len());
@@ -303,7 +307,7 @@ async fn refresh_senate<S: Source>(
.into_iter()
.filter(|r| !known.contains(&format!("senate:{}", r.id)))
.collect();
todo.sort_by(|a, b| b.date_received.cmp(&a.date_received));
todo.sort_by_key(|r| std::cmp::Reverse(r.date_received));
todo.truncate(cfg.max_new_per_chamber);
debug!("senate: {} new PTRs to fetch", todo.len());
+5 -6
View File
@@ -38,17 +38,16 @@ pub struct Roster {
pub fn normalize_name(s: &str) -> Vec<String> {
s.split(|c: char| c.is_whitespace() || c == ',')
.map(|w| w.chars().filter(|c| c.is_alphanumeric() || *c == '-').collect::<String>().to_lowercase())
.filter(|w| !w.is_empty() && !matches!(w.as_str(), "jr" | "sr" | "ii" | "iii" | "iv" | "hon" | "the" | "honorable"))
.filter(|w| {
!w.is_empty() && !matches!(w.as_str(), "jr" | "sr" | "ii" | "iii" | "iv" | "hon" | "the" | "honorable")
})
.collect()
}
impl Roster {
pub fn new(house: Vec<RosterMember>, senate: Vec<RosterMember>) -> Self {
let by_district = house
.iter()
.enumerate()
.filter_map(|(i, m)| m.state_district.clone().map(|d| (d, i)))
.collect();
let by_district =
house.iter().enumerate().filter_map(|(i, m)| m.state_district.clone().map(|d| (d, i))).collect();
Self { house, senate, by_district }
}
+2 -5
View File
@@ -183,11 +183,8 @@ pub fn parse_ptr_html(html: &str) -> Result<SenatePtr> {
let t = t.trim_start_matches("The Honorable").trim();
t.split(" (").next().unwrap_or(t).trim().to_string()
});
let filed_date = doc
.select(&sel("p.muted strong"))
.map(text_of)
.find(|t| t.contains("Filed"))
.and_then(|t| parse_us_date(&t));
let filed_date =
doc.select(&sel("p.muted strong")).map(text_of).find(|t| t.contains("Filed")).and_then(|t| parse_us_date(&t));
let table = doc
.select(&sel("table.table"))
+1 -1
View File
@@ -1,11 +1,11 @@
//! Parser tests against real filings captured from the official sources.
use chrono::{Datelike, NaiveDate};
use tr_congress::Chamber;
use tr_congress::house::{self, PdfError};
use tr_congress::model::{AmountRange, Party, TxType};
use tr_congress::roster::{self, Roster};
use tr_congress::senate::{self, ReportKind};
use tr_congress::Chamber;
const FIX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
+4 -11
View File
@@ -64,11 +64,7 @@ pub fn rsi(closes: &[f64], n: usize) -> Vec<f64> {
let al = rma(&losses, n);
for i in (n - 1)..gains.len() {
let (g, l) = (ag[i], al[i]);
out[i + 1] = if l == 0.0 {
if g == 0.0 { 50.0 } else { 100.0 }
} else {
100.0 - 100.0 / (1.0 + g / l)
};
out[i + 1] = if l == 0.0 { if g == 0.0 { 50.0 } else { 100.0 } } else { 100.0 - 100.0 / (1.0 + g / l) };
}
out
}
@@ -158,10 +154,7 @@ mod tests {
fn assert_series(got: &[f64], want: &[f64]) {
assert_eq!(got.len(), want.len());
for (i, (g, w)) in got.iter().zip(want).enumerate() {
assert!(
(g.is_nan() && w.is_nan()) || close_to(*g, *w),
"index {i}: got {g}, want {w}"
);
assert!((g.is_nan() && w.is_nan()) || close_to(*g, *w), "index {i}: got {g}, want {w}");
}
}
@@ -186,8 +179,8 @@ mod tests {
fn rsi_reference_values() {
// Wilder's original 14-period example (New Concepts in Technical Trading Systems).
let closes = [
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28,
46.28, 46.00, 46.03, 46.41, 46.22, 45.64,
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28,
46.00, 46.03, 46.41, 46.22, 45.64,
];
let r = rsi(&closes, 14);
assert!(r[13].is_nan());
+2 -3
View File
@@ -168,9 +168,8 @@ mod tests {
#[test]
fn resample_minutes_to_hour() {
let bars: Vec<Bar> = (0..120)
.map(|i| Bar::new(i * 60, i as f64, i as f64 + 1.0, i as f64 - 1.0, i as f64 + 0.5, 1.0))
.collect();
let bars: Vec<Bar> =
(0..120).map(|i| Bar::new(i * 60, i as f64, i as f64 + 1.0, i as f64 - 1.0, i as f64 + 0.5, 1.0)).collect();
let h = Timeframe::H1.resample(&bars);
assert_eq!(h.len(), 2);
assert_eq!(h[0], Bar::new(0, 0.0, 60.0, -1.0, 59.5, 60.0));
+2 -6
View File
@@ -61,12 +61,8 @@ struct RawBar {
/// Parse one page of `/v2/stocks/{symbol}/bars`.
pub fn parse_bars_page(json: &str) -> Result<(Vec<Bar>, Option<String>)> {
let page: BarsPage = serde_json::from_str(json).context("parsing Alpaca bars")?;
let bars = page
.bars
.unwrap_or_default()
.into_iter()
.map(|b| Bar::new(b.t.timestamp(), b.o, b.h, b.l, b.c, b.v))
.collect();
let bars =
page.bars.unwrap_or_default().into_iter().map(|b| Bar::new(b.t.timestamp(), b.o, b.h, b.l, b.c, b.v)).collect();
Ok((bars, page.next_page_token.filter(|t| !t.is_empty())))
}
+8 -3
View File
@@ -180,10 +180,15 @@ impl MarketData for YahooProvider {
for s in symbols {
let c = self.chart(s, "1d", now - 10 * 86_400, now + 60).await?;
let Some(last) = c.bars.last() else { continue };
let prev = c.prev_close.unwrap_or_else(|| {
if c.bars.len() > 1 { c.bars[c.bars.len() - 2].close } else { last.open }
let prev = c
.prev_close
.unwrap_or_else(|| if c.bars.len() > 1 { c.bars[c.bars.len() - 2].close } else { last.open });
out.push(Quote {
symbol: s.clone(),
last: c.last_price.unwrap_or(last.close),
prev_close: prev,
ts: last.ts,
});
out.push(Quote { symbol: s.clone(), last: c.last_price.unwrap_or(last.close), prev_close: prev, ts: last.ts });
}
Ok(out)
}
+2
View File
@@ -0,0 +1,2 @@
max_width = 120
use_small_heuristics = "Max"