-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathclient.rs
More file actions
339 lines (285 loc) · 10 KB
/
client.rs
File metadata and controls
339 lines (285 loc) · 10 KB
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
use crate::auth::{AuthManager, MemoryTokenStore, TokenStore};
use crate::config::WebullConfig;
use crate::endpoints::{
account::AccountEndpoints, market_data::MarketDataEndpoints, orders::OrderEndpoints,
watchlists::WatchlistEndpoints,
};
use crate::error::{WebullError, WebullResult};
use crate::streaming::client::WebSocketClient;
use crate::utils::credentials::{CredentialStore, MemoryCredentialStore};
use std::sync::Arc;
use std::time::Duration;
use uuid::Uuid;
/// Builder for creating a WebullClient.
pub struct WebullClientBuilder {
api_key: Option<String>,
api_secret: Option<String>,
device_id: Option<String>,
timeout: Duration,
base_url: String,
paper_trading: bool,
token_store: Option<Box<dyn TokenStore>>,
credential_store: Option<Box<dyn CredentialStore>>,
}
impl WebullClientBuilder {
/// Create a new builder with default values.
pub fn new() -> Self {
Self {
api_key: None,
api_secret: None,
device_id: None,
timeout: Duration::from_secs(30),
base_url: "https://api.webull.com".to_string(),
paper_trading: false,
token_store: None,
credential_store: None,
}
}
/// Set the API key.
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
/// Set the API secret.
pub fn with_api_secret(mut self, api_secret: impl Into<String>) -> Self {
self.api_secret = Some(api_secret.into());
self
}
/// Set the device ID.
pub fn with_device_id(mut self, device_id: impl Into<String>) -> Self {
self.device_id = Some(device_id.into());
self
}
/// Set the timeout for API requests.
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
/// Set a custom base URL.
pub fn with_custom_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
/// Set whether to use paper trading.
pub fn with_paper_trading(mut self, paper_trading: bool) -> Self {
self.paper_trading = paper_trading;
self
}
/// Enable paper trading.
pub fn paper_trading(mut self) -> Self {
self.paper_trading = true;
self
}
/// Set a custom token store.
pub fn with_token_store(mut self, store: impl TokenStore + 'static) -> Self {
self.token_store = Some(Box::new(store));
self
}
/// Set a custom credential store.
pub fn with_credential_store(mut self, store: impl CredentialStore + 'static) -> Self {
self.credential_store = Some(Box::new(store));
self
}
/// Build the WebullClient.
pub fn build(self) -> WebullResult<WebullClient> {
// Generate a random device ID if not provided
let device_id = self
.device_id
.unwrap_or_else(|| Uuid::new_v4().to_hyphenated().to_string());
// Create the configuration
let config = WebullConfig {
api_key: self.api_key,
api_secret: self.api_secret,
device_id: Some(device_id),
timeout: self.timeout,
base_url: self.base_url,
paper_trading: self.paper_trading,
};
// Create the HTTP client
let client = reqwest::Client::builder()
.timeout(config.timeout)
.build()
.map_err(|e| WebullError::NetworkError(e))?;
// Create the token store
let token_store = self
.token_store
.unwrap_or_else(|| Box::new(MemoryTokenStore::default()));
// Create the credential store
let credential_store = self
.credential_store
.unwrap_or_else(|| Box::new(MemoryCredentialStore::default()));
// Create the auth manager
let auth_manager = Arc::new(AuthManager::new(
config.clone(),
token_store,
client.clone(),
));
Ok(WebullClient {
inner: client,
config,
auth_manager,
credential_store: Arc::new(credential_store),
})
}
}
/// Client for interacting with the Webull API.
pub struct WebullClient {
/// HTTP client
inner: reqwest::Client,
/// Configuration
config: WebullConfig,
/// Authentication manager
auth_manager: Arc<AuthManager>,
/// Credential store
credential_store: Arc<Box<dyn CredentialStore>>,
}
impl WebullClient {
/// Create a new builder for configuring the client.
pub fn builder() -> WebullClientBuilder {
WebullClientBuilder::new()
}
/// Login to Webull.
pub async fn login(&self, username: &str, password: &str) -> WebullResult<()> {
// Create a new AuthManager with the same configuration
let mut auth_manager = AuthManager::new(
self.config.clone(),
Box::new(MemoryTokenStore::default()),
self.inner.clone(),
);
// Authenticate
let token = auth_manager.authenticate(username, password).await?;
// Store the token in the original auth_manager
let token_store = self.auth_manager.token_store.as_ref();
token_store.store_token(token)?;
// Store the credentials
let credentials = crate::auth::Credentials {
username: username.to_string(),
password: password.to_string(),
};
self.credential_store.store_credentials(credentials)?;
Ok(())
}
/// Logout from Webull.
pub async fn logout(&self) -> WebullResult<()> {
// Create a new AuthManager with the same configuration
let mut auth_manager = AuthManager::new(
self.config.clone(),
Box::new(MemoryTokenStore::default()),
self.inner.clone(),
);
// Get the current token from the original auth_manager
let token = match self.auth_manager.token_store.get_token()? {
Some(token) => token,
None => {
// No token to revoke
return Ok(());
}
};
// Store the token in the new auth_manager
auth_manager.token_store.store_token(token)?;
// Revoke the token
auth_manager.revoke_token().await?;
// Clear the token in the original auth_manager
self.auth_manager.token_store.clear_token()?;
// Clear the credentials
self.credential_store.clear_credentials()?;
Ok(())
}
/// Refresh the authentication token.
pub async fn refresh_token(&self) -> WebullResult<()> {
// Create a new AuthManager with the same configuration
let mut auth_manager = AuthManager::new(
self.config.clone(),
Box::new(MemoryTokenStore::default()),
self.inner.clone(),
);
// Get the current token from the original auth_manager
let token = match self.auth_manager.token_store.get_token()? {
Some(token) => token,
None => {
return Err(WebullError::InvalidRequest(
"No token available for refresh".to_string(),
));
}
};
// Store the token in the new auth_manager
auth_manager.token_store.store_token(token)?;
// Refresh the token
let new_token = auth_manager.refresh_token().await?;
// Store the new token in the original auth_manager
self.auth_manager.token_store.store_token(new_token)?;
Ok(())
}
/// Get account endpoints.
pub fn accounts(&self) -> AccountEndpoints {
AccountEndpoints::new(
self.inner.clone(),
self.config.base_url.clone(),
self.auth_manager.clone(),
)
}
/// Get market data endpoints.
pub fn market_data(&self) -> MarketDataEndpoints {
MarketDataEndpoints::new(
self.inner.clone(),
self.config.base_url.clone(),
self.auth_manager.clone(),
)
}
/// Get order endpoints.
pub fn orders(&self) -> OrderEndpoints {
OrderEndpoints::new(
self.inner.clone(),
self.config.base_url.clone(),
self.auth_manager.clone(),
)
}
/// Get watchlist endpoints.
pub fn watchlists(&self) -> WatchlistEndpoints {
WatchlistEndpoints::new(
self.inner.clone(),
self.config.base_url.clone(),
self.auth_manager.clone(),
)
}
/// Create a WebSocket client for streaming data.
pub fn streaming(&self) -> WebSocketClient {
let ws_base_url = self.config.base_url.clone().replace("http", "ws");
WebSocketClient::new(ws_base_url, self.auth_manager.clone())
}
/// Get the stored credentials.
pub fn get_credentials(&self) -> WebullResult<Option<crate::auth::Credentials>> {
self.credential_store.get_credentials()
}
/// Get the credential store.
pub fn credential_store(&self) -> &Arc<Box<dyn CredentialStore>> {
&self.credential_store
}
/// Check if the client is configured for paper trading.
pub fn is_paper_trading(&self) -> bool {
self.config.paper_trading
}
/// Create a new client for paper trading.
pub fn paper_trading(&self) -> WebullResult<Self> {
let mut config = self.config.clone();
config.paper_trading = true;
// Create a new client with the same settings but for paper trading
let client = reqwest::ClientBuilder::new()
.timeout(config.timeout)
.build()
.map_err(|e| WebullError::NetworkError(e))?;
let token_store = Box::new(MemoryTokenStore::default());
let credential_store = Box::new(MemoryCredentialStore::default());
let auth_manager = Arc::new(AuthManager::new(
config.clone(),
token_store,
client.clone(),
));
Ok(Self {
inner: client,
config,
auth_manager,
credential_store: Arc::new(credential_store),
})
}
}