Skip to content

Commit 9bc7b68

Browse files
committed
style: _
1 parent 2718720 commit 9bc7b68

File tree

1 file changed

+61
-38
lines changed

1 file changed

+61
-38
lines changed

discretionary_engine/src/adjust_pos.rs

Lines changed: 61 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,43 @@ use serde::{Deserialize, Serialize};
88
use sha2::Sha256;
99
use tracing::info;
1010
use v_exchanges::Ticker;
11-
use v_utils::trades::Timeframe;
11+
use v_utils::{log, trades::Timeframe};
1212

1313
use crate::config::AppConfig;
1414

1515
type HmacSha256 = Hmac<Sha256>;
1616

1717
#[derive(clap::Args, Debug)]
1818
#[command(group(
19-
clap::ArgGroup::new("size")
19+
clap::ArgGroup::new("size_group")
2020
.required(true)
21-
.multiple(true)
22-
.args(["size_quote", "size_usd"]),
21+
.args(["quote", "notional", "size"]),
2322
))]
2423
pub(crate) struct AdjustPosArgs {
2524
/// Ticker to adjust position for.
2625
ticker: Ticker,
2726

2827
/// Size in quote currency.
2928
#[arg(short = 'q', long)]
30-
size_quote: Option<f64>,
29+
quote: Option<f64>,
3130

32-
/// Size in USD
31+
/// Size in notional (USD)
32+
#[arg(short = 'n', long)]
33+
notional: Option<f64>,
34+
35+
/// Size with suffix inference: "$" for USD, asset name (e.g., "BTC") for that asset, or plain number for quote
3336
#[arg(short = 's', long)]
34-
size_usd: Option<f64>,
37+
size: Option<String>,
3538

3639
/// timeframe, in the format of "1m", "1h", "3M", etc.
3740
/// determines the target period for which we expect the edge to persist.
3841
#[arg(short, long)]
3942
tf: Option<Timeframe>,
4043

44+
/// Reduce-only mode: only reduce existing position, don't increase it
45+
#[arg(long)]
46+
reduce: bool,
47+
4148
/// Use testnet instead of mainnet
4249
#[arg(long)]
4350
testnet: bool,
@@ -55,6 +62,8 @@ struct BybitOrderRequest {
5562
time_in_force: String,
5663
#[serde(rename = "orderLinkId")]
5764
order_link_id: String,
65+
#[serde(rename = "reduceOnly")]
66+
reduce_only: bool,
5867
}
5968

6069
#[derive(Debug, Deserialize)]
@@ -79,9 +88,9 @@ fn convert_symbol_to_bybit(symbol: &str) -> String {
7988
without_suffix.replace('-', "").to_uppercase()
8089
}
8190

82-
/// Round quantity to the appropriate step size, rounding up to ensure minimum notional
83-
fn round_to_step_ceil(value: f64, step: f64) -> f64 {
84-
(value / step).ceil() * step
91+
/// Round quantity to the appropriate step size
92+
fn round_to_step(value: f64, step: f64) -> f64 {
93+
(value / step).round() * step
8594
}
8695

8796
/// Sign Bybit API request
@@ -95,19 +104,37 @@ fn sign_request(api_secret: &str, timestamp: &str, api_key: &str, recv_window: &
95104
pub(crate) async fn main(args: AdjustPosArgs, config: Arc<AppConfig>) -> Result<()> {
96105
info!("Starting adjust-pos for ticker: {:?}", args.ticker);
97106

98-
// Determine the target size in USD
99-
let target_usd = if let Some(usd) = args.size_usd {
100-
usd
101-
} else if let Some(quote) = args.size_quote {
102-
// For now, assume quote == USD (works for USDT pairs)
103-
// TODO: Handle conversion if quote currency != USD
104-
quote
107+
// Determine whether we have a quote amount (quantity) or notional amount (USD)
108+
enum SizeType {
109+
Quote(f64), // Actual quantity of asset
110+
Notional(f64), // USD value
111+
}
112+
113+
let size_type = if let Some(notional) = args.notional {
114+
SizeType::Notional(notional)
115+
} else if let Some(quote) = args.quote {
116+
SizeType::Quote(quote)
117+
} else if let Some(size_str) = args.size {
118+
// Parse size with suffix inference
119+
if size_str.ends_with('$') {
120+
// Strip $ and parse as USD
121+
let usd = size_str.trim_end_matches('$').parse::<f64>().context("Failed to parse USD amount from --size")?;
122+
SizeType::Notional(usd)
123+
} else if let Some(pos) = size_str.chars().position(|c| c.is_alphabetic()) {
124+
// Has a suffix like "BTC", "ETH", etc.
125+
let (number_part, asset_part) = size_str.split_at(pos);
126+
let amount = number_part.parse::<f64>().context("Failed to parse amount from --size")?;
127+
// TODO: Handle conversion from other assets to USD
128+
bail!("Asset conversion not yet implemented. Got {} {}, need to convert to USD", amount, asset_part);
129+
} else {
130+
// Plain number - treat as quote currency (actual quantity)
131+
let qty = size_str.parse::<f64>().context("Failed to parse --size as number")?;
132+
SizeType::Quote(qty)
133+
}
105134
} else {
106135
bail!("No size specified");
107136
};
108137

109-
info!("Target size: ${} USD", target_usd);
110-
111138
// Get exchange config based on ticker's exchange
112139
let exchange_config = config.get_exchange(args.ticker.exchange_name)?;
113140

@@ -168,28 +195,23 @@ pub(crate) async fn main(args: AdjustPosArgs, config: Arc<AppConfig>) -> Result<
168195
.ok_or_else(|| color_eyre::eyre::eyre!("Failed to get maxOrderQty"))?
169196
.parse()?;
170197

171-
// Get minimum notional value
172-
let min_notional: f64 = instruments_response["result"]["list"][0]["lotSizeFilter"]["minNotionalValue"]
173-
.as_str()
174-
.ok_or_else(|| color_eyre::eyre::eyre!("Failed to get minNotionalValue"))?
175-
.parse()?;
198+
info!("Instrument info - qtyStep: {}, minOrderQty: {}, maxOrderQty: {}", qty_step, min_order_qty, max_order_qty);
176199

177-
info!(
178-
"Instrument info - qtyStep: {}, minOrderQty: {}, maxOrderQty: {}, minNotional: ${}",
179-
qty_step, min_order_qty, max_order_qty, min_notional
180-
);
200+
// Calculate quantity based on size type, extracting sign for order side
201+
let (raw_quantity, side) = match size_type {
202+
SizeType::Quote(qty) => (qty, if qty >= 0.0 { "Buy" } else { "Sell" }),
203+
SizeType::Notional(usd) => {
204+
let qty = usd / current_price;
205+
(qty, if usd >= 0.0 { "Buy" } else { "Sell" })
206+
}
207+
};
181208

182-
// Calculate quantity and round UP to step size to ensure we meet minimum notional
183-
let raw_quantity = target_usd / current_price;
184-
let quantity = round_to_step_ceil(raw_quantity, qty_step).max(min_order_qty);
209+
// Work with absolute value for rounding
210+
let abs_raw_qty = raw_quantity.abs();
211+
let quantity = round_to_step(abs_raw_qty, qty_step);
185212

186-
// Verify we meet minimum notional value
187213
let actual_notional = quantity * current_price;
188-
if actual_notional < min_notional {
189-
bail!("Order value ${:.2} is below minimum notional ${:.2}. Need to increase size.", actual_notional, min_notional);
190-
}
191-
192-
info!("Calculated quantity: {:.6} -> rounded to {:.6} (notional: ${:.2})", raw_quantity, quantity, actual_notional);
214+
log!("{} order: {:.6} -> rounded to {:.6} (notional: ${:.2})", side, abs_raw_qty, quantity, actual_notional);
193215

194216
// Format quantity properly based on step size
195217
let qty_str = if qty_step >= 1.0 {
@@ -206,11 +228,12 @@ pub(crate) async fn main(args: AdjustPosArgs, config: Arc<AppConfig>) -> Result<
206228
let order_request = BybitOrderRequest {
207229
category: "linear".to_string(),
208230
symbol: symbol.clone(),
209-
side: "Buy".to_string(),
231+
side: side.to_string(),
210232
order_type: "Market".to_string(),
211233
qty: qty_str.clone(),
212234
time_in_force: "IOC".to_string(),
213235
order_link_id: format!("adjust-{}", uuid::Uuid::new_v4()),
236+
reduce_only: args.reduce,
214237
};
215238

216239
let params_json = serde_json::to_string(&order_request)?;

0 commit comments

Comments
 (0)