parent
9469a2a06a
commit
e3dbe48ee8
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@ |
||||
[package] |
||||
name = "secret_trader" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
tdapi = { path = "/home/g/Code/Rust/tdapi" } |
||||
serde = "1.0.152" |
||||
serde_json = "1.0.91" |
||||
reqwest = "0.11.13" |
||||
futures = "0.3.25" |
||||
actix-web = "4" |
||||
@ -0,0 +1,18 @@ |
||||
# Trade Mgmt |
||||
[X] Function to get account #s |
||||
[X] Function to get current open orders |
||||
[X] Function to submit market equity orders |
||||
[X] Function to cancel orders |
||||
|
||||
# API |
||||
[ ] GET account IDs |
||||
[ ] GET open orders |
||||
[ ] POST new secret stop loss |
||||
[ ] POST cancel order |
||||
|
||||
# DB |
||||
[ ] Store new account |
||||
- username |
||||
- user pw hash |
||||
- client id |
||||
- refresh token |
||||
@ -0,0 +1,4 @@ |
||||
use std::fmt::{Display}; |
||||
|
||||
|
||||
pub type DynResult<T> = Result<T, Box<dyn std::error::Error>>; |
||||
@ -0,0 +1,27 @@ |
||||
use tdapi::account::tda_account::ApiAccount; |
||||
|
||||
mod trade_mgmt; |
||||
mod errors; |
||||
mod order; |
||||
|
||||
#[actix_web::main] |
||||
async fn main() { |
||||
let TEST_ACT = "PNNX2GJXDTQNWY0SRCYGZMUHBFUUQHW7"; |
||||
let TEST_CALLBACK_URI = "https://localhost"; |
||||
|
||||
let mut tda = match ApiAccount::new(TEST_ACT, TEST_CALLBACK_URI) { |
||||
Ok(act) => act, |
||||
Err(e) => {println!("Failed to create TD act:\n{e:?}"); return;} |
||||
}; |
||||
|
||||
let act_nums = trade_mgmt::get_account_ids(&mut tda).await; |
||||
println!("act_nums = {:?}", act_nums); |
||||
|
||||
let main_act = 686092736; |
||||
|
||||
let buy_order = order::MarketOrder::new("SPY", 300.0, 1.0, order::Direction::BUY); |
||||
println!("buy_order = {:?}", buy_order); |
||||
|
||||
let order_send = trade_mgmt::send_order(&mut tda, main_act, &buy_order).await; |
||||
println!("order_send = {:?}", order_send); |
||||
}
|
||||
@ -0,0 +1,58 @@ |
||||
use serde::Deserialize; |
||||
use serde::Serialize; |
||||
|
||||
use tdapi::helpers::ToHashMap; |
||||
|
||||
pub enum Direction { |
||||
BUY, |
||||
SELL |
||||
} |
||||
impl Direction { |
||||
fn to_string(self) -> String { |
||||
match self { |
||||
BUY => "BUY".to_string(), |
||||
SELL => "Sell".to_string() |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] |
||||
#[serde(rename_all = "camelCase")] |
||||
pub struct MarketOrder { |
||||
pub order_type: String, |
||||
pub session: String, |
||||
pub duration: String, |
||||
pub order_strategy_type: String, |
||||
pub order_leg_collection: Vec<OrderLegCollection>, |
||||
} |
||||
impl MarketOrder { |
||||
pub fn new(symbol: &str, price: f64, quantity: f64, direction: Direction) -> Self { |
||||
MarketOrder { |
||||
order_type: "MARKET".to_string(), |
||||
session: "SEAMLESS".to_string(), |
||||
duration: "GOOD_TILL_CANCEL".to_string(), |
||||
order_strategy_type: "SINGLE".to_string(), |
||||
order_leg_collection: vec![OrderLegCollection { |
||||
instruction: direction.to_string(), |
||||
quantity: quantity, |
||||
instrument: Instrument { symbol: symbol.to_string(), asset_type: "EQUITY".to_string() } |
||||
}] |
||||
} |
||||
} |
||||
} |
||||
impl ToHashMap for MarketOrder {} |
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] |
||||
#[serde(rename_all = "camelCase")] |
||||
pub struct OrderLegCollection { |
||||
pub instruction: String, |
||||
pub quantity: f64, |
||||
pub instrument: Instrument, |
||||
} |
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] |
||||
#[serde(rename_all = "camelCase")] |
||||
pub struct Instrument { |
||||
pub symbol: String, |
||||
pub asset_type: String, |
||||
} |
||||
@ -0,0 +1,180 @@ |
||||
use std::collections::HashMap; |
||||
use serde::Deserialize; |
||||
use serde::Serialize; |
||||
use serde_json::Value; |
||||
|
||||
use tdapi::account::tda_account::ApiAccount; |
||||
use tdapi::helpers::ToHashMap; |
||||
use crate::errors::{DynResult}; |
||||
use crate::order::MarketOrder; |
||||
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] |
||||
#[serde(rename_all = "camelCase")] |
||||
pub struct Position { |
||||
pub average_price: f64, |
||||
pub current_day_cost: f64, |
||||
pub current_day_profit_loss: f64, |
||||
pub current_day_profit_loss_percentage: f64, |
||||
pub instrument: Instrument, |
||||
pub long_quantity: f64, |
||||
pub maintenance_requirement: f64, |
||||
pub market_value: f64, |
||||
pub previous_session_long_quantity: f64, |
||||
pub settled_long_quantity: f64, |
||||
pub settled_short_quantity: f64, |
||||
pub short_quantity: f64, |
||||
} |
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] |
||||
#[serde(rename_all = "camelCase")] |
||||
pub struct Instrument { |
||||
pub asset_type: String, |
||||
pub cusip: String, |
||||
pub description: Option<String>, |
||||
pub symbol: String, |
||||
#[serde(rename = "type")] |
||||
pub type_field: Option<String>, |
||||
} |
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] |
||||
#[serde(rename_all = "camelCase")] |
||||
pub struct OrderLeg { |
||||
pub instruction: String, |
||||
pub instrument: Instrument, |
||||
pub leg_id: i64, |
||||
pub order_leg_type: String, |
||||
pub position_effect: String, |
||||
pub quantity: f64, |
||||
} |
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] |
||||
#[serde(rename_all = "camelCase")] |
||||
pub struct OpenOrderDetails { |
||||
pub symbol: String, |
||||
pub price: f64, |
||||
pub quantity: f64, |
||||
pub instruction: String, |
||||
pub order_type: String, |
||||
pub order_id: u64 |
||||
} |
||||
|
||||
|
||||
/// Returns current TD account ID's along with their current liquidation values
|
||||
pub async fn get_account_ids<'a>( td_act: &mut ApiAccount<'a>) -> DynResult<Vec<(u32,f64)>>{ |
||||
let sec_acts = td_act.get_accounts().await?; |
||||
|
||||
let mut act_nums = vec![]; |
||||
|
||||
for act in sec_acts { |
||||
act_nums.push( (act.account_id.parse::<u32>()?, act.current_balances.liquidation_value)) |
||||
} |
||||
|
||||
Ok(act_nums) |
||||
} |
||||
|
||||
|
||||
pub async fn get_open_positions<'a>(td_act: &mut ApiAccount<'a>, act_num: u32) -> DynResult<Vec<Position>> { |
||||
|
||||
let mut query = HashMap::new(); |
||||
query.insert("fields".to_string(), "positions".to_string()); |
||||
let td_resp = td_act.perform_get(&format!("https://api.tdameritrade.com/v1/accounts/{act_num}"), Some(query)).await?; |
||||
|
||||
|
||||
let td_obj = td_resp.as_object() |
||||
.ok_or("Failed to create object from td_resp")?; |
||||
|
||||
// Get the securitiesAccount subjection of the return
|
||||
// This has slightly different components than the regular accounts call so Securities struct is not used (for now)
|
||||
let sec_act = td_obj.get("securitiesAccount") |
||||
.ok_or("securitiesAccount key not found in TD resp")? |
||||
.as_object().ok_or("securitiesAccount could not be made into an object")?; |
||||
|
||||
let position_values= sec_act.get("positions").ok_or("No positions key found")? |
||||
.as_array().ok_or("Positions is not an array!")?; |
||||
|
||||
let mut positions = vec![]; |
||||
for p in position_values { |
||||
let position: Position = serde_json::from_value(p.clone())?; |
||||
if position.instrument.asset_type != "CASH_EQUIVALENT" { |
||||
positions.push(position); |
||||
} |
||||
} |
||||
Ok(positions) |
||||
} |
||||
|
||||
pub async fn get_open_orders<'a>( td_act: &mut ApiAccount<'a>, act_num: u32) ->DynResult<Vec<OpenOrderDetails>> { |
||||
|
||||
let mut query = HashMap::new(); |
||||
query.insert("fields".to_string(), "orders".to_string()); |
||||
let td_resp = td_act.perform_get(&format!("https://api.tdameritrade.com/v1/accounts/{act_num}"), Some(query)).await?; |
||||
|
||||
|
||||
let td_obj = td_resp.as_object() |
||||
.ok_or("Failed to create object from td_resp")?.to_owned(); |
||||
|
||||
// Get the securitiesAccount subjection of the return
|
||||
// This has slightly different components than the regular accounts call so Securities struct is not used (for now)
|
||||
let sec_act = td_obj.get("securitiesAccount") |
||||
.ok_or("securitiesAccount key not found in TD resp")? |
||||
.as_object().ok_or("securitiesAccount could not be made into an object")?.to_owned(); |
||||
|
||||
let order_strats = sec_act.get("orderStrategies").ok_or("No orderStrategies included in securities account!")? |
||||
.as_array().ok_or("Order strategies is not an array")?; |
||||
|
||||
let mut open_orders = vec![]; |
||||
for order_value in order_strats { |
||||
let order = order_value.as_object().ok_or("Order value is not obj")?; |
||||
|
||||
// If an order isn't cancelable, we don't care about it
|
||||
if order.get("cancelable").ok_or("Order does not have key: cancelable")?.as_bool().unwrap() { |
||||
|
||||
let price = order.get("price").ok_or("price not present")?.as_f64().ok_or("Price is not f64")?; |
||||
let quantity = order.get("remainingQuantity").ok_or("quantity not present")?.as_f64().ok_or("Quant is not f64")?; |
||||
let order_type = order.get("orderType").ok_or("orderType not present")?.as_str().ok_or("orderType is not str")?; |
||||
let order_id = order.get("orderId").ok_or("orderId not present")?.as_u64().ok_or("orderId is not u64")?; |
||||
|
||||
let order_leg: OrderLeg = serde_json::from_value(order.get("orderLegCollection") |
||||
.ok_or("orderLegCollection not present")?.as_array() |
||||
.ok_or("Not an array")?[0] |
||||
.clone())?; |
||||
|
||||
let open_order = OpenOrderDetails { |
||||
symbol: order_leg.instrument.symbol, |
||||
order_id: order_id, |
||||
price: price, |
||||
quantity: quantity, |
||||
order_type : order_type.to_string(), |
||||
instruction : order_leg.order_leg_type |
||||
}; |
||||
open_orders.push(open_order); |
||||
}
|
||||
} |
||||
Ok(open_orders) |
||||
} |
||||
|
||||
|
||||
/// This works despite returning an error ¯\_('_')_/¯
|
||||
pub async fn cancel_order<'a>(td_act: &mut ApiAccount<'a>, account_id: u32, order_id: u64) -> DynResult<serde_json::Value> { |
||||
let resp = td_act.perform_delete(&format!("https://api.tdameritrade.com/v1/accounts/{account_id}/orders/{order_id}")).await; |
||||
resp |
||||
} |
||||
|
||||
pub async fn send_order<'a>(td_act: &mut ApiAccount<'a>, act_id: u32, order: &MarketOrder) -> DynResult<Value> { |
||||
|
||||
let td_resp: Value = match td_act.perform_post(&format!("https://api.tdameritrade.com/v1/accounts/{act_id}/orders"), order).await { |
||||
Ok(good_resp) => { |
||||
match good_resp.as_object() { |
||||
Some(resp_obj) => { |
||||
if resp_obj.contains_key("error") { |
||||
return Err(format!("TD Resp error: {:?}", resp_obj.get("error").unwrap().as_str().unwrap()).into()) |
||||
} |
||||
good_resp |
||||
}, |
||||
None => return Err("TD resp is not an object!".into()) |
||||
} |
||||
}, |
||||
Err(e) => return Err(e) |
||||
}; |
||||
Ok(td_resp) |
||||
} |
||||
Loading…
Reference in new issue