Skip to content

Commit

Permalink
Feature/cli (#5)
Browse files Browse the repository at this point in the history
* cqf cli && add readme cli demo
  • Loading branch information
Liberxue authored Aug 26, 2024
1 parent 8d17a79 commit d0a12d5
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 57 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

[![](https://img.shields.io/badge/Rust-1.79.0+-blue)](https://releases.rs/docs/1.79.0)

## CLI DEMO
[<img src="./ui/cqf.gif" width="200%"/>](./ui/cqf.gif)


## Examples

<details>
Expand Down
2 changes: 2 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ clap = { version = "4.0", features = ["derive"] }
burn = { version = "0.13.2", features = ["train", "wgpu", "vision"] }
core = { path = "../core" }
plotters = "0.3.6"
ratatui = "0.28.0"
crossterm = "0.28.1"
Binary file removed cli/options_price_chart.png
Binary file not shown.
270 changes: 213 additions & 57 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
extern crate core;
extern crate plotters;

use clap::Parser;
use core::models::{BlackScholesModel, OptionParameters, OptionPricingModel};
use plotters::prelude::*;
use core::models::{OptionParameters, OptionPricingModel};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Cell, Row, Table, TableState},
Frame, Terminal,
};
use std::io::{self};
use std::{fs, path::Path};

#[derive(Parser)]
struct Opts {
Expand All @@ -18,70 +28,216 @@ struct Opts {
#[arg(short, long)]
t: f64,
}
// example
fn main() {

struct ModelWrapper {
name: String,
model: Box<dyn OptionPricingModel>,
}

struct App {
models: Vec<ModelWrapper>,
table_state: TableState,
params: OptionParameters,
}

impl App {
fn new(opts: Opts) -> Self {
let params = OptionParameters {
s: opts.s,
k: opts.k,
r: opts.r,
sigma: opts.sigma,
t: opts.t,
};
let mut table_state = TableState::default();
table_state.select(Some(0));
App {
models: load_models(),
table_state,
params,
}
}

fn next(&mut self) {
let i = match self.table_state.selected() {
Some(i) => {
if i >= self.models.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.table_state.select(Some(i));
}

fn previous(&mut self) {
let i = match self.table_state.selected() {
Some(i) => {
if i == 0 {
self.models.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.table_state.select(Some(i));
}
}

fn create_model(model_name: &str) -> Option<Box<dyn OptionPricingModel>> {
match model_name {
"black_scholes" => Some(Box::new(core::models::BlackScholesModel)),
"binomial_tree" => Some(Box::new(core::models::BinomialTreeModel::default())),
"garch" => Some(Box::new(core::models::GarchModel::default())),
"monte_carlo" => Some(Box::new(core::models::MonteCarloModel {
simulations: 1000,
epsilon: 0.01,
})),
_ => None,
}
}

fn is_valid_model_file(entry: &fs::DirEntry) -> bool {
entry.path().is_file()
&& entry.path().extension().and_then(|s| s.to_str()) == Some("rs")
&& entry
.path()
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s != "mod")
.unwrap_or(false)
}

fn create_model_wrapper(entry: &fs::DirEntry) -> Option<ModelWrapper> {
entry
.path()
.file_stem()
.and_then(|s| s.to_str())
.and_then(|model_name| {
create_model(model_name).map(|model| ModelWrapper {
name: model_name.to_string(),
model,
})
})
}

fn load_models() -> Vec<ModelWrapper> {
let model_dir = Path::new("../core/src/models");

fs::read_dir(model_dir)
.map(|entries| {
entries
.filter_map(Result::ok)
.filter(is_valid_model_file)
.filter_map(|entry| create_model_wrapper(&entry))
.collect()
})
.unwrap_or_else(|_| Vec::new())
}

fn main() -> Result<(), io::Error> {
let opts: Opts = Opts::parse();
let mut app = App::new(opts);

enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;

let model = BlackScholesModel;
let params = OptionParameters {
s: opts.s,
k: opts.k,
r: opts.r,
sigma: opts.sigma,
t: opts.t,
};
let res = run_app(&mut terminal, &mut app);

let call_price = model.call_price(&params);
let put_price = model.put_price(&params);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;

println!("Call Price: {:.2}", call_price);
println!("Put Price: {:.2}", put_price);
if let Err(err) = res {
println!("Error: {:?}", err)
}

generate_chart(&model, &params);
Ok(())
}

fn generate_chart(model: &BlackScholesModel, params: &OptionParameters) {
let root_area = BitMapBackend::new("options_price_chart.png", (640, 480)).into_drawing_area();
root_area.fill(&WHITE).unwrap();
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;

let mut chart = ChartBuilder::on(&root_area)
.caption("Option Prices", ("sans-serif", 50).into_font())
.margin(10)
.x_label_area_size(30)
.y_label_area_size(30)
.build_cartesian_2d(80.0..120.0, 0.0..20.0)
.unwrap();
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
_ => {}
}
}
}
}

chart.configure_mesh().draw().unwrap();
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Percentage(100)].as_ref())
.split(f.area());

let call_series = (80..=120).map(|s| {
let mut params = params.clone();
params.s = s as f64;
(s as f64, model.call_price(&params))
let header_cells = [
"Models",
"Call Price",
"Put Price",
"Delta",
"Gamma",
"Vega",
"Theta",
"Rho",
]
.iter()
.map(|h| {
Cell::from(*h).style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED),
)
});
let header = Row::new(header_cells).style(Style::default().bg(Color::Black));

let put_series = (80..=120).map(|s| {
let mut params = params.clone();
params.s = s as f64;
(s as f64, model.put_price(&params))
let rows = app.models.iter().map(|wrapper| {
let cells = vec![
Cell::from(wrapper.name.as_str()),
Cell::from(format!("{:.4}", wrapper.model.call_price(&app.params))),
Cell::from(format!("{:.4}", wrapper.model.put_price(&app.params))),
Cell::from(format!("{:.4}", wrapper.model.delta(&app.params))),
Cell::from(format!("{:.4}", wrapper.model.gamma(&app.params))),
Cell::from(format!("{:.4}", wrapper.model.vega(&app.params))),
Cell::from(format!("{:.4}", wrapper.model.theta(&app.params))),
Cell::from(format!("{:.4}", wrapper.model.rho(&app.params))),
];
Row::new(cells)
});

chart
.draw_series(LineSeries::new(call_series, &RED))
.unwrap()
.label("Call Price")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));

chart
.draw_series(LineSeries::new(put_series, &BLUE))
.unwrap()
.label("Put Price")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE));

chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()
.unwrap();
let widths = [
Constraint::Percentage(15),
Constraint::Percentage(12),
Constraint::Percentage(12),
Constraint::Percentage(12),
Constraint::Percentage(12),
Constraint::Percentage(12),
Constraint::Percentage(12),
Constraint::Percentage(13),
];

let table = Table::new(rows, widths)
.header(header)
.block(Block::default())
.highlight_style(Style::default().bg(Color::Yellow).fg(Color::Black))
.highlight_symbol("> ");

f.render_stateful_widget(table, chunks[0], &mut app.table_state.clone());
}
Binary file added ui/cqf.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d0a12d5

Please sign in to comment.