1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
use std::collections::HashMap;
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use uuid::Uuid;
use lnm_sdk::rest::v3::models::{FundingSettlement, LastPrice, OhlcCandle};
use crate::{shared::OhlcResolution, trade::TradeTrailingStoploss};
use super::{
error::Result,
models::{FundingSettlementRow, OhlcCandleRow, PriceTickRow},
};
#[async_trait]
/// Read-only price tick repository API.
pub trait PriceTicksRepositoryRead: Send + Sync {
/// Returns the latest known price entry as `(time, price)`, or `None` when no price data exists.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// let latest = db.price_ticks().get_latest_entry().await?;
/// # Ok(())
/// # }
/// ```
async fn get_latest_entry(&self) -> Result<Option<(DateTime<Utc>, f64)>>;
/// Returns the price range since `start` as `(min_price, max_price, latest_time, latest_price)`.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// use chrono::{Duration, Utc};
///
/// let start = Utc::now() - Duration::hours(24);
/// let range = db.price_ticks().get_price_range_from(start).await?;
/// # Ok(())
/// # }
/// ```
async fn get_price_range_from(
&self,
start: DateTime<Utc>,
) -> Result<Option<(f64, f64, DateTime<Utc>, f64)>>;
}
#[async_trait]
pub(crate) trait PriceTicksRepository: PriceTicksRepositoryRead {
/// Adds multiple price ticks to the database in a single batch operation.
/// Uses INSERT ON CONFLICT DO NOTHING to avoid duplicate entries.
///
/// Returns only the ticks that were successfully inserted (new entries).
async fn add_ticks(&self, ticks: &[LastPrice]) -> Result<Vec<PriceTickRow>>;
async fn remove_ticks(&self, before: DateTime<Utc>) -> Result<()>;
}
#[async_trait]
/// Read-only running trades repository API.
pub trait RunningTradesRepositoryRead: Send + Sync {
/// Returns running trades for `account_id`, keyed by trade ID.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// let account_id = uuid::Uuid::new_v4();
/// let running_trades = db
/// .running_trades()
/// .get_running_trades_map(account_id)
/// .await?;
/// # Ok(())
/// # }
/// ```
async fn get_running_trades_map(
&self,
account_id: Uuid,
) -> Result<HashMap<Uuid, Option<TradeTrailingStoploss>>>;
}
#[async_trait]
pub(crate) trait RunningTradesRepository: RunningTradesRepositoryRead {
async fn add_running_trade(
&self,
account_id: Uuid,
trade_id: Uuid,
trailing_stoploss: Option<TradeTrailingStoploss>,
) -> Result<()>;
async fn remove_running_trades(&self, account_id: Uuid, trade_ids: &[Uuid]) -> Result<()>;
}
#[async_trait]
/// Read-only OHLC candle repository API.
pub trait OhlcCandlesRepositoryRead: Send + Sync {
/// Fetches one-minute OHLC candles within the specified time range, ordered by time ASC.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// use chrono::{Duration, Utc};
///
/// let to = Utc::now();
/// let from = to - Duration::hours(24);
///
/// let candles = db.ohlc_candles().get_candles(from, to).await?;
/// # Ok(())
/// # }
/// ```
async fn get_candles(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> Result<Vec<OhlcCandleRow>>;
/// Fetches OHLC candles consolidated to the specified resolution.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// use chrono::{Duration, Utc};
/// use quantoxide::models::OhlcResolution;
///
/// let to = Utc::now();
/// let from = to - Duration::hours(24);
/// let resolution = OhlcResolution::OneHour;
///
/// let candles = db
/// .ohlc_candles()
/// .get_candles_consolidated(from, to, resolution)
/// .await?;
/// # Ok(())
/// # }
/// ```
async fn get_candles_consolidated(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
resolution: OhlcResolution,
) -> Result<Vec<OhlcCandleRow>>;
/// Returns the earliest candle time in the database, or `None` when no candles exist.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// let earliest = db.ohlc_candles().get_earliest_candle_time().await?;
/// # Ok(())
/// # }
/// ```
async fn get_earliest_candle_time(&self) -> Result<Option<DateTime<Utc>>>;
/// Returns the latest candle time in the database, or `None` when no candles exist.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// let latest = db.ohlc_candles().get_latest_candle_time().await?;
/// # Ok(())
/// # }
/// ```
async fn get_latest_candle_time(&self) -> Result<Option<DateTime<Utc>>>;
/// Returns stable candle gaps as `(from_time, gap_time)` pairs ordered by `gap_time` ASC.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// let gaps = db.ohlc_candles().get_gaps().await?;
/// # Ok(())
/// # }
/// ```
async fn get_gaps(&self) -> Result<Vec<(DateTime<Utc>, DateTime<Utc>)>>;
}
#[async_trait]
pub(crate) trait OhlcCandlesRepository: OhlcCandlesRepositoryRead {
/// Adds OHLC candles to the database, distinguishing between stable and unstable candles.
async fn add_candles(
&self,
before_candle_time: Option<DateTime<Utc>>,
new_candles: &[OhlcCandle],
) -> Result<()>;
async fn remove_gap_flag(&self, time: DateTime<Utc>) -> Result<()>;
/// Finds unflagged gaps in the candle history and marks surrounding candles as unstable
/// so they can be re-fetched from the API.
async fn flag_missing_candles(&self, range: Duration) -> Result<()>;
}
#[async_trait]
/// Read-only funding settlements repository API.
pub trait FundingSettlementsRepositoryRead: Send + Sync {
/// Retrieves funding settlements within the specified time range, ordered by time ASC.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// use chrono::{Duration, Utc};
///
/// let to = Utc::now();
/// let from = to - Duration::days(30);
///
/// let settlements = db.funding_settlements().get_settlements(from, to).await?;
/// # Ok(())
/// # }
/// ```
async fn get_settlements(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> Result<Vec<FundingSettlementRow>>;
/// Returns the earliest settlement time in the database, or `None` when no settlements exist.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// let earliest = db
/// .funding_settlements()
/// .get_earliest_settlement_time()
/// .await?;
/// # Ok(())
/// # }
/// ```
async fn get_earliest_settlement_time(&self) -> Result<Option<DateTime<Utc>>>;
/// Returns the latest settlement time in the database, or `None` when no settlements exist.
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// let latest = db
/// .funding_settlements()
/// .get_latest_settlement_time()
/// .await?;
/// # Ok(())
/// # }
/// ```
async fn get_latest_settlement_time(&self) -> Result<Option<DateTime<Utc>>>;
/// Returns missing settlement times on the funding settlement grid, ordered by time ASC.
///
/// Handles all three LNM funding settlement grid phases internally:
/// + Phase A ({08} UTC, 24h)
/// + Phase B ({04, 12, 20} UTC, 8h)
/// + Phase C ({00, 08, 16} UTC, 8h)
///
/// ```rust,no_run
/// # async fn example(db: &quantoxide::Database) -> Result<(), Box<dyn std::error::Error>> {
/// use chrono::{Duration, Utc};
///
/// let to = Utc::now();
/// let from = to - Duration::days(30);
///
/// let missing = db
/// .funding_settlements()
/// .get_missing_settlement_times(from, to)
/// .await?;
/// # Ok(())
/// # }
/// ```
async fn get_missing_settlement_times(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> Result<Vec<DateTime<Utc>>>;
}
#[async_trait]
pub(crate) trait FundingSettlementsRepository: FundingSettlementsRepositoryRead {
/// Adds multiple funding settlements to the database. Idempotent.
async fn add_settlements(&self, settlements: &[FundingSettlement]) -> Result<()>;
}