Skip to content

Commit

Permalink
add kagi chart (#93)
Browse files Browse the repository at this point in the history
* add kagi chart

still need to add ability to define custom reversal amounts from config
file and in the GUI

* remember show volumes state when disabling for kagi

* add chart_type option

* add chart configuration widget

* add high low for kagi

* add kagi options to config

* allow reversals per time frame

* fix single / none match

* fix clippy

* update CHANGELOG

* add support for < & >

this doesn't work currently, but no reason to redo it when
#95 needs to be merged first
and we can rebase the proper change to that

* disable 'c' while config pane open

* properly only change selected time frame

* support backtab in chart config pane (#103)

* be strict with modifiers on matching < & > (#105)

* intuitive keybindings (#104)

* use more intuitive keybindings

up/down or tab/backtab to switch widgets

left/right to choose options

* increase info/error message box to contain all entries

* disable q:quit with kagi config pane open (#102)

* disable q:quit with kagi config pane open

* reassign q binding to close kagi config pane

* add help entry for chart config pane (#109)

* allow chart scroll with side pane open (#106)

* allow scrolling chart while on chart config pane

* allow scrolling chart while on options pane

Co-authored-by: Miraculous Owonubi <omiraculous@gmail.com>
  • Loading branch information
tarkah and miraclx committed Feb 26, 2021
1 parent 2fec8bf commit 69f76a5
Show file tree
Hide file tree
Showing 15 changed files with 1,605 additions and 113 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ and `Removed`.

## [Unreleased]

### Added

- Kagi charts have been added!
- You can specify custom reversal type (pct or amount), reversal value, and
price type (close or high_low) within the GUI by pressing 'e'
- New config options have been added to configure the behavior of Kagi charts,
see the updated [wiki entry](https://github.com/tarkah/tickrs/wiki/Config-file)
- As Kagi charts x-axis is decoupled from time, the chart width may be wider than
the terminal. You can now press <kbd>SHIFT</kbd> + <kbd><</kbd> / <kbd>></kbd>
or <kbd>SHIFT</kbd> + <kbd>LEFT</kbd> / <kbd>RIGHT</kbd> to scroll the chart.
An indicator in the bottom right corner will notify you if you can scroll further
left / right
- `--candle` has been deprecated in favor of `--chart-type`

### Packaging

- Linux: Binary size has been reduced due to some optimizations, from 10.6MB to
Expand Down
4 changes: 3 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crossterm::event::Event;

use crate::common::TimeFrame;
use crate::common::{ChartType, TimeFrame};
use crate::service::default_timestamps::DefaultTimestampService;
use crate::service::Service;
use crate::{widget, DEFAULT_TIMESTAMPS};

#[derive(PartialEq, Clone, Copy, Debug)]
pub enum Mode {
AddStock,
ConfigureChart,
DisplayStock,
DisplayOptions,
DisplaySummary,
Expand All @@ -26,6 +27,7 @@ pub struct App {
pub summary_time_frame: TimeFrame,
pub default_timestamp_service: DefaultTimestampService,
pub summary_scroll_state: SummaryScrollState,
pub chart_type: ChartType,
}

impl App {
Expand Down
33 changes: 30 additions & 3 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,49 @@ use tickrs_api::Interval;
use crate::api::model::ChartData;
use crate::api::Range;

#[derive(PartialEq, Clone, Copy, Debug, Hash)]
#[derive(PartialEq, Clone, Copy, Debug, Hash, Deserialize)]
pub enum ChartType {
#[serde(rename = "line")]
Line,
#[serde(rename = "candle")]
Candlestick,
#[serde(rename = "kagi")]
Kagi,
}

impl ChartType {
pub fn toggle(self) -> Self {
match self {
ChartType::Line => ChartType::Candlestick,
ChartType::Candlestick => ChartType::Line,
ChartType::Candlestick => ChartType::Kagi,
ChartType::Kagi => ChartType::Line,
}
}

pub fn as_str(self) -> &'static str {
match self {
ChartType::Line => "Line",
ChartType::Candlestick => "Candle",
ChartType::Kagi => "Kagi",
}
}
}

#[derive(Clone, Copy, PartialOrd, Debug, Hash, PartialEq, Eq, Deserialize)]
impl FromStr for ChartType {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
use ChartType::*;

match s {
"line" => Ok(Line),
"candle" => Ok(Candlestick),
"kagi" => Ok(Kagi),
_ => Err("Valid chart types are: 'line', 'candle', 'kagi'"),
}
}
}
#[derive(Clone, Copy, PartialOrd, Debug, Hash, PartialEq, Eq, Deserialize, Ord)]
pub enum TimeFrame {
#[serde(alias = "1D")]
Day1,
Expand Down
92 changes: 64 additions & 28 deletions src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ use tui::widgets::{Block, Borders, Clear, Paragraph, Tabs, Wrap};
use tui::{Frame, Terminal};

use crate::app::{App, Mode, ScrollDirection};
use crate::common::TimeFrame;
use crate::common::{ChartType, TimeFrame};
use crate::theme::style;
use crate::widget::{
block, AddStockWidget, OptionsWidget, StockSummaryWidget, StockWidget, HELP_HEIGHT, HELP_WIDTH,
block, AddStockWidget, ChartConfigurationWidget, OptionsWidget, StockSummaryWidget,
StockWidget, HELP_HEIGHT, HELP_WIDTH,
};
use crate::{SHOW_VOLUMES, THEME};

Expand Down Expand Up @@ -144,23 +145,24 @@ fn draw_main<B: Backend>(frame: &mut Frame<B>, app: &mut App, area: Rect) {
// Draw main widget
if let Some(stock) = app.stocks.get_mut(app.current_tab) {
// main_chunks[0] - Stock widget
// main_chunks[1] - Options widget (optional)
let mut main_chunks = if app.mode == Mode::DisplayOptions {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(44)].as_ref())
.split(layout[1])
} else {
vec![layout[1]]
};
// main_chunks[1] - Options widget / Configuration widget (optional)
let mut main_chunks =
if app.mode == Mode::DisplayOptions || app.mode == Mode::ConfigureChart {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(44)].as_ref())
.split(layout[1])
} else {
vec![layout[1]]
};

match app.mode {
Mode::DisplayStock | Mode::AddStock => {
frame.render_stateful_widget(StockWidget {}, main_chunks[0], stock);
}
// If width is too small, don't render stock widget and use entire space
// for options widget
Mode::DisplayOptions => {
// for options / configure widget
Mode::DisplayOptions | Mode::ConfigureChart => {
if main_chunks[0].width >= 19 {
frame.render_stateful_widget(StockWidget {}, main_chunks[0], stock);
} else {
Expand All @@ -170,21 +172,55 @@ fn draw_main<B: Backend>(frame: &mut Frame<B>, app: &mut App, area: Rect) {
_ => {}
}

if let Some(options) = stock.options.as_mut() {
if main_chunks[1].width >= 44 && main_chunks[1].height >= 14 {
frame.render_stateful_widget(OptionsWidget {}, main_chunks[1], options);
} else {
main_chunks[1] = add_padding(main_chunks[1], 1, PaddingDirection::Left);
main_chunks[1] = add_padding(main_chunks[1], 1, PaddingDirection::Top);

frame.render_widget(
Paragraph::new(Text::styled(
"Increase screen size to display options",
style(),
)),
main_chunks[1],
);
match app.mode {
Mode::DisplayOptions => {
if let Some(options) = stock.options.as_mut() {
if main_chunks[1].width >= 44 && main_chunks[1].height >= 14 {
frame.render_stateful_widget(OptionsWidget {}, main_chunks[1], options);
} else {
main_chunks[1] = add_padding(main_chunks[1], 1, PaddingDirection::Left);
main_chunks[1] = add_padding(main_chunks[1], 1, PaddingDirection::Top);

frame.render_widget(
Paragraph::new(Text::styled(
"Increase screen size to display options",
style(),
)),
main_chunks[1],
);
}
}
}
Mode::ConfigureChart => {
if main_chunks[1].width >= 44 && main_chunks[1].height >= 14 {
let state = &mut stock.chart_configuration;

let chart_type = stock.chart_type;
let time_frame = stock.time_frame;

frame.render_stateful_widget(
ChartConfigurationWidget {
chart_type,
time_frame,
},
main_chunks[1],
state,
);
} else {
main_chunks[1] = add_padding(main_chunks[1], 1, PaddingDirection::Left);
main_chunks[1] = add_padding(main_chunks[1], 1, PaddingDirection::Top);

frame.render_widget(
Paragraph::new(Text::styled(
"Increase screen size to display configuration screen",
style(),
))
.wrap(Wrap { trim: false }),
main_chunks[1],
);
}
}
_ => {}
}
}
}
Expand All @@ -199,7 +235,7 @@ fn draw_summary<B: Backend>(frame: &mut Frame<B>, app: &mut App, mut area: Rect)
area = add_padding(area, 1, PaddingDirection::All);
area = add_padding(area, 1, PaddingDirection::Right);

let show_volumes = *SHOW_VOLUMES.read().unwrap();
let show_volumes = *SHOW_VOLUMES.read().unwrap() && app.chart_type != ChartType::Kagi;
let stock_widget_height = if show_volumes { 7 } else { 6 };

let height = area.height;
Expand Down
89 changes: 81 additions & 8 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ use crossbeam_channel::Sender;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::app::{self, Mode};
use crate::common::ChartType;
use crate::widget::options;
use crate::{cleanup_terminal, CHART_TYPE, ENABLE_PRE_POST, SHOW_VOLUMES, SHOW_X_LABELS};
use crate::{cleanup_terminal, ENABLE_PRE_POST, SHOW_VOLUMES, SHOW_X_LABELS};

fn handle_keys_add_stock(keycode: KeyCode, mut app: &mut app::App) {
match keycode {
KeyCode::Enter => {
let mut stock = app.add_stock.enter();
let mut stock = app.add_stock.enter(app.chart_type);

if app.previous_mode == app::Mode::DisplaySummary {
stock.set_time_frame(app.summary_time_frame);
Expand Down Expand Up @@ -96,6 +97,11 @@ fn handle_keys_display_stock(keycode: KeyCode, modifiers: KeyModifiers, mut app:
app.mode = app::Mode::DisplayOptions;
}
}
(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 {
app.current_tab = 0;
Expand Down Expand Up @@ -225,6 +231,47 @@ fn handle_keys_display_options(keycode: KeyCode, mut app: &mut app::App) {
}
}

pub fn handle_keys_configure_chart(keycode: KeyCode, mut app: &mut app::App) {
match keycode {
KeyCode::Esc | KeyCode::Char('e') | KeyCode::Char('q') => {
app.stocks[app.current_tab].toggle_configure();
app.mode = app::Mode::DisplayStock;
}
KeyCode::Up | KeyCode::BackTab => {
let config = app.stocks[app.current_tab].chart_config_mut();
config.selection_up();
}
KeyCode::Down | KeyCode::Tab => {
let config = app.stocks[app.current_tab].chart_config_mut();
config.selection_down();
}
KeyCode::Left => {
let config = app.stocks[app.current_tab].chart_config_mut();
config.back_tab();
}
KeyCode::Right => {
let config = app.stocks[app.current_tab].chart_config_mut();
config.tab();
}
KeyCode::Enter => {
let time_frame = app.stocks[app.current_tab].time_frame;
let config = app.stocks[app.current_tab].chart_config_mut();
config.enter(time_frame);
}
KeyCode::Char(c) => {
if c.is_numeric() || c == '.' {
let config = app.stocks[app.current_tab].chart_config_mut();
config.add_char(c);
}
}
KeyCode::Backspace => {
let config = app.stocks[app.current_tab].chart_config_mut();
config.del_char();
}
_ => {}
}
}

pub fn handle_key_bindings(
mode: Mode,
key_event: KeyEvent,
Expand All @@ -251,21 +298,28 @@ pub fn handle_key_bindings(
app.mode = app.previous_mode;
}
}
(mode, KeyModifiers::NONE, KeyCode::Char('q')) if mode != Mode::DisplayOptions => {
(mode, KeyModifiers::NONE, KeyCode::Char('q'))
if !matches!(mode, Mode::DisplayOptions | Mode::ConfigureChart) =>
{
cleanup_terminal();
std::process::exit(0);
}
(_, KeyModifiers::NONE, KeyCode::Char('?')) => {
app.previous_mode = app.mode;
app.mode = app::Mode::Help;
}
(_, KeyModifiers::NONE, KeyCode::Char('c')) => {
let mut chart_type = CHART_TYPE.write().unwrap();
*chart_type = chart_type.toggle();
(_, KeyModifiers::NONE, KeyCode::Char('c')) if mode != Mode::ConfigureChart => {
app.chart_type = app.chart_type.toggle();

for stock in app.stocks.iter_mut() {
stock.set_chart_type(app.chart_type);
}
}
(_, KeyModifiers::NONE, KeyCode::Char('v')) => {
let mut show_volumes = SHOW_VOLUMES.write().unwrap();
*show_volumes = !*show_volumes;
if app.chart_type != ChartType::Kagi {
let mut show_volumes = SHOW_VOLUMES.write().unwrap();
*show_volumes = !*show_volumes;
}
}
(_, KeyModifiers::NONE, KeyCode::Char('p')) => {
let mut guard = ENABLE_PRE_POST.write().unwrap();
Expand All @@ -280,11 +334,30 @@ pub fn handle_key_bindings(
let mut show_x_labels = SHOW_X_LABELS.write().unwrap();
*show_x_labels = !*show_x_labels;
}
(_, KeyModifiers::SHIFT, KeyCode::Left) | (_, KeyModifiers::NONE, KeyCode::Char('<')) => {
if let Some(stock) = app.stocks.get_mut(app.current_tab) {
if let Some(chart_state) = stock.chart_state_mut() {
chart_state.scroll_left();
}
}
}
(_, KeyModifiers::SHIFT, KeyCode::Right) | (_, KeyModifiers::NONE, KeyCode::Char('>')) => {
if let Some(stock) = app.stocks.get_mut(app.current_tab) {
if let Some(chart_state) = stock.chart_state_mut() {
chart_state.scroll_right();
}
}
}
(Mode::DisplayOptions, modifiers, keycode) => {
if modifiers.is_empty() {
handle_keys_display_options(keycode, app)
}
}
(Mode::ConfigureChart, modifiers, keycode) => {
if modifiers.is_empty() {
handle_keys_configure_chart(keycode, app)
}
}
(Mode::DisplayStock, modifiers, keycode) => {
handle_keys_display_stock(keycode, modifiers, app)
}
Expand Down
Loading

0 comments on commit 69f76a5

Please sign in to comment.