diff --git a/src/cli.rs b/src/cli.rs index 2745075..5ce97b1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,10 +1,12 @@ pub mod analyze; +pub mod init; mod orders; pub mod quote; pub mod run; mod status; use crate::cli::analyze::AnalyzeArgs; +use crate::cli::init::InitArgs; use crate::cli::orders::OrdersArgs; use crate::cli::quote::QuoteArgs; use crate::cli::run::RunCommandArgs; @@ -21,6 +23,8 @@ pub struct Cli { pub enum Command { #[command(about = "Analyze stocks")] Analyze(AnalyzeArgs), + #[command(about = "Generate a starter config file")] + Init(InitArgs), #[command(about = "Fetch recent orders")] Orders(OrdersArgs), #[command(about = "Fetch quote")] diff --git a/src/cli/init.rs b/src/cli/init.rs new file mode 100644 index 0000000..d5e682f --- /dev/null +++ b/src/cli/init.rs @@ -0,0 +1,20 @@ +use clap::{Args, ValueEnum}; +use std::path::PathBuf; + +#[derive(Args, Debug)] +pub struct InitArgs { + /// Type of config to generate (greed, strategy, agent) + #[arg(value_name = "TYPE", value_enum)] + pub config_type: InitConfigType, + + /// Path to write the template file (defaults to current directory) + #[arg(value_name = "PATH")] + pub path: Option, +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum InitConfigType { + Greed, + Strategy, + Agent, +} diff --git a/src/config/agent.rs b/src/config/agent.rs index abbda2e..8fdcbaa 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -92,7 +92,12 @@ impl AgentProvider { fn resolve_env_var_url(url: &str) -> Result { if let Some(var_name) = url.strip_prefix('$') { - Ok(env::var(var_name)?) + env::var(var_name).map_err(|_| { + GreedError::new(&format!( + "agent url environment variable '{}' is not set", + var_name + )) + }) } else { Ok(url.to_string()) } @@ -173,7 +178,12 @@ mod tests { url: "$GREED_NONEXISTENT_VAR_XYZ".to_string(), model: "llama3".to_string(), }; - assert!(provider.resolve_env_vars().is_err()); + let err = provider.resolve_env_vars().unwrap_err(); + assert!( + err.to_string() + .contains("agent url environment variable 'GREED_NONEXISTENT_VAR_XYZ' is not set"), + "unexpected error message: {err}" + ); } #[test] diff --git a/src/lib.rs b/src/lib.rs index ff1ac05..3dfa448 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ pub mod run; mod statistics; mod strategy; mod tactic; +pub mod template; mod trading_days; pub async fn greed_loop(args: GreedRunnerArgs) -> Result<(), GreedError> { diff --git a/src/main.rs b/src/main.rs index 5454072..cdfebc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,9 @@ use clap::{CommandFactory, Parser}; use log::LevelFilter; use simplelog::{ColorChoice, CombinedLogger, Config, ConfigBuilder, TermLogger, TerminalMode}; +use greed::error::GreedError; use greed::platform::args::PlatformArgs; +use greed::template; use greed::{analyze_stocks, fetch_quote, fetch_recent_orders, fetch_status, greed_loop}; use crate::cli::{Cli, Command}; @@ -22,6 +24,12 @@ async fn async_main(log_config: Config) { let cli = Cli::parse(); let command = cli.command; match command { + Command::Init(args) => { + generate_config_template(args) + .await + .expect("config template generation failed"); + } + Command::Analyze(args) => { analyze_stocks( &args.symbols, @@ -64,6 +72,26 @@ async fn async_main(log_config: Config) { } } +async fn generate_config_template(args: cli::init::InitArgs) -> Result<(), GreedError> { + use cli::init::InitConfigType; + + let (tmpl, filename) = match args.config_type { + InitConfigType::Greed => (template::greed_config_template(), "greed.toml"), + InitConfigType::Strategy => (template::strategy_config_template(), "strategy.toml"), + InitConfigType::Agent => (template::agent_config_template(), "agent.toml"), + }; + + let output_path = match args.path { + Some(p) if p.is_dir() => p.join(filename), + Some(p) => p, + None => std::env::current_dir()?.join(filename), + }; + + tokio::fs::write(&output_path, tmpl).await?; + println!("Wrote template to {}", output_path.display()); + Ok(()) +} + fn create_log_config() -> Config { ConfigBuilder::new() .set_time_offset_to_local() diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..c288616 --- /dev/null +++ b/src/template.rs @@ -0,0 +1,94 @@ +pub fn greed_config_template() -> &'static str { + r#"# Greed configuration file + +# Platform to use for trading. Currently only "alpaca" is supported. +platform = "alpaca" + +# How often (in seconds) to run the trading loop. +interval = 60 + +# Strategies allow you to compose multiple tactic configs together. +# Each strategy references a file path or agent config, and gets a share of your portfolio. +# [[strategies]] +# name = "My Strategy" +# portfolio_percent = 100.0 # Percentage of portfolio allocated to this strategy +# path = "strategy.toml" # Path to a local tactic config file +# # OR use an AI agent strategy: +# # agent_path = "agent.toml" + +# Tactics define buy/sell rules for individual assets. +# [[tactics]] +# name = "ETF" +# [tactics.buy] +# for = { stock = "VTI" } +# when = { below_median_percent = 5.0, median_period = "month" } +# do = { buy_percent = 10.0 } +# [tactics.sell] +# for = { stock = "VTI" } +# when = { gain_above_percent = 5.0 } +# do = { sell_all = true } +"# +} + +pub fn strategy_config_template() -> &'static str { + r#"# Strategy configuration file +# A strategy defines buy/sell tactics for one or more assets. + +# Platform to use for trading. Currently only "alpaca" is supported. +platform = "alpaca" + +# How often (in seconds) to run the trading loop. +interval = 60 + +# Each [[tactics]] block defines buy/sell rules for one asset. +[[tactics]] +name = "VTI" + +[tactics.buy] +for = { stock = "VTI" } +when = { below_median_percent = 1.0 } +do = { buy_percent = 25 } + +[tactics.sell] +for = { stock = "VTI" } +when = { gain_above_percent = 1.0 } +do = { sell_all = true } +"# +} + +pub fn agent_config_template() -> &'static str { + r#"# Agent configuration file +# An agent uses an AI model to make trading decisions. + +# The system prompt that describes the agent's trading strategy and behavior. +prompt = "You are a trading agent. Analyze the current portfolio and market conditions, then decide whether to buy or sell." + +# Provider configuration for the AI model. +[agent_provider] +# Provider type. Currently only "Ollama" is supported. +type = "Ollama" +# URL of the Ollama server. Can be a literal URL or an environment variable (e.g. "$OLLAMA_URL"). +url = "http://localhost:11434" +# The model to use (e.g. "llama3", "mistral"). +model = "llama3" + +# Optional allowlist of stock symbols the agent is permitted to trade. +# If empty, all symbols are allowed. +# allow = ["VTI", "SPY"] + +# Optional denylist of stock symbols the agent is not permitted to trade. +# deny = ["GME", "AMC"] + +# Tool permissions — set any to false to disable that capability for the agent. +[tools] +account = true +positions = true +open_orders = true +quotes = true +buy = true +sell = true +web_fetch = true +read_note = true +write_note = true +"# +}