Merge pull request #20 from selimaj-dev/status-config-ui
Status, config, and UI
This commit is contained in:
@@ -16,6 +16,20 @@ pub enum LogKind {
|
|||||||
Debug,
|
Debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ItemState {
|
||||||
|
Starting,
|
||||||
|
Running,
|
||||||
|
Stopped,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum Mode {
|
||||||
|
Auto,
|
||||||
|
Manual,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Signal {
|
pub struct Signal {
|
||||||
pub symbol: String,
|
pub symbol: String,
|
||||||
@@ -41,6 +55,12 @@ pub struct Position {
|
|||||||
pub pnl: Decimal,
|
pub pnl: Decimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct EngineStatus {
|
||||||
|
pub strategy_mode: Mode,
|
||||||
|
pub strategy_state: ItemState,
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for MarketTrend {
|
impl std::fmt::Display for MarketTrend {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -61,3 +81,23 @@ impl std::fmt::Display for LogKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Mode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Auto => write!(f, "\x1b[96mAUTO\x1b[0m"),
|
||||||
|
Self::Manual => write!(f, "\x1b[97mMANUAL\x1b[0m"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ItemState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Starting => write!(f, "\x1b[93mSTARTING\x1b[0m"),
|
||||||
|
Self::Running => write!(f, "\x1b[92mRUNNING\x1b[0m"),
|
||||||
|
Self::Stopped => write!(f, "\x1b[90mSTOPPED\x1b[0m"),
|
||||||
|
Self::Error => write!(f, "\x1b[91mERROR\x1b[0m"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+13
-54
@@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
general::{EventLog, Position, Signal},
|
general::{EngineStatus, EventLog, Position, Signal},
|
||||||
strategy::StrategyManifest,
|
strategy::StrategyManifest,
|
||||||
};
|
};
|
||||||
use hypersdk::{Decimal, hypercore::CandleInterval};
|
use hypersdk::Decimal;
|
||||||
|
|
||||||
pub type SignalStatus = Result<Signal, Signal>;
|
pub type SignalStatus = Result<Signal, Signal>;
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ pub enum TerminalServerMessage {
|
|||||||
// Positions
|
// Positions
|
||||||
PositionsUpdated(Vec<Position>),
|
PositionsUpdated(Vec<Position>),
|
||||||
|
|
||||||
// Strategy
|
// Configuration
|
||||||
StrategyUpdated(StrategyStatus),
|
ConfigUpdated(EngineConfig),
|
||||||
|
|
||||||
// Signals
|
// Signals
|
||||||
SignalsUpdated(Vec<SignalStatus>),
|
SignalsUpdated(Vec<SignalStatus>),
|
||||||
@@ -24,7 +24,7 @@ pub enum TerminalServerMessage {
|
|||||||
Inspect(InspectTarget),
|
Inspect(InspectTarget),
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
StatusUpdated(Status),
|
StatusUpdated(EngineStatus),
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
SetLogs(Vec<EventLog>),
|
SetLogs(Vec<EventLog>),
|
||||||
@@ -47,62 +47,21 @@ pub struct MarketItem {
|
|||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum InspectTarget {
|
pub enum InspectTarget {
|
||||||
None,
|
None,
|
||||||
Some(Vec<InspectItem>),
|
Some(Vec<InspectRow>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct InspectRow(pub InspectItem, pub InspectItem, pub InspectItem);
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum InspectItem {
|
pub enum InspectItem {
|
||||||
String(String),
|
Name(String),
|
||||||
Symbol(String),
|
Symbol(String),
|
||||||
USD(f64),
|
USD(Decimal),
|
||||||
F64(f64),
|
F64(Decimal),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Status {
|
pub struct EngineConfig {
|
||||||
pub feed: Mode,
|
|
||||||
pub exchange: String,
|
|
||||||
pub dex: String,
|
|
||||||
pub latency: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum Mode {
|
|
||||||
Auto,
|
|
||||||
Manual,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum ItemState {
|
|
||||||
Running,
|
|
||||||
Stopped,
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct StrategyStatus {
|
|
||||||
pub strategy: StrategyManifest,
|
pub strategy: StrategyManifest,
|
||||||
|
|
||||||
pub mode: Mode,
|
|
||||||
pub state: ItemState,
|
|
||||||
pub cooldown: CandleInterval,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Mode {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Auto => write!(f, "\x1b[96mAUTO\x1b[0m"),
|
|
||||||
Self::Manual => write!(f, "\x1b[97mMANUAL\x1b[0m"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ItemState {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Running => write!(f, "\x1b[92mRUNNING\x1b[0m"),
|
|
||||||
Self::Stopped => write!(f, "\x1b[90mSTOPPED\x1b[0m"),
|
|
||||||
Self::Error => write!(f, "\x1b[91mERROR\x1b[0m"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-39
@@ -15,58 +15,79 @@ pub struct RenderScope {
|
|||||||
draw_instructions: Vec<Instr>,
|
draw_instructions: Vec<Instr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RenderTextResult {
|
||||||
|
text: String,
|
||||||
|
line_len: u16,
|
||||||
|
lines_len: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderTextResult {
|
||||||
|
fn insert_newline(&mut self, height: u16) -> bool {
|
||||||
|
if self.lines_len >= height {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text.push('\n');
|
||||||
|
self.line_len = 0;
|
||||||
|
self.lines_len += 1;
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert(&mut self, c: char) {
|
||||||
|
self.text.push(c);
|
||||||
|
self.line_len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderScope {
|
impl RenderScope {
|
||||||
pub fn draw_text<P: Into<Point>, T: Display>(&mut self, at: P, text: T) -> u16 {
|
pub fn draw_text<P: Into<Point>, T: Display>(&mut self, at: P, text: T) -> u16 {
|
||||||
|
if self.rect.width == 0 || self.rect.height == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
let point: Point = at.into();
|
let point: Point = at.into();
|
||||||
|
|
||||||
let lines = text
|
let text = text.to_string();
|
||||||
.to_string()
|
|
||||||
.lines()
|
let mut chars = text.chars().peekable();
|
||||||
.map(|line| {
|
|
||||||
let mut chars = line.chars().peekable();
|
let mut result = RenderTextResult {
|
||||||
let mut new_line = String::new();
|
text: String::new(),
|
||||||
let mut len = 0;
|
line_len: 0,
|
||||||
|
lines_len: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '\x1b' && chars.peek() == Some(&'[') {
|
||||||
|
result.text.push(c);
|
||||||
|
|
||||||
while let Some(c) = chars.next() {
|
while let Some(c) = chars.next() {
|
||||||
if c == '\x1b' && chars.peek() == Some(&'[') {
|
result.text.push(c);
|
||||||
new_line.push(c);
|
|
||||||
|
|
||||||
while let Some(c) = chars.next() {
|
if c.is_ascii_alphabetic() {
|
||||||
new_line.push(c);
|
break;
|
||||||
|
|
||||||
if c.is_ascii_alphabetic() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
new_line.push(c);
|
|
||||||
len += 1;
|
|
||||||
|
|
||||||
if len >= self.rect.width as usize {
|
|
||||||
new_line.push('\n');
|
|
||||||
len = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if c == '\n' {
|
||||||
|
if result.insert_newline(self.rect.height) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.insert(c);
|
||||||
|
|
||||||
new_line
|
if result.line_len >= self.rect.width {
|
||||||
})
|
if result.insert_newline(self.rect.height) {
|
||||||
.collect::<Vec<_>>()
|
break;
|
||||||
.join("\n");
|
}
|
||||||
|
}
|
||||||
let lines = lines
|
}
|
||||||
.lines()
|
}
|
||||||
.take((self.rect.height - point.y) as usize)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let lines_len = lines.len();
|
|
||||||
|
|
||||||
self.draw_instructions
|
self.draw_instructions
|
||||||
.push(Instr::DrawText(point, lines.join("\n")));
|
.push(Instr::DrawText(point, result.text));
|
||||||
|
|
||||||
lines_len as u16
|
result.lines_len
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +144,10 @@ impl RenderScope {
|
|||||||
match inst {
|
match inst {
|
||||||
Instr::DrawText(point, text) => {
|
Instr::DrawText(point, text) => {
|
||||||
for (i, line) in text.lines().enumerate() {
|
for (i, line) in text.lines().enumerate() {
|
||||||
|
if i as u16 + point.y >= self.rect.height {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
print_at(
|
print_at(
|
||||||
self.rect.x + point.x,
|
self.rect.x + point.x,
|
||||||
self.rect.y + point.y + i as u16,
|
self.rect.y + point.y + i as u16,
|
||||||
|
|||||||
@@ -10,20 +10,17 @@ pub struct ScrollText {
|
|||||||
|
|
||||||
impl Widget for ScrollText {
|
impl Widget for ScrollText {
|
||||||
fn render(&self, scope: &mut crate::render::RenderScope) {
|
fn render(&self, scope: &mut crate::render::RenderScope) {
|
||||||
scope.draw_text(0, &self.title);
|
let title_lines = scope.draw_text(0, &self.title);
|
||||||
|
|
||||||
let title_lines = self.title.lines().count();
|
let text = self
|
||||||
|
|
||||||
let mut y = title_lines as u16;
|
|
||||||
|
|
||||||
for line in self
|
|
||||||
.text
|
.text
|
||||||
.lines()
|
.lines()
|
||||||
.skip(self.scroll)
|
.skip(self.scroll)
|
||||||
.take(scope.rect.height as usize - title_lines)
|
.map(|v| v.trim_end())
|
||||||
{
|
.collect::<Vec<_>>()
|
||||||
y += scope.draw_text((0, y), line);
|
.join("\n");
|
||||||
}
|
|
||||||
|
scope.draw_text((0, title_lines), text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,16 @@ impl Engine {
|
|||||||
match args[0] {
|
match args[0] {
|
||||||
"reload" => {
|
"reload" => {
|
||||||
*self.config.lock().await = Config::new().await?;
|
*self.config.lock().await = Config::new().await?;
|
||||||
|
self.terminal_server
|
||||||
|
.info("engine::config", "successfully reloaded")
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
"save" => {
|
"save" => {
|
||||||
self.config.lock().await.save().await?;
|
self.config.lock().await.save().await?;
|
||||||
|
self.terminal_server
|
||||||
|
.info("engine::config", "successfully saved")
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
"set" => {
|
"set" => {
|
||||||
@@ -49,6 +55,12 @@ impl Engine {
|
|||||||
self.terminal_server
|
self.terminal_server
|
||||||
.info("config::set", "watchlist set successfully, use `config save` to persist changes")
|
.info("config::set", "watchlist set successfully, use `config save` to persist changes")
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
self.terminal_server
|
||||||
|
.broadcast(TerminalServerMessage::ConfigUpdated(
|
||||||
|
self.get_config_status().await,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
"strategy" | "strat" | "sg" => {
|
"strategy" | "strat" | "sg" => {
|
||||||
@@ -62,7 +74,7 @@ impl Engine {
|
|||||||
return self
|
return self
|
||||||
.terminal_server
|
.terminal_server
|
||||||
.error(
|
.error(
|
||||||
"config::set::strategy",
|
"config::set[strategy]",
|
||||||
&format!("Non existent strategy `{id}`"),
|
&format!("Non existent strategy `{id}`"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -75,23 +87,24 @@ impl Engine {
|
|||||||
self.terminal_server
|
self.terminal_server
|
||||||
.info("config::set", "strategy set successfully, use `config save` to persist changes")
|
.info("config::set", "strategy set successfully, use `config save` to persist changes")
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
self.terminal_server
|
||||||
|
.broadcast(TerminalServerMessage::ConfigUpdated(
|
||||||
|
self.get_config_status().await,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
"cooldown" | "cool" | "cd" => {
|
"cooldown" | "cool" | "cd" => {
|
||||||
set_cfg!(cooldown, {
|
set_cfg!(cooldown, {
|
||||||
self.config.lock().await.cooldown = cooldown;
|
self.config.lock().await.cooldown = cooldown;
|
||||||
|
|
||||||
self.terminal_server.broadcast(
|
|
||||||
pulse_sdk::terminal::TerminalServerMessage::StrategyUpdated(StrategyStatus {
|
|
||||||
strategy: self.strategy_engine.strategy.lock().await.manifest.clone(),
|
|
||||||
|
|
||||||
mode: Mode::Auto,
|
|
||||||
state: ItemState::Running,
|
|
||||||
cooldown,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.terminal_server
|
||||||
|
.broadcast(TerminalServerMessage::ConfigUpdated(
|
||||||
|
self.get_config_status().await,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
@@ -101,25 +114,25 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.invalid_command_usage("config").await?;
|
self.invalid_command_usage("engine::config").await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
self.invalid_command_usage("config").await?;
|
self.invalid_command_usage("engine::config").await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"account" | "acc" => {
|
"account" | "acc" => {
|
||||||
if args.len() == 0 {
|
if args.len() == 0 {
|
||||||
return self.invalid_command_usage("account man").await;
|
return self.invalid_command_usage("engine::account").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
match args[0] {
|
match args[0] {
|
||||||
"list" | "ls" => {
|
"list" | "ls" => {
|
||||||
self.terminal_server
|
self.terminal_server
|
||||||
.info("account man", "ACCOUNT LIST")
|
.info("engine::account", "ACCOUNT LIST")
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let accounts = self.accounts.lock().await;
|
let accounts = self.accounts.lock().await;
|
||||||
@@ -127,7 +140,7 @@ impl Engine {
|
|||||||
for (name, acc) in &accounts.accounts {
|
for (name, acc) in &accounts.accounts {
|
||||||
self.terminal_server
|
self.terminal_server
|
||||||
.info(
|
.info(
|
||||||
"account man",
|
"engine::account",
|
||||||
&if name == &accounts.active {
|
&if name == &accounts.active {
|
||||||
format!(
|
format!(
|
||||||
"{} (active) -> {}",
|
"{} (active) -> {}",
|
||||||
@@ -144,7 +157,7 @@ impl Engine {
|
|||||||
|
|
||||||
"use" | "set" => {
|
"use" | "set" => {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
return self.invalid_command_usage("account man").await;
|
return self.invalid_command_usage("engine::account").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_active = args[1];
|
let new_active = args[1];
|
||||||
@@ -154,7 +167,10 @@ impl Engine {
|
|||||||
if !accounts.accounts.contains_key(new_active) {
|
if !accounts.accounts.contains_key(new_active) {
|
||||||
return self
|
return self
|
||||||
.terminal_server
|
.terminal_server
|
||||||
.error("account man", &format!("Account not found ({new_active})"))
|
.error(
|
||||||
|
"engine::account",
|
||||||
|
&format!("Account not found ({new_active})"),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,14 +178,14 @@ impl Engine {
|
|||||||
|
|
||||||
self.terminal_server
|
self.terminal_server
|
||||||
.info(
|
.info(
|
||||||
"account man",
|
"engine::account",
|
||||||
&format!("Account set to {new_active} successfully!"),
|
&format!("Account set to {new_active} successfully!"),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
return self.invalid_command_usage("account man").await;
|
return self.invalid_command_usage("engine::account").await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,20 +195,23 @@ impl Engine {
|
|||||||
return self.invalid_command_usage("strategy").await;
|
return self.invalid_command_usage("strategy").await;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.strategy_engine
|
match strategy_command {
|
||||||
.send(&StrategyEngineMessage::Command {
|
"start" => {}
|
||||||
command: strategy_command.to_owned(),
|
|
||||||
args: args.into_iter().map(Into::into).collect(),
|
_ => {
|
||||||
})
|
self.strategy_engine
|
||||||
.await?;
|
.send(&StrategyEngineMessage::Command {
|
||||||
|
command: strategy_command.to_owned(),
|
||||||
|
args: args.into_iter().map(Into::into).collect(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
self.terminal_server
|
self.terminal_server
|
||||||
.error(
|
.error("engine::cmd", &format!("Command '{}' not found", command))
|
||||||
"Command executor",
|
|
||||||
&format!("Command '{}' not found", command),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ pub struct Engine {
|
|||||||
// data
|
// data
|
||||||
pub config: Arc<Mutex<Config>>,
|
pub config: Arc<Mutex<Config>>,
|
||||||
pub accounts: Arc<Mutex<AccountList>>,
|
pub accounts: Arc<Mutex<AccountList>>,
|
||||||
|
|
||||||
|
// live data / status
|
||||||
|
pub status: Arc<Mutex<EngineStatus>>,
|
||||||
pub watch_list: Arc<Mutex<WatchList>>,
|
pub watch_list: Arc<Mutex<WatchList>>,
|
||||||
pub signals: Arc<Mutex<Vec<SignalStatus>>>,
|
pub signals: Arc<Mutex<Vec<SignalStatus>>>,
|
||||||
}
|
}
|
||||||
@@ -44,11 +47,20 @@ impl Engine {
|
|||||||
let config = Arc::new(Mutex::new(config));
|
let config = Arc::new(Mutex::new(config));
|
||||||
|
|
||||||
Ok(Arc::new_cyclic(|engine| Self {
|
Ok(Arc::new_cyclic(|engine| Self {
|
||||||
config,
|
// engine
|
||||||
accounts,
|
|
||||||
ws_stream: Arc::new(Mutex::new(ws_stream)),
|
|
||||||
terminal_server: TerminalServer::new(engine.clone()),
|
terminal_server: TerminalServer::new(engine.clone()),
|
||||||
strategy_engine: strategy.initialize(engine.clone()),
|
strategy_engine: strategy.initialize(engine.clone()),
|
||||||
|
ws_stream: Arc::new(Mutex::new(ws_stream)),
|
||||||
|
|
||||||
|
// data
|
||||||
|
config,
|
||||||
|
accounts,
|
||||||
|
|
||||||
|
// live data / status
|
||||||
|
status: Arc::new(Mutex::new(EngineStatus {
|
||||||
|
strategy_mode: Mode::Auto,
|
||||||
|
strategy_state: ItemState::Stopped,
|
||||||
|
})),
|
||||||
watch_list: Arc::new(Mutex::new(WatchList {
|
watch_list: Arc::new(Mutex::new(WatchList {
|
||||||
name_to_index: HashMap::new(),
|
name_to_index: HashMap::new(),
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
@@ -61,11 +73,33 @@ impl Engine {
|
|||||||
/// When a strategy is reloaded it restarts automatically
|
/// When a strategy is reloaded it restarts automatically
|
||||||
pub async fn run(&self) -> anyhow::Result<()> {
|
pub async fn run(&self) -> anyhow::Result<()> {
|
||||||
loop {
|
loop {
|
||||||
|
self.terminal_server
|
||||||
|
.info("engine::main", "Strategy starting")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.status.lock().await.strategy_state = ItemState::Starting;
|
||||||
|
|
||||||
|
self.update_status().await?;
|
||||||
|
|
||||||
self.strategy_engine.run().await?;
|
self.strategy_engine.run().await?;
|
||||||
|
|
||||||
self.terminal_server
|
self.terminal_server
|
||||||
.info("engine::main", "Strategy stopped, restarting")
|
.info("engine::main", "Strategy stopped")
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_config_status(&self) -> EngineConfig {
|
||||||
|
EngineConfig {
|
||||||
|
strategy: self.strategy_engine.strategy.lock().await.manifest.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_status(&self) -> tokio::io::Result<()> {
|
||||||
|
self.terminal_server
|
||||||
|
.broadcast(TerminalServerMessage::StatusUpdated(
|
||||||
|
self.status.lock().await.clone(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,14 @@ impl StrategyEngine {
|
|||||||
|
|
||||||
self.send(&StrategyEngineMessage::Initialize).await?;
|
self.send(&StrategyEngineMessage::Initialize).await?;
|
||||||
|
|
||||||
|
engine
|
||||||
|
.terminal_server
|
||||||
|
.info("engine::main", "Strategy Started")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
engine.status.lock().await.strategy_state = ItemState::Running;
|
||||||
|
engine.update_status().await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match StrategyChild::read(&mut stdout).await? {
|
match StrategyChild::read(&mut stdout).await? {
|
||||||
None => {}
|
None => {}
|
||||||
@@ -158,8 +166,6 @@ impl StrategyEngine {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.as_millis() as u64;
|
.as_millis() as u64;
|
||||||
|
|
||||||
println!("getting candlestick");
|
|
||||||
|
|
||||||
let interval_ms = match interval {
|
let interval_ms = match interval {
|
||||||
CandleInterval::OneMinute => 60_000,
|
CandleInterval::OneMinute => 60_000,
|
||||||
CandleInterval::ThreeMinutes => 3 * 60_000,
|
CandleInterval::ThreeMinutes => 3 * 60_000,
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ impl TerminalServer {
|
|||||||
|
|
||||||
let listener = UnixListener::bind(&path)?;
|
let listener = UnixListener::bind(&path)?;
|
||||||
|
|
||||||
println!("Terminal server listening on {:?}", path);
|
self.info(
|
||||||
|
"engine::terminal",
|
||||||
|
&format!("Terminal server listening on {:?}", path),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (stream, _) = listener.accept().await?;
|
let (stream, _) = listener.accept().await?;
|
||||||
@@ -60,7 +64,23 @@ impl TerminalServer {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(err) = s.handle_client(&id, reader).await {
|
if let Err(err) = s.handle_client(&id, reader).await {
|
||||||
eprintln!("Terminal connection error: {err}");
|
if matches!(err.kind(), tokio::io::ErrorKind::UnexpectedEof) {
|
||||||
|
Self::print_log(&EventLog {
|
||||||
|
kind: LogKind::Err,
|
||||||
|
name: "engine::terminal".to_string(),
|
||||||
|
message: format!("Terminal connection error: {err}"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
s.error(
|
||||||
|
"engine::terminal",
|
||||||
|
&format!("Terminal connection error: {err}"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,18 +91,13 @@ impl TerminalServer {
|
|||||||
|
|
||||||
self.send_to(
|
self.send_to(
|
||||||
id,
|
id,
|
||||||
pulse_sdk::terminal::TerminalServerMessage::StrategyUpdated(StrategyStatus {
|
TerminalServerMessage::ConfigUpdated(engine.get_config_status().await),
|
||||||
strategy: engine
|
)
|
||||||
.strategy_engine
|
.await?;
|
||||||
.strategy
|
|
||||||
.lock()
|
self.send_to(
|
||||||
.await
|
id,
|
||||||
.manifest
|
TerminalServerMessage::StatusUpdated(engine.status.lock().await.clone()),
|
||||||
.clone(),
|
|
||||||
mode: Mode::Auto,
|
|
||||||
state: ItemState::Running,
|
|
||||||
cooldown: engine.config.lock().await.cooldown,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -147,7 +162,12 @@ impl TerminalServer {
|
|||||||
for (id, client) in clients.iter_mut() {
|
for (id, client) in clients.iter_mut() {
|
||||||
if let Err(e) = Self::send_to_client(client, &msg).await {
|
if let Err(e) = Self::send_to_client(client, &msg).await {
|
||||||
remove_clients.push(*id);
|
remove_clients.push(*id);
|
||||||
println!("{e:?}");
|
|
||||||
|
Self::print_log(&EventLog {
|
||||||
|
kind: LogKind::Err,
|
||||||
|
name: "engine::terminal".to_string(),
|
||||||
|
message: format!("Failed to broadcast to client: {e}"),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,19 +284,32 @@ impl TerminalServer {
|
|||||||
name: &str,
|
name: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
) -> tokio::io::Result<()> {
|
) -> tokio::io::Result<()> {
|
||||||
let log = EventLog {
|
self.log_raw(EventLog {
|
||||||
kind,
|
kind,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
};
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
self.logs.lock().await.push(log.clone());
|
pub fn print_log(log: &EventLog) {
|
||||||
|
println!(
|
||||||
self.broadcast(pulse_sdk::terminal::TerminalServerMessage::AddLog(log))
|
"[{}{}\x1b[0m] (\x1b[37m{}\x1b[0m) {}",
|
||||||
.await
|
match &log.kind {
|
||||||
|
LogKind::Info => "\x1b[34m",
|
||||||
|
LogKind::Debug => "\x1b[36m",
|
||||||
|
LogKind::Err => "\x1b[31m",
|
||||||
|
LogKind::Warn => "\x1b[33m",
|
||||||
|
},
|
||||||
|
log.kind,
|
||||||
|
log.name,
|
||||||
|
log.message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn log_raw(self: &Arc<Self>, log: EventLog) -> tokio::io::Result<()> {
|
pub async fn log_raw(self: &Arc<Self>, log: EventLog) -> tokio::io::Result<()> {
|
||||||
|
Self::print_log(&log);
|
||||||
|
|
||||||
self.logs.lock().await.push(log.clone());
|
self.logs.lock().await.push(log.clone());
|
||||||
|
|
||||||
self.broadcast(pulse_sdk::terminal::TerminalServerMessage::AddLog(log))
|
self.broadcast(pulse_sdk::terminal::TerminalServerMessage::AddLog(log))
|
||||||
|
|||||||
+49
-37
@@ -10,15 +10,24 @@ impl Formatted for InspectTarget {
|
|||||||
match self {
|
match self {
|
||||||
Self::None => vec!["\x1b[2mnothing to inspect\x1b[0m".to_string()],
|
Self::None => vec!["\x1b[2mnothing to inspect\x1b[0m".to_string()],
|
||||||
|
|
||||||
Self::Some(items) => items
|
Self::Some(items) => {
|
||||||
.iter()
|
let items = items
|
||||||
.map(|item| match item {
|
.iter()
|
||||||
InspectItem::F64(v) => format_f64(*v),
|
.map(|row| {
|
||||||
InspectItem::USD(v) => format!("${}", format_f64(*v)),
|
(
|
||||||
InspectItem::Symbol(v) => format!("\x1b[35m{v}\x1b[0m"),
|
format_inspect_item(&row.0),
|
||||||
InspectItem::String(v) => v.clone(),
|
format_inspect_item(&row.1),
|
||||||
})
|
format_inspect_item(&row.2),
|
||||||
.collect(),
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|(a, b, c)| Triple(a, b, c))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.get_formatted()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,28 +123,8 @@ impl Formatted for Position {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Pair<'a>(&'a str, &'a str);
|
|
||||||
|
|
||||||
struct Triple<'a>(&'a str, &'a str, &'a str);
|
struct Triple<'a>(&'a str, &'a str, &'a str);
|
||||||
|
|
||||||
impl Formatted for Status {
|
|
||||||
fn get_formatted(&self) -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
Pair("Feed", &format!("{:?}", self.feed)),
|
|
||||||
Pair("Exchange", &self.exchange.clone()),
|
|
||||||
Pair("DEX", &self.dex.clone()),
|
|
||||||
Pair("Latency", &format!("{} ms", self.latency)),
|
|
||||||
]
|
|
||||||
.get_formatted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Formatted for Pair<'a> {
|
|
||||||
fn get_formatted(&self) -> Vec<String> {
|
|
||||||
vec![self.0.to_string(), self.1.to_string()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Formatted for Triple<'a> {
|
impl<'a> Formatted for Triple<'a> {
|
||||||
fn get_formatted(&self) -> Vec<String> {
|
fn get_formatted(&self) -> Vec<String> {
|
||||||
vec![self.0.to_string(), self.1.to_string(), self.2.to_string()]
|
vec![self.0.to_string(), self.1.to_string(), self.2.to_string()]
|
||||||
@@ -206,19 +195,33 @@ impl<T: Formatted> Formatted for Vec<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Formatted for StrategyStatus {
|
impl Formatted for EngineStatus {
|
||||||
|
fn get_formatted(&self) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
Triple(
|
||||||
|
"\x1b[2mState\x1b[0m",
|
||||||
|
"\x1b[2mMode\x1b[0m",
|
||||||
|
"\x1b[2m..\x1b[0m",
|
||||||
|
),
|
||||||
|
Triple(
|
||||||
|
&self.strategy_state.to_string(),
|
||||||
|
&self.strategy_mode.to_string(),
|
||||||
|
"..",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.get_formatted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Formatted for EngineConfig {
|
||||||
fn get_formatted(&self) -> Vec<String> {
|
fn get_formatted(&self) -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
Triple(
|
Triple(
|
||||||
"\x1b[2mStrategy\x1b[0m",
|
"\x1b[2mStrategy\x1b[0m",
|
||||||
"\x1b[2mState\x1b[0m",
|
"\x1b[2m..\x1b[0m",
|
||||||
"\x1b[2mMode\x1b[0m",
|
"\x1b[2m..\x1b[0m",
|
||||||
),
|
|
||||||
Triple(
|
|
||||||
&format!("\x1b[97m{}\x1b[0m", self.strategy.name),
|
|
||||||
&self.state.to_string(),
|
|
||||||
&self.mode.to_string(),
|
|
||||||
),
|
),
|
||||||
|
Triple(&self.strategy.name, "..", ".."),
|
||||||
]
|
]
|
||||||
.get_formatted()
|
.get_formatted()
|
||||||
}
|
}
|
||||||
@@ -232,6 +235,15 @@ pub fn apply_padding(mut items: Vec<String>) -> Vec<String> {
|
|||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format_inspect_item(item: &InspectItem) -> String {
|
||||||
|
match item {
|
||||||
|
InspectItem::F64(v) => format_f64(v.as_f64()),
|
||||||
|
InspectItem::USD(v) => format!("${}", format_f64(v.as_f64())),
|
||||||
|
InspectItem::Symbol(v) => format!("\x1b[35m{v}\x1b[0m"),
|
||||||
|
InspectItem::Name(v) => v.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn format_symbol(value: &str) -> String {
|
pub fn format_symbol(value: &str) -> String {
|
||||||
format!("\x1b[35m{value}\x1b[0m")
|
format!("\x1b[35m{value}\x1b[0m")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ pub struct PulseTradeApp {
|
|||||||
command: State<InputState>,
|
command: State<InputState>,
|
||||||
scroll: State<ScrollState<7>>,
|
scroll: State<ScrollState<7>>,
|
||||||
|
|
||||||
strategy: State<Option<StrategyStatus>>,
|
config: State<Option<EngineConfig>>,
|
||||||
status: State<Option<Status>>,
|
status: State<Option<EngineStatus>>,
|
||||||
|
|
||||||
watch_list: State<Vec<MarketItem>>,
|
watch_list: State<Vec<MarketItem>>,
|
||||||
active_positions: State<Vec<Position>>,
|
active_positions: State<Vec<Position>>,
|
||||||
@@ -144,8 +144,7 @@ impl App for PulseTradeApp {
|
|||||||
(
|
(
|
||||||
LayoutItem::Widget(Size::Flex(1)),
|
LayoutItem::Widget(Size::Flex(1)),
|
||||||
Box::new(
|
Box::new(
|
||||||
advanced_option_draw(&self.scroll, 3, "CONFIGURATION", &self.strategy)
|
advanced_option_draw(&self.scroll, 3, "CONFIGURATION", &self.config).await,
|
||||||
.await,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -182,7 +181,7 @@ async fn main() -> tokio::io::Result<()> {
|
|||||||
signals: ctx.use_state(Vec::new()),
|
signals: ctx.use_state(Vec::new()),
|
||||||
logs: ctx.use_state(Vec::new()),
|
logs: ctx.use_state(Vec::new()),
|
||||||
inspect: ctx.use_state(InspectTarget::None),
|
inspect: ctx.use_state(InspectTarget::None),
|
||||||
strategy: ctx.use_state(None),
|
config: ctx.use_state(None),
|
||||||
status: ctx.use_state(None),
|
status: ctx.use_state(None),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ impl TerminalClient {
|
|||||||
let active_positions = app.active_positions.clone();
|
let active_positions = app.active_positions.clone();
|
||||||
let logs = app.logs.clone();
|
let logs = app.logs.clone();
|
||||||
let signals = app.signals.clone();
|
let signals = app.signals.clone();
|
||||||
let market_overview = app.strategy.clone();
|
let market_overview = app.config.clone();
|
||||||
let status = app.status.clone();
|
let status = app.status.clone();
|
||||||
let inspect = app.inspect.clone();
|
let inspect = app.inspect.clone();
|
||||||
|
|
||||||
@@ -82,8 +82,8 @@ impl TerminalClient {
|
|||||||
active_positions: State<Vec<Position>>,
|
active_positions: State<Vec<Position>>,
|
||||||
logs: State<Vec<EventLog>>,
|
logs: State<Vec<EventLog>>,
|
||||||
signals: State<Vec<SignalStatus>>,
|
signals: State<Vec<SignalStatus>>,
|
||||||
market_overview: State<Option<StrategyStatus>>,
|
config: State<Option<EngineConfig>>,
|
||||||
status: State<Option<Status>>,
|
status: State<Option<EngineStatus>>,
|
||||||
inspect: State<InspectTarget>,
|
inspect: State<InspectTarget>,
|
||||||
) -> tokio::io::Result<()> {
|
) -> tokio::io::Result<()> {
|
||||||
loop {
|
loop {
|
||||||
@@ -111,8 +111,8 @@ impl TerminalClient {
|
|||||||
*active_positions.lock().await = v;
|
*active_positions.lock().await = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalServerMessage::StrategyUpdated(v) => {
|
TerminalServerMessage::ConfigUpdated(v) => {
|
||||||
*market_overview.lock().await = Some(v);
|
*config.lock().await = Some(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalServerMessage::SignalsUpdated(v) => {
|
TerminalServerMessage::SignalsUpdated(v) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user