Files
foxhunt/crates/ctrader-openapi/src/account.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

130 lines
3.7 KiB
Rust

//! Account information and position reconciliation.
use std::time::Duration;
use prost::Message;
use tracing::info;
use crate::dispatch::MessageDispatcher;
use crate::error::{CTraderError, Result};
use crate::proto::{self, ProtoMessage, ProtoOaOrder, ProtoOaPosition, ProtoOaTrader};
/// Account trader information.
#[derive(Debug, Clone)]
pub struct TraderInfo {
/// Account balance in deposit currency (cents / base units).
pub balance: i64,
/// Deposit currency asset ID.
pub deposit_asset_id: i64,
/// Leverage in cents (e.g. 10000 = 1:100).
pub leverage_in_cents: u32,
/// Registration timestamp (Unix ms).
pub registration_timestamp: i64,
/// Raw trader proto for additional fields.
pub raw: ProtoOaTrader,
}
/// Result of a position/order reconciliation.
#[derive(Debug, Clone)]
pub struct ReconcileResult {
/// Open positions.
pub positions: Vec<ProtoOaPosition>,
/// Pending orders.
pub orders: Vec<ProtoOaOrder>,
}
/// Query account trader information.
pub async fn get_trader_info(
dispatcher: &MessageDispatcher,
account_id: i64,
timeout: Duration,
) -> Result<TraderInfo> {
info!("querying trader info");
let req = proto::ProtoOaTraderReq {
payload_type: Some(proto::PT_TRADER_REQ as i32),
ctid_trader_account_id: account_id,
};
let envelope = ProtoMessage {
payload_type: proto::PT_TRADER_REQ,
payload: Some(req.encode_to_vec()),
client_msg_id: None,
};
let resp = dispatcher.send_request(envelope, timeout).await?;
if resp.payload_type != proto::PT_TRADER_RES {
return Err(CTraderError::ProtocolError(format!(
"expected TraderRes ({}), got {}",
proto::PT_TRADER_RES,
resp.payload_type
)));
}
let trader_res = proto::ProtoOaTraderRes::decode(
resp.payload
.as_deref()
.ok_or_else(|| CTraderError::ProtocolError("empty TraderRes".into()))?,
)?;
// `trader` is a required field in the proto — prost gives it directly
let trader = trader_res.trader;
Ok(TraderInfo {
balance: trader.balance,
deposit_asset_id: trader.deposit_asset_id,
leverage_in_cents: trader.leverage_in_cents.unwrap_or(0),
registration_timestamp: trader.registration_timestamp.unwrap_or(0),
raw: trader,
})
}
/// Reconcile open positions and pending orders.
pub async fn reconcile(
dispatcher: &MessageDispatcher,
account_id: i64,
timeout: Duration,
) -> Result<ReconcileResult> {
info!("reconciling positions and orders");
let req = proto::ProtoOaReconcileReq {
payload_type: Some(proto::PT_RECONCILE_REQ as i32),
ctid_trader_account_id: account_id,
return_protection_orders: Some(false),
};
let envelope = ProtoMessage {
payload_type: proto::PT_RECONCILE_REQ,
payload: Some(req.encode_to_vec()),
client_msg_id: None,
};
let resp = dispatcher.send_request(envelope, timeout).await?;
if resp.payload_type != proto::PT_RECONCILE_RES {
return Err(CTraderError::ProtocolError(format!(
"expected ReconcileRes ({}), got {}",
proto::PT_RECONCILE_RES,
resp.payload_type
)));
}
let reconcile_res = proto::ProtoOaReconcileRes::decode(
resp.payload
.as_deref()
.ok_or_else(|| CTraderError::ProtocolError("empty ReconcileRes".into()))?,
)?;
info!(
positions = reconcile_res.position.len(),
orders = reconcile_res.order.len(),
"reconcile complete"
);
Ok(ReconcileResult {
positions: reconcile_res.position,
orders: reconcile_res.order,
})
}