Skip to content

Commit 755ed20

Browse files
authored
Merge pull request #66 from sixtysixx/master
small fixes
2 parents 0e8301c + 62d0c8f commit 755ed20

5 files changed

Lines changed: 111 additions & 5 deletions

File tree

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
strategy:
5656
fail-fast: false
5757
matrix:
58-
target: [x86, x86]
58+
target: [x64, x86]
5959
steps:
6060
- uses: actions/checkout@v4
6161
- uses: actions/setup-python@v5

BinaryOptionsToolsV2/python/BinaryOptionsToolsV2/pocketoption/asynchronous.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ async def buy(self, asset: str, amount: float, time: int, check_win: bool = Fals
257257
asset (str): Trading asset (e.g., "EURUSD_otc", "EURUSD")
258258
amount (float): Trade amount in account currency
259259
time (int): Expiry time in seconds (e.g., 60 for 1 minute)
260-
check_win (bool): If True, waits for trade result. Defaults to True.
260+
check_win (bool): If True, waits for trade result. Defaults to False.
261261
262262
Returns:
263263
Tuple[str, Dict]: Tuple containing (trade_id, trade_details)
@@ -289,7 +289,7 @@ async def sell(self, asset: str, amount: float, time: int, check_win: bool = Fal
289289
asset (str): Trading asset (e.g., "EURUSD_otc", "EURUSD")
290290
amount (float): Trade amount in account currency
291291
time (int): Expiry time in seconds (e.g., 60 for 1 minute)
292-
check_win (bool): If True, waits for trade result. Defaults to True.
292+
check_win (bool): If True, waits for trade result. Defaults to False.
293293
294294
Returns:
295295
Tuple[str, Dict]: Tuple containing (trade_id, trade_details)

crates/binary_options_tools/src/pocketoption/modules/trades.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ pub struct TradesApiModule {
123123
pending_orders: HashMap<Uuid, PendingOrderTracker>,
124124
// Secondary index for matching failures (which lack UUID)
125125
// Map of (Asset, Amount) -> Queue of UUIDs (FIFO)
126+
/// A heuristic-based mapping for correlating server-side failures to client requests.
127+
///
128+
/// Since the PocketOption protocol does not return a `request_id` for `failopenOrder`
129+
/// messages, we maintain a FIFO queue of pending requests per (Asset, Amount).
130+
///
131+
/// # Warning
132+
/// This is susceptible to race conditions if multiple identical trades are
133+
/// executed simultaneously and the server responds out-of-order.
126134
failure_matching: HashMap<(String, Decimal), VecDeque<Uuid>>,
127135
}
128136

@@ -206,7 +214,8 @@ impl ApiModule<State> for TradesApiModule {
206214
if let Ok(res) = serde_json::from_str::<ServerResponse>(text) {
207215
Ok(res)
208216
} else if let Some(start) = text.find('[') {
209-
// Try parsing as a 1-step Socket.IO message: 42["successopenOrder", {...}]
217+
// Resilient Socket.IO parsing: extract the JSON array content
218+
// Handles prefixes like "42", "451-", etc.
210219
match serde_json::from_str::<serde_json::Value>(&text[start..]) {
211220
Ok(serde_json::Value::Array(arr)) => {
212221
if arr.len() >= 2 && (arr[0] == "successopenOrder" || arr[0] == "failopenOrder") {
@@ -241,6 +250,9 @@ impl ApiModule<State> for TradesApiModule {
241250
let key = (tracker.asset, tracker.amount);
242251
if let Some(queue) = self.failure_matching.get_mut(&key) {
243252
queue.retain(|&id| id != req_id);
253+
if queue.is_empty() {
254+
self.failure_matching.remove(&key);
255+
}
244256
}
245257
} else {
246258
warn!(target: "TradesApiModule", "Received success for unknown request ID: {}", req_id);
@@ -250,7 +262,11 @@ impl ApiModule<State> for TradesApiModule {
250262
let key = (fail.asset.clone(), fail.amount);
251263

252264
let found_req_id = if let Some(queue) = self.failure_matching.get_mut(&key) {
253-
queue.pop_front()
265+
let id = queue.pop_front();
266+
if queue.is_empty() {
267+
self.failure_matching.remove(&key);
268+
}
269+
id
254270
} else {
255271
None
256272
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use super::common::*;
2+
use crate::pocketoption::types::Action;
3+
use rust_decimal_macros::dec;
4+
use std::sync::Arc;
5+
use tokio::time::{timeout, Duration};
6+
use binary_options_tools_core_pre::reimports::Message;
7+
use uuid::Uuid;
8+
9+
#[tokio::test]
10+
async fn test_concurrent_identical_trades_hammer() {
11+
let setup = create_test_setup().await;
12+
let asset = "EURUSD_otc".to_string();
13+
let amount = dec!(10.0);
14+
let time = 60;
15+
16+
// Place 5 identical trades
17+
let mut handles = Vec::new();
18+
for _ in 0..5 {
19+
let h = setup.handle.clone();
20+
let a = asset.clone();
21+
handles.push(tokio::spawn(async move {
22+
h.trade(a, Action::Call, amount, time).await
23+
}));
24+
}
25+
26+
// Wait for all 5 to be sent to WS
27+
let mut req_ids = Vec::new();
28+
for _ in 0..5 {
29+
if let Ok(Message::Text(text)) = timeout(Duration::from_secs(1), setup.ws_rx.recv()).await.unwrap() {
30+
// 42["openOrder",{"amount":"10.0","asset":"EURUSD_otc","action":"call","isDemo":0,"requestId":"...","optionType":100,"time":60}]
31+
let start = text.find('{').unwrap();
32+
let end = text.rfind('}').unwrap();
33+
let json_str = &text[start..end+1];
34+
let v: serde_json::Value = serde_json::from_str(json_str).unwrap();
35+
let req_id = Uuid::parse_str(v["requestId"].as_str().unwrap()).unwrap();
36+
req_ids.push(req_id);
37+
}
38+
}
39+
40+
assert_eq!(req_ids.len(), 5);
41+
42+
// Now simulate responses from server.
43+
// Server doesn't send requestId for failures, it uses asset/amount matching.
44+
// For successes, it DOES send requestId.
45+
46+
// 1. Success for 2nd request
47+
let deal2 = create_test_deal(req_ids[1], &asset);
48+
let resp2 = format!(r#"42["successopenOrder",{}]"#, serde_json::to_string(&deal2).unwrap());
49+
setup.msg_tx.send(Arc::new(Message::Text(resp2.into()))).await.unwrap();
50+
51+
// 2. Failure (will be matched to 1st request because it's the oldest in failure_matching queue)
52+
let fail_data = create_test_fail(&asset, amount);
53+
let resp_fail = format!(r#"42["failopenOrder",{}]"#, serde_json::to_string(&fail_data).unwrap());
54+
setup.msg_tx.send(Arc::new(Message::Text(resp_fail.into()))).await.unwrap();
55+
56+
// 3. Success for 4th request
57+
let deal4 = create_test_deal(req_ids[3], &asset);
58+
let resp4 = format!(r#"42["successopenOrder",{}]"#, serde_json::to_string(&deal4).unwrap());
59+
setup.msg_tx.send(Arc::new(Message::Text(resp4.into()))).await.unwrap();
60+
61+
// Now collect results from handles
62+
// handle 2 (req_ids[1]) should be Success
63+
// handle 1 (req_ids[0]) should be Fail
64+
// handle 4 (req_ids[3]) should be Success
65+
// handle 3 and 5 are still pending (we can ignore or fail them later)
66+
67+
// Note: Since we spawned them, we don't easily know which JoinHandle corresponds to which req_id
68+
// unless we mapped them. But the TradesApiModule routes them based on req_id for success.
69+
// For failure, it routes to the OLDEST one in its internal queue.
70+
71+
// Let's wait for the ones we expect to finish
72+
// Since they are in tokio::spawn, we just await them.
73+
// Actually, req_ids[1] was deal2, so handles[1] should return Ok(deal2).
74+
// req_ids[0] was matched to fail_data, so handles[0] should return Err(FailOpenOrder).
75+
// req_ids[3] was deal4, so handles[3] should return Ok(deal4).
76+
77+
let res1 = handles.remove(0).await.unwrap();
78+
let res2 = handles.remove(0).await.unwrap(); // handles[1] is now at index 0
79+
let _res3 = handles.remove(0); // skip handles[2]
80+
let res4 = handles.remove(0).await.unwrap(); // handles[3] is now at index 0
81+
82+
assert!(res1.is_err());
83+
assert!(res2.is_ok());
84+
assert!(res4.is_ok());
85+
86+
if let Err(e) = res1 {
87+
assert!(format!("{:?}", e).contains("Insufficient balance"));
88+
}
89+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod common;
2+
pub mod concurrency;

0 commit comments

Comments
 (0)