Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 83 additions & 27 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::time::Duration;

use chrono::{Local, TimeZone, Utc};
use chrono::{Datelike, Local, NaiveDateTime, TimeZone, Utc, Weekday};
use itertools::izip;
use serde::Deserialize;
use tickrs_api::Interval;

use crate::api::model::ChartData;
use crate::api::Range;

pub const DAY: i64 = 60 * 60 * 24;

#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, Deserialize)]
pub enum ChartType {
#[serde(rename = "line")]
Expand Down Expand Up @@ -200,32 +202,6 @@ impl TimeFrame {
}
}

#[derive(Debug, Clone, Copy)]
pub struct MarketHours(pub i64, pub i64);

impl Default for MarketHours {
fn default() -> Self {
MarketHours(52200, 75600)
}
}

impl Iterator for MarketHours {
type Item = i64;

fn next(&mut self) -> Option<Self::Item> {
let min_rounded_0 = self.0 - self.0 % 60;
let min_rounded_1 = self.1 - self.1 % 60;

if min_rounded_0 == min_rounded_1 {
None
} else {
let result = Some(min_rounded_0);
self.0 = min_rounded_0 + 60;
result
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TradingPeriod {
Pre,
Expand All @@ -243,6 +219,23 @@ pub struct Price {
pub date: i64,
}

#[derive(Debug, Clone, Copy, Default)]
pub struct MarketTimes {
pub start: i64,
pub end: i64,
pub delta: i64,
pub current: i64,
}

impl Price {
pub fn new(date: i64) -> Self {
Price {
date,
..Default::default()
}
}
}

impl Hash for Price {
fn hash<H: Hasher>(&self, state: &mut H) {
self.close.to_bits().hash(state);
Expand All @@ -254,6 +247,55 @@ impl Hash for Price {
}
}

impl MarketTimes {
pub fn new(start: i64, end: i64, delta: i64) -> Self {
MarketTimes {
start,
end,
delta,
current: start,
}
}
}

impl Iterator for MarketTimes {
type Item = i64;

fn next(&mut self) -> Option<Self::Item> {
// Round to the last value divisible by delta
let start = self.start - self.start % self.delta;
let end = self.end - self.end % self.delta;
let current = self.current - self.current % self.delta;

// We exceeded the end time, stop the iteration
if current >= end {
return None;
}

// Elapsed time since 00:00
let start_time = start % DAY;
let end_time = end % DAY;
let current_time = current % DAY;

if (start_time < end_time && (start_time <= current_time && current_time < end_time))
|| (start_time > end_time && (current_time < end_time || start_time <= current_time))
{
// Market hasn't closed yet, advance by delta
self.current = current + self.delta;
} else {
// We're post the market closing time

// Go back to today's market opening time
self.current = current - current_time + start_time;

// Advance one day and skip the weekend
self.current += get_next_business_day_delta(self.current);
}

Some(current)
}
}

pub fn chart_data_to_prices(mut chart_data: ChartData) -> Vec<Price> {
if chart_data.indicators.quote.len() != 1 {
return vec![];
Expand Down Expand Up @@ -323,3 +365,17 @@ pub fn format_decimals(value: f64) -> String {
format!("{:.*}", n, value)
}
}

pub fn get_next_business_day_delta(epoch: i64) -> i64 {
DAY * match get_weekday(epoch) {
Weekday::Fri => 3,
Weekday::Sat => 2,
_ => 1,
}
}

fn get_weekday(epoch: i64) -> Weekday {
NaiveDateTime::from_timestamp_opt(epoch, 0)
.unwrap_or_default()
.weekday()
}
41 changes: 19 additions & 22 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ fn handle_keys_display_stock(keycode: KeyCode, modifiers: KeyModifiers, app: &mu
(KeyCode::Char('s'), KeyModifiers::NONE) => {
app.mode = app::Mode::DisplaySummary;
}
(KeyCode::Char('o'), KeyModifiers::NONE) => {
if app.stocks[app.current_tab].toggle_options() {
app.mode = app::Mode::DisplayOptions;
}
(KeyCode::Char('o'), KeyModifiers::NONE)
if app.stocks[app.current_tab].toggle_options() =>
{
app.mode = app::Mode::DisplayOptions;
}
(KeyCode::Char('e'), KeyModifiers::NONE) => {
if app.stocks[app.current_tab].toggle_configure() {
app.mode = app::Mode::ConfigureChart;
}
(KeyCode::Char('e'), KeyModifiers::NONE)
if app.stocks[app.current_tab].toggle_configure() =>
{
app.mode = app::Mode::ConfigureChart;
}
(KeyCode::Tab, KeyModifiers::NONE) => {
if app.current_tab == app.stocks.len() - 1 {
Expand Down Expand Up @@ -196,20 +196,19 @@ fn handle_keys_display_options(keycode: KeyCode, app: &mut app::App) {
.unwrap()
.selection_mode_left();
}
KeyCode::Right => {
KeyCode::Right
if app.stocks[app.current_tab]
.options
.as_mut()
.unwrap()
.data()
.is_some()
{
app.stocks[app.current_tab]
.options
.as_mut()
.unwrap()
.selection_mode_right();
}
.is_some() =>
{
app.stocks[app.current_tab]
.options
.as_mut()
.unwrap()
.selection_mode_right();
}
_ => {}
}
Expand Down Expand Up @@ -242,11 +241,9 @@ pub fn handle_keys_configure_chart(keycode: KeyCode, modifiers: KeyModifiers, ap
let config = app.stocks[app.current_tab].chart_config_mut();
config.enter(time_frame);
}
(KeyCode::Char(c), KeyModifiers::NONE) => {
if c.is_numeric() || c == '.' {
let config = app.stocks[app.current_tab].chart_config_mut();
config.add_char(c);
}
(KeyCode::Char(c), KeyModifiers::NONE) if (c.is_numeric() || c == '.') => {
let config = app.stocks[app.current_tab].chart_config_mut();
config.add_char(c);
}
(KeyCode::Backspace, KeyModifiers::NONE) => {
let config = app.stocks[app.current_tab].chart_config_mut();
Expand Down
5 changes: 2 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,16 +167,15 @@ fn main() {
Ok(Event::Key(key_event)) => {
event::handle_key_bindings(app.mode, key_event, &mut app, &request_redraw);
}
Ok(Event::Mouse(MouseEvent { kind, row, column,.. })) => {
if app.debug.enabled {
Ok(Event::Mouse(MouseEvent { kind, row, column,.. }))
if app.debug.enabled => {
match kind {
MouseEventKind::Down(_) => app.debug.cursor_location = Some((row, column)),
MouseEventKind::Up(_) => app.debug.cursor_location = Some((row, column)),
MouseEventKind::Drag(_) => app.debug.cursor_location = Some((row, column)),
_ => {}
}
}
}
Ok(Event::Resize(..)) => {
let _ = request_redraw.try_send(());
}
Expand Down
3 changes: 2 additions & 1 deletion src/portfolio.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use serde::Deserialize;
use std::collections::HashMap;

use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
pub struct PortfolioItem {
pub quantity: f64,
Expand Down
39 changes: 20 additions & 19 deletions src/widget/add_stock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,26 @@ impl StatefulWidget for AddStockWidget {
type State = AddStockState;

fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let spans = if !state.has_user_input && state.error_msg.is_some() {
Line::from(vec![
Span::styled("> ", style().fg(THEME.text_normal())),
Span::styled(
state.error_msg.as_ref().unwrap(),
style().add_modifier(Modifier::BOLD).fg(THEME.loss()),
),
])
} else {
Line::from(vec![
Span::styled("> ", style().fg(THEME.text_normal())),
Span::styled(
&state.search_string,
style()
.add_modifier(Modifier::BOLD)
.fg(THEME.text_secondary()),
),
])
};
let spans =
if let (false, Some(error_msg)) = (state.has_user_input, state.error_msg.clone()) {
Line::from(vec![
Span::styled("> ", style().fg(THEME.text_normal())),
Span::styled(
error_msg,
style().add_modifier(Modifier::BOLD).fg(THEME.loss()),
),
])
} else {
Line::from(vec![
Span::styled("> ", style().fg(THEME.text_normal())),
Span::styled(
&state.search_string,
style()
.add_modifier(Modifier::BOLD)
.fg(THEME.text_secondary()),
),
])
};

Paragraph::new(spans)
.block(block::new(" Add Ticker "))
Expand Down
2 changes: 1 addition & 1 deletion src/widget/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use ratatui::widgets::{Block, Borders};
use crate::theme::style;
use crate::THEME;

pub fn new(title: &str) -> Block {
pub fn new(title: &'_ str) -> Block<'_> {
Block::default()
.borders(Borders::ALL)
.border_style(style().fg(THEME.border_primary()))
Expand Down
20 changes: 10 additions & 10 deletions src/widget/chart/prices_candlestick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ impl StatefulWidget for PricesCandlestickChart<'_> {
});

let (min, max) = state.min_max(&data);
let (start, end) = state.start_end();
let x_bounds = state.x_bounds(start, end, &data);
let x_bounds = state.x_bounds(&data);

// x_layout[0] - chart + y labels
// x_layout[1] - (x labels)
Expand Down Expand Up @@ -100,7 +99,7 @@ impl StatefulWidget for PricesCandlestickChart<'_> {
x_area.x = layout[1].x + 1;
x_area.width = layout[1].width - 1;

let labels = state.x_labels(area.width, start, end, self.data);
let labels = state.x_labels(area.width, self.data);
let total_width = labels.iter().map(Span::width).sum::<usize>() as u16;
let labels_len = labels.len() as u16;
if total_width < x_area.width && labels_len > 1 {
Expand Down Expand Up @@ -188,16 +187,17 @@ impl StatefulWidget for PricesCandlestickChart<'_> {
.x_bounds([0.0, num_candles as f64 * 4.0])
.y_bounds(state.y_bounds(min, max))
.paint(move |ctx| {
if state.time_frame == TimeFrame::Day1
&& self.loaded
&& !*HIDE_PREV_CLOSE
&& state.prev_close_price.is_some()
{
if let (true, true, false, Some(prev_close_price)) = (
state.time_frame == TimeFrame::Day1,
self.loaded,
*HIDE_PREV_CLOSE,
state.prev_close_price,
) {
ctx.draw(&Line {
x1: 0.0,
x2: num_candles as f64 * 4.0,
y1: state.prev_close_price.unwrap(),
y2: state.prev_close_price.unwrap(),
y1: prev_close_price,
y2: prev_close_price,
color: THEME.gray(),
})
}
Expand Down
17 changes: 9 additions & 8 deletions src/widget/chart/prices_kagi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,16 +471,17 @@ impl StatefulWidget for PricesKagiChart<'_> {
.x_bounds([0.0, chart_width])
.y_bounds(state.y_bounds(min, max))
.paint(move |ctx| {
if state.time_frame == TimeFrame::Day1
&& self.loaded
&& !*HIDE_PREV_CLOSE
&& state.prev_close_price.is_some()
{
if let (true, true, false, Some(prev_close_price)) = (
state.time_frame == TimeFrame::Day1,
self.loaded,
*HIDE_PREV_CLOSE,
state.prev_close_price,
) {
ctx.draw(&Line {
x1: 0.0,
x2: chart_width,
y1: state.prev_close_price.unwrap(),
y2: state.prev_close_price.unwrap(),
y1: prev_close_price,
y2: prev_close_price,
color: THEME.gray(),
});
}
Expand Down Expand Up @@ -590,7 +591,7 @@ impl StatefulWidget for PricesKagiChart<'_> {
}
}

fn x_labels(width: u16, trends: &[Trend], time_frame: TimeFrame) -> Vec<Span> {
fn x_labels(width: u16, trends: &'_ [Trend], time_frame: TimeFrame) -> Vec<Span<'_>> {
let mut labels = vec![];

let trends = trends
Expand Down
Loading
Loading