Repository: tarkah/tickrs (language: rust)

Issue:
Price history stretches to fit the timespan
If you try to view a timespan longer than the available data for an asset, the longest available history will be shown instead, but with no indication that it is being stretched. 

![20210205_10h33m51s_grim](https://user-images.githubusercontent.com/8876925/107054161-ac315880-67c7-11eb-9d84-80bb7b402977.png)
![20210205_10h34m07s_grim](https://user-images.githubusercontent.com/8876925/107054183-b3f0fd00-67c7-11eb-87da-c976bbe7e325.png)

For example, THNQ has existed for only about 9 months -- but attempting to view 5Y of its history gives no indication of this. This is not an issue when labels are visible, but it could be misleading in the summary view as there is no indication that different assets are not shown on the same timescale.



Discussion / hints:
Hmm, good point. I'll look into a fix for this. Thanks for opening this issue!

Fix patch:
diff --git a/api/src/lib.rs b/api/src/lib.rs
index 431d6dd..4847bba 100644
--- a/api/src/lib.rs
+++ b/api/src/lib.rs
@@ -3,7 +3,7 @@ pub use self::client::Client;
 mod client;
 pub mod model;
 
-#[derive(Copy, Clone)]
+#[derive(Debug, Copy, Clone)]
 pub enum Interval {
     Minute1,
     Minute2,
@@ -44,7 +44,7 @@ impl std::fmt::Display for Interval {
     }
 }
 
-#[derive(Copy, Clone)]
+#[derive(Debug, Copy, Clone)]
 pub enum Range {
     Day1,
     Day5,
diff --git a/src/app.rs b/src/app.rs
index bcd6dd6..42419dc 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,7 +1,9 @@
 use crossterm::event::Event;
 
 use crate::common::TimeFrame;
-use crate::widget;
+use crate::service::default_timestamps::DefaultTimestampService;
+use crate::service::Service;
+use crate::{widget, DEFAULT_TIMESTAMPS};
 
 #[derive(PartialEq, Clone, Copy, Debug)]
 pub enum Mode {
@@ -22,6 +24,17 @@ pub struct App {
     pub debug: DebugInfo,
     pub previous_mode: Mode,
     pub summary_time_frame: TimeFrame,
+    pub default_timestamp_service: DefaultTimestampService,
+}
+
+impl App {
+    pub fn update(&self) {
+        let mut timestamp_updates = self.default_timestamp_service.updates();
+
+        if let Some(new_defaults) = timestamp_updates.pop() {
+            *DEFAULT_TIMESTAMPS.write().unwrap() = new_defaults;
+        }
+    }
 }
 
 #[derive(Debug)]
diff --git a/src/common.rs b/src/common.rs
index 6ecc670..3017dfa 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -3,11 +3,12 @@ use std::time::Duration;
 
 use chrono::{Local, TimeZone, Utc};
 use itertools::izip;
+use tickrs_api::Interval;
 
 use crate::api::model::ChartData;
 use crate::api::Range;
 
-#[derive(PartialEq, Clone, Copy, PartialOrd, Debug)]
+#[derive(Clone, Copy, PartialOrd, Debug, Hash, PartialEq, Eq)]
 pub enum TimeFrame {
     Day1,
     Week1,
@@ -54,6 +55,16 @@ impl TimeFrame {
         ["1D", "1W", "1M", "3M", "6M", "1Y", "5Y"]
     }
 
+    pub const ALL: [TimeFrame; 7] = [
+        TimeFrame::Day1,
+        TimeFrame::Week1,
+        TimeFrame::Month1,
+        TimeFrame::Month3,
+        TimeFrame::Month6,
+        TimeFrame::Year1,
+        TimeFrame::Year5,
+    ];
+
     pub fn update_interval(self) -> Duration {
         match self {
             TimeFrame::Day1 => Duration::from_secs(60),
@@ -102,6 +113,28 @@ impl TimeFrame {
         }
     }
 
+    pub fn api_interval(self) -> Interval {
+        match self {
+            TimeFrame::Day1 => Interval::Minute1,
+            TimeFrame::Week1 => Interval::Minute5,
+            TimeFrame::Month1 => Interval::Minute30,
+            TimeFrame::Month3 => Interval::Minute60,
+            TimeFrame::Month6 => Interval::Minute60,
+            _ => Interval::Day1,
+        }
+    }
+
+    pub fn round_by(self) -> i64 {
+        match self {
+            TimeFrame::Day1 => 60,
+            TimeFrame::Week1 => 60 * 5,
+            TimeFrame::Month1 => 60 * 30,
+            TimeFrame::Month3 => 60 * 60,
+            TimeFrame::Month6 => 60 * 60,
+            _ => 60 * 60 * 24,
+        }
+    }
+
     pub fn format_time(&self, timestamp: i64) -> String {
         let utc_date = Utc.timestamp(timestamp, 0);
         let local_date = utc_date.with_timezone(&Local);
diff --git a/src/main.rs b/src/main.rs
index a1bc78c..5ad64d2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
 extern crate tickrs_api as api;
 
+use std::collections::HashMap;
 use std::io::{self, Write};
 use std::sync::{Arc, Mutex, RwLock};
 use std::time::Duration;
@@ -9,6 +10,7 @@ use crossbeam_channel::{bounded, select, unbounded, Receiver, Sender};
 use crossterm::event::{Event, MouseEvent};
 use crossterm::{cursor, execute, terminal, write_ansi_code};
 use lazy_static::lazy_static;
+use service::default_timestamps::DefaultTimestampService;
 use tui::backend::CrosstermBackend;
 use tui::Terminal;
 
@@ -36,6 +38,7 @@ lazy_static! {
     pub static ref ENABLE_PRE_POST: RwLock<bool> = RwLock::new(OPTS.enable_pre_post);
     pub static ref TRUNC_PRE: bool = OPTS.trunc_pre;
     pub static ref SHOW_VOLUMES: RwLock<bool> = RwLock::new(OPTS.show_volumes);
+    pub static ref DEFAULT_TIMESTAMPS: RwLock<HashMap<TimeFrame, Vec<i64>>> = Default::default();
 }
 
 fn main() {
@@ -67,6 +70,8 @@ fn main() {
         app::Mode::DisplayStock
     };
 
+    let default_timestamp_service = DefaultTimestampService::new();
+
     let app = Arc::new(Mutex::new(app::App {
         mode: starting_mode,
         stocks: starting_stocks,
@@ -90,6 +95,7 @@ fn main() {
             app::Mode::DisplayStock
         },
         summary_time_frame: opts.time_frame,
+        default_timestamp_service,
     }));
 
     let move_app = app.clone();
@@ -124,6 +130,8 @@ fn main() {
             recv(data_received) -> _ => {
                 let mut app = app.lock().unwrap();
 
+                app.update();
+
                 for stock in app.stocks.iter_mut() {
                     stock.update();
 
diff --git a/src/service.rs b/src/service.rs
index 43fbe7c..8b1dd27 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -1,3 +1,4 @@
+pub mod default_timestamps;
 pub mod options;
 pub mod stock;
 
diff --git a/src/service/default_timestamps.rs b/src/service/default_timestamps.rs
new file mode 100644
index 0000000..fd0afbb
--- /dev/null
+++ b/src/service/default_timestamps.rs
@@ -0,0 +1,26 @@
+use std::collections::HashMap;
+
+use super::*;
+use crate::common::TimeFrame;
+use crate::task::*;
+
+pub struct DefaultTimestampService {
+    handle: AsyncTaskHandle<HashMap<TimeFrame, Vec<i64>>>,
+}
+
+impl DefaultTimestampService {
+    pub fn new() -> DefaultTimestampService {
+        let task = DefaultTimestamps::new();
+        let handle = task.connect();
+
+        DefaultTimestampService { handle }
+    }
+}
+
+impl Service for DefaultTimestampService {
+    type Update = HashMap<TimeFrame, Vec<i64>>;
+
+    fn updates(&self) -> Vec<Self::Update> {
+        self.handle.response().try_iter().collect()
+    }
+}
diff --git a/src/task.rs b/src/task.rs
index 82ed7f5..2413c5f 100644
--- a/src/task.rs
+++ b/src/task.rs
@@ -7,6 +7,7 @@ use futures::future::BoxFuture;
 
 pub use self::company::Company;
 pub use self::current_price::CurrentPrice;
+pub use self::default_timestamps::DefaultTimestamps;
 pub use self::options_data::OptionsData;
 pub use self::options_dates::OptionsDates;
 pub use self::prices::Prices;
@@ -14,6 +15,7 @@ use crate::{DATA_RECEIVED, UPDATE_INTERVAL};
 
 mod company;
 mod current_price;
+mod default_timestamps;
 mod options_data;
 mod options_dates;
 mod prices;
diff --git a/src/task/default_timestamps.rs b/src/task/default_timestamps.rs
new file mode 100644
index 0000000..120f5f7
--- /dev/null
+++ b/src/task/default_timestamps.rs
@@ -0,0 +1,63 @@
+use std::collections::HashMap;
+use std::convert::identity;
+
+use async_std::sync::Arc;
+use futures::future::{join_all, BoxFuture};
+
+use super::*;
+use crate::common::TimeFrame;
+
+/// Default timestamps to reference for stocks that haven't been around as long
+/// as the interval we are trying to graph
+pub struct DefaultTimestamps {}
+
+impl DefaultTimestamps {
+    pub fn new() -> DefaultTimestamps {
+        DefaultTimestamps {}
+    }
+}
+
+impl AsyncTask for DefaultTimestamps {
+    type Input = ();
+    type Response = HashMap<TimeFrame, Vec<i64>>;
+
+    fn update_interval(&self) -> Option<Duration> {
+        Some(Duration::from_secs(60 * 15))
+    }
+
+    fn input(&self) -> Self::Input {}
+
+    fn task<'a>(_input: Arc<Self::Input>) -> BoxFuture<'a, Option<Self::Response>> {
+        Box::pin(async move {
+            let client = Arc::new(api::Client::new());
+
+            let symbol = "SPY";
+
+            let tasks = TimeFrame::ALL[1..].iter().map(|timeframe| {
+                let client_clone = client.clone();
+
+                async move {
+                    let interval = timeframe.api_interval();
+                    let range = timeframe.as_range();
+
+                    if let Ok(chart) = client_clone
+                        .get_chart_data(symbol, interval, range, false)
+                        .await
+                    {
+                        Some((*timeframe, chart.timestamp))
+                    } else {
+                        None
+                    }
+                }
+            });
+
+            Some(
+                join_all(tasks)
+                    .await
+                    .into_iter()
+                    .filter_map(identity)
+                    .collect(),
+            )
+        })
+    }
+}
diff --git a/src/task/options_dates.rs b/src/task/options_dates.rs
index 6fbfaa5..dc0ec26 100644
--- a/src/task/options_dates.rs
+++ b/src/task/options_dates.rs
@@ -21,7 +21,7 @@ impl AsyncTask for OptionsDates {
     type Response = Vec<i64>;
 
     fn update_interval(&self) -> Option<Duration> {
-        Some(Duration::from_secs(60 * 60))
+        Some(Duration::from_secs(60 * 15))
     }
 
     fn input(&self) -> Self::Input {
diff --git a/src/task/prices.rs b/src/task/prices.rs
index 921ef04..5dea9b8 100644
--- a/src/task/prices.rs
+++ b/src/task/prices.rs
@@ -3,7 +3,6 @@ use futures::future::BoxFuture;
 
 use super::*;
 use crate::api::model::ChartMeta;
-use crate::api::Interval;
 use crate::common::{chart_data_to_prices, Price, TimeFrame};
 
 /// Returns an array of prices, depending on the TimeFrame chosen
@@ -36,14 +35,7 @@ impl AsyncTask for Prices {
             let time_frame = input.1;
             let client = &input.2;
 
-            let interval = match time_frame {
-                TimeFrame::Day1 => Interval::Minute1,
-                TimeFrame::Week1 => Interval::Minute5,
-                TimeFrame::Month1 => Interval::Minute30,
-                TimeFrame::Month3 => Interval::Minute60,
-                TimeFrame::Month6 => Interval::Minute60,
-                _ => Interval::Day1,
-            };
+            let interval = time_frame.api_interval();
 
             let include_pre_post = time_frame == TimeFrame::Day1;
 
diff --git a/src/widget/stock.rs b/src/widget/stock.rs
index 33806ae..739ac88 100644
--- a/src/widget/stock.rs
+++ b/src/widget/stock.rs
@@ -14,8 +14,8 @@ use crate::common::*;
 use crate::draw::{add_padding, PaddingDirection};
 use crate::service::{self, Service};
 use crate::{
-    ENABLE_PRE_POST, HIDE_PREV_CLOSE, HIDE_TOGGLE, SHOW_VOLUMES, SHOW_X_LABELS, TIME_FRAME,
-    TRUNC_PRE,
+    DEFAULT_TIMESTAMPS, ENABLE_PRE_POST, HIDE_PREV_CLOSE, HIDE_TOGGLE, SHOW_VOLUMES, SHOW_X_LABELS,
+    TIME_FRAME, TRUNC_PRE,
 };
 
 const NUM_LOADING_TICKS: usize = 4;
@@ -86,6 +86,11 @@ impl StockState {
 
         let max_time = prices.last().map(|p| p.date).unwrap_or(end);
 
+        let default_timestamps = {
+            let defaults = DEFAULT_TIMESTAMPS.read().unwrap();
+            defaults.get(&self.time_frame).cloned()
+        };
+
         let prices = if self.time_frame == TimeFrame::Day1 {
             let times = MarketHours(start, max_time);
 
@@ -105,6 +110,25 @@ impl StockState {
                     }
                 })
                 .collect::<Vec<_>>()
+        } else if let Some(default_timestamps) = default_timestamps {
+            default_timestamps
+                .into_iter()
+                .map(|t| {
+                    if let Some(p) = prices.iter().find(|p| {
+                        let a_rounded = p.date - p.date % self.time_frame.round_by();
+                        let b_rounded = t - t % self.time_frame.round_by();
+
+                        a_rounded == b_rounded
+                    }) {
+                        *p
+                    } else {
+                        Price {
+                            date: t,
+                            ..Default::default()
+                        }
+                    }
+                })
+                .collect::<Vec<_>>()
         } else {
             prices
         };
@@ -403,7 +427,7 @@ impl StockState {
     }
 
     pub fn pct_change(&self) -> f64 {
-        if self.prices().count() == 0 {
+        if self.prices().filter(|p| p.close > 0.0).count() == 0 {
             return 0.0;
         }
 
@@ -411,10 +435,16 @@ impl StockState {
             if let Some(prev_close) = self.prev_close_price {
                 prev_close
             } else {
-                self.prices().next().map(|d| d.close).unwrap()
+                self.prices()
+                    .find(|p| p.close > 0.0)
+                    .map(|d| d.close)
+                    .unwrap()
             }
         } else {
-            self.prices().next().map(|d| d.close).unwrap()
+            self.prices()
+                .find(|p| p.close > 0.0)
+                .map(|d| d.close)
+                .unwrap()
         };
 
         self.current_price() / baseline - 1.0
@@ -768,13 +798,12 @@ impl StatefulWidget for StockWidget {
                 .graph_type(graph_type)
                 .data(&reg_prices)];
 
-            if let Some(data) = pre_prices.as_ref() {
-                datasets.insert(
-                    0,
+            if let Some(data) = post_prices.as_ref() {
+                datasets.push(
                     Dataset::default()
                         .marker(Marker::Braille)
                         .style(
-                            Style::default().fg(if trading_period != TradingPeriod::Pre {
+                            Style::default().fg(if trading_period != TradingPeriod::Post {
                                 Color::DarkGray
                             } else if pct_change >= 0.0 {
                                 Color::Green
@@ -787,13 +816,13 @@ impl StatefulWidget for StockWidget {
                 );
             }
 
-            if let Some(data) = post_prices.as_ref() {
+            if let Some(data) = pre_prices.as_ref() {
                 datasets.insert(
                     0,
                     Dataset::default()
                         .marker(Marker::Braille)
                         .style(
-                            Style::default().fg(if trading_period != TradingPeriod::Post {
+                            Style::default().fg(if trading_period != TradingPeriod::Pre {
                                 Color::DarkGray
                             } else if pct_change >= 0.0 {
                                 Color::Green
diff --git a/src/widget/stock_summary.rs b/src/widget/stock_summary.rs
index 9c3238e..4286413 100644
--- a/src/widget/stock_summary.rs
+++ b/src/widget/stock_summary.rs
@@ -260,13 +260,12 @@ impl StatefulWidget for StockSummaryWidget {
                 .graph_type(graph_type)
                 .data(&reg_prices)];
 
-            if let Some(data) = pre_prices.as_ref() {
-                datasets.insert(
-                    0,
+            if let Some(data) = post_prices.as_ref() {
+                datasets.push(
                     Dataset::default()
                         .marker(Marker::Braille)
                         .style(
-                            Style::default().fg(if trading_period != TradingPeriod::Pre {
+                            Style::default().fg(if trading_period != TradingPeriod::Post {
                                 Color::DarkGray
                             } else if pct_change >= 0.0 {
                                 Color::Green
@@ -279,13 +278,13 @@ impl StatefulWidget for StockSummaryWidget {
                 );
             }
 
-            if let Some(data) = post_prices.as_ref() {
+            if let Some(data) = pre_prices.as_ref() {
                 datasets.insert(
                     0,
                     Dataset::default()
                         .marker(Marker::Braille)
                         .style(
-                            Style::default().fg(if trading_period != TradingPeriod::Post {
+                            Style::default().fg(if trading_period != TradingPeriod::Pre {
                                 Color::DarkGray
                             } else if pct_change >= 0.0 {
                                 Color::Green


Test patch: