Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions pocketoptionapi_async/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def __init__(
self._orders: Dict[str, OrderResult] = {}
self._active_orders: Dict[str, OrderResult] = {}
self._order_results: Dict[str, OrderResult] = {}
self._server_id_to_request_id: Dict[str, str] = {} # Maps server deal IDs to client request IDs
self._candles_cache: Dict[str, List[Candle]] = {}
self._server_time: Optional[ServerTime] = None
self._event_callbacks: Dict[str, List[Callable]] = defaultdict(list)
Expand Down Expand Up @@ -1035,6 +1036,14 @@ async def _on_json_data(self, data: Dict[str, Any]) -> None:
if "requestId" in data and "asset" in data and "amount" in data:
request_id = str(data["requestId"])

# Store mapping from server ID to request ID if server ID is present and valid
if "id" in data and data["id"]:
server_id = str(data["id"])
if server_id: # Ensure string is not empty
self._server_id_to_request_id[server_id] = request_id
if self.enable_logging:
logger.debug(f"Mapped server ID {server_id} to request ID {request_id}")
Comment on lines +1042 to +1045
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant check: server_id is guaranteed to be non-empty at this point. Line 1040 already checks data['id'] is truthy before converting to string on line 1041. An empty string input would have failed the line 1040 check. This redundant check on line 1042 adds unnecessary complexity.

Suggested change
if server_id: # Ensure string is not empty
self._server_id_to_request_id[server_id] = request_id
if self.enable_logging:
logger.debug(f"Mapped server ID {server_id} to request ID {request_id}")
self._server_id_to_request_id[server_id] = request_id
if self.enable_logging:
logger.debug(f"Mapped server ID {server_id} to request ID {request_id}")

Copilot uses AI. Check for mistakes.

# If this is a new order, add it to tracking
if (
request_id not in self._active_orders
Expand Down Expand Up @@ -1069,10 +1078,17 @@ async def _on_json_data(self, data: Dict[str, Any]) -> None:
elif "deals" in data and isinstance(data["deals"], list):
for deal in data["deals"]:
if isinstance(deal, dict) and "id" in deal:
order_id = str(deal["id"])

if order_id in self._active_orders:
active_order = self._active_orders[order_id]
server_deal_id = str(deal["id"])

# Try to find the request_id for this server deal ID
request_id = self._server_id_to_request_id.get(server_deal_id)

# If we have a mapping, use request_id to find the order
# Otherwise, fall back to trying server_deal_id directly
Comment on lines +1086 to +1087
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment on line 1086-1087 could be clearer. Consider: 'Use mapped request_id if available; otherwise fall back to server_deal_id for backward compatibility' to explicitly mention backward compatibility, which is the key design decision here.

Suggested change
# If we have a mapping, use request_id to find the order
# Otherwise, fall back to trying server_deal_id directly
# Use mapped request_id if available; otherwise fall back to server_deal_id for backward compatibility

Copilot uses AI. Check for mistakes.
lookup_id = request_id or server_deal_id

if lookup_id in self._active_orders:
active_order = self._active_orders[lookup_id]
profit = float(deal.get("profit", 0))

# Determine status
Expand All @@ -1096,13 +1112,17 @@ async def _on_json_data(self, data: Dict[str, Any]) -> None:
payout=deal.get("payout"),
)

# Move from active to completed
self._order_results[order_id] = result
del self._active_orders[order_id]
# Move from active to completed - use the original order_id (request_id)
self._order_results[active_order.order_id] = result
del self._active_orders[lookup_id]

# Clean up the server ID mapping
if request_id and server_deal_id in self._server_id_to_request_id:
del self._server_id_to_request_id[server_deal_id]

if self.enable_logging:
logger.success(
f" Order {order_id} completed via JSON data: {status.value} - Profit: ${profit:.2f}"
f" Order {active_order.order_id} completed via JSON data: {status.value} - Profit: ${profit:.2f}"
)
await self._emit_event("order_closed", result)

Expand Down
239 changes: 239 additions & 0 deletions tests/test_check_win.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
"""
Test script to verify the check_win functionality
Tests that the server ID to request ID mapping works correctly
"""

import asyncio
from datetime import datetime, timedelta
from pocketoptionapi_async import AsyncPocketOptionClient, OrderDirection
from pocketoptionapi_async.models import OrderResult, OrderStatus


async def test_check_win_id_mapping():
"""Test that server deal IDs are properly mapped to client request IDs"""

# Create a client with a dummy SSID (we won't connect, just test internal logic)
dummy_ssid = r'42["auth",{"session":"test_session","isDemo":1,"uid":12345,"platform":1}]'
client = AsyncPocketOptionClient(ssid=dummy_ssid, is_demo=True, enable_logging=False)

print("Testing check_win ID mapping fix")
print("=" * 50)

# Test 1: Verify the mapping dictionary exists
print("\nTest 1: Verify _server_id_to_request_id mapping exists")
assert hasattr(client, '_server_id_to_request_id'), "Client should have _server_id_to_request_id dict"
assert isinstance(client._server_id_to_request_id, dict), "Should be a dictionary"
print(" ✅ PASS: _server_id_to_request_id mapping exists")

# Test 2: Simulate order creation with server ID mapping
print("\nTest 2: Simulate order data with both requestId and server id")

client_request_id = "abc-123-def-456" # Our client-generated UUID
server_deal_id = "98765432" # Server-assigned ID

# Simulate the server response that includes both IDs
order_data = {
"requestId": client_request_id,
"id": server_deal_id,
"asset": "EURUSD_otc",
"amount": 10.0,
"command": 0, # CALL
"time": 60
}

# Call the handler that processes order data
await client._on_json_data(order_data)

# Verify the order was added to active orders with request_id
assert client_request_id in client._active_orders, "Order should be in active orders with request_id"
print(f" ✅ PASS: Order added to active orders with request_id: {client_request_id}")

# Verify the server ID mapping was created
assert server_deal_id in client._server_id_to_request_id, "Server ID should be mapped to request_id"
assert client._server_id_to_request_id[server_deal_id] == client_request_id, "Mapping should point to request_id"
print(f" ✅ PASS: Server ID {server_deal_id} mapped to request_id {client_request_id}")

# Test 3: Simulate deal completion using server's deal ID
print("\nTest 3: Simulate deal completion with server's deal ID")

deal_data = {
"deals": [
{
"id": server_deal_id, # Server uses its own ID
"profit": 8.5,
"payout": 85.0
}
]
}

# Call the handler that processes deal completion
await client._on_json_data(deal_data)

# Verify the order was moved from active to completed
assert client_request_id not in client._active_orders, "Order should be removed from active orders"
assert client_request_id in client._order_results, "Order should be in order_results with request_id"
print(f" ✅ PASS: Order moved from active to completed using request_id")

# Verify the result data is correct
result = client._order_results[client_request_id]
assert result.order_id == client_request_id, "Result order_id should match request_id"
assert result.profit == 8.5, f"Profit should be 8.5, got {result.profit}"
assert result.status == OrderStatus.WIN, f"Status should be WIN, got {result.status}"
print(f" ✅ PASS: Order result has correct profit ({result.profit}) and status ({result.status})")

# Verify the server ID mapping was cleaned up
assert server_deal_id not in client._server_id_to_request_id, "Server ID mapping should be cleaned up"
print(" ✅ PASS: Server ID mapping was cleaned up after order completion")

# Test 4: Test check_win function can find the completed order
print("\nTest 4: Verify check_win finds the completed order")

check_result = await client.check_win(client_request_id, max_wait_time=1.0)

assert check_result is not None, "check_win should return a result"
assert check_result["completed"], "Order should be completed"
assert check_result["result"] == "win", f"Result should be 'win', got {check_result['result']}"
assert check_result["profit"] == 8.5, f"Profit should be 8.5, got {check_result['profit']}"
print(f" ✅ PASS: check_win returned correct result: {check_result}")

# Test 5: Test check_order_result function
print("\nTest 5: Verify check_order_result finds the completed order")

order_result = await client.check_order_result(client_request_id)

assert order_result is not None, "check_order_result should return a result"
assert order_result.order_id == client_request_id, "Order ID should match"
assert order_result.profit == 8.5, "Profit should be correct"
print(f" ✅ PASS: check_order_result returned correct result")

print("\n" + "=" * 50)
print("🎉 ALL TESTS PASSED! check_win ID mapping fix is working!")

return True


async def test_check_win_loss_scenario():
"""Test that loss orders are correctly handled"""

dummy_ssid = r'42["auth",{"session":"test_session","isDemo":1,"uid":12345,"platform":1}]'
client = AsyncPocketOptionClient(ssid=dummy_ssid, is_demo=True, enable_logging=False)

print("\nTesting check_win with loss scenario")
print("=" * 50)

client_request_id = "loss-order-123"
server_deal_id = "88888888"

# Create order
order_data = {
"requestId": client_request_id,
"id": server_deal_id,
"asset": "EURUSD_otc",
"amount": 10.0,
"command": 1, # PUT
"time": 60
}
await client._on_json_data(order_data)

# Complete order with a loss
deal_data = {
"deals": [
{
"id": server_deal_id,
"profit": -10.0, # Lost the trade
"payout": 0
}
]
}
await client._on_json_data(deal_data)

# Verify check_win returns loss
check_result = await client.check_win(client_request_id, max_wait_time=1.0)

assert check_result["result"] == "loss", f"Result should be 'loss', got {check_result['result']}"
assert check_result["profit"] == -10.0, f"Profit should be -10.0, got {check_result['profit']}"
print(f" ✅ PASS: check_win correctly identifies loss: {check_result}")

return True


async def test_check_win_fallback_without_mapping():
"""Test that check_win still works if server ID happens to match request ID (backward compatibility)"""

dummy_ssid = r'42["auth",{"session":"test_session","isDemo":1,"uid":12345,"platform":1}]'
client = AsyncPocketOptionClient(ssid=dummy_ssid, is_demo=True, enable_logging=False)

print("\nTesting check_win fallback (no mapping scenario)")
print("=" * 50)

# Simulate a case where the order was added directly with the server ID
# (e.g., if server returns request matching what we sent)
order_id = "direct-order-789"

# Directly add to active orders (simulating order placement)
order_result = OrderResult(
order_id=order_id,
asset="GBPUSD_otc",
amount=5.0,
direction=OrderDirection.CALL,
duration=60,
status=OrderStatus.ACTIVE,
placed_at=datetime.now(),
expires_at=datetime.now() + timedelta(seconds=60),
)
client._active_orders[order_id] = order_result

# Complete the order using the same ID (no mapping needed)
deal_data = {
"deals": [
{
"id": order_id, # Using the same ID
"profit": 4.25,
"payout": 85.0
}
]
}
await client._on_json_data(deal_data)

# Verify order was completed
check_result = await client.check_win(order_id, max_wait_time=1.0)

assert check_result["result"] == "win", f"Result should be 'win', got {check_result['result']}"
print(f" ✅ PASS: Fallback without mapping works correctly")

return True


async def run_all_tests():
"""Run all check_win tests"""
all_passed = True

try:
all_passed = await test_check_win_id_mapping() and all_passed
except Exception as e:
print(f"❌ FAIL: test_check_win_id_mapping - {e}")
all_passed = False

try:
all_passed = await test_check_win_loss_scenario() and all_passed
except Exception as e:
print(f"❌ FAIL: test_check_win_loss_scenario - {e}")
all_passed = False

try:
all_passed = await test_check_win_fallback_without_mapping() and all_passed
Comment on lines +212 to +224
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect short-circuit evaluation order. The current order await test() and all_passed will skip tests if all_passed is already False due to short-circuiting. The tests should run first and then AND with all_passed. Change to all_passed = all_passed and await test() to ensure all tests execute.

Suggested change
all_passed = await test_check_win_id_mapping() and all_passed
except Exception as e:
print(f"❌ FAIL: test_check_win_id_mapping - {e}")
all_passed = False
try:
all_passed = await test_check_win_loss_scenario() and all_passed
except Exception as e:
print(f"❌ FAIL: test_check_win_loss_scenario - {e}")
all_passed = False
try:
all_passed = await test_check_win_fallback_without_mapping() and all_passed
all_passed = all_passed and await test_check_win_id_mapping()
except Exception as e:
print(f"❌ FAIL: test_check_win_id_mapping - {e}")
all_passed = False
try:
all_passed = all_passed and await test_check_win_loss_scenario()
except Exception as e:
print(f"❌ FAIL: test_check_win_loss_scenario - {e}")
all_passed = False
try:
all_passed = all_passed and await test_check_win_fallback_without_mapping()

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +224
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect short-circuit evaluation order. The current order await test() and all_passed will skip tests if all_passed is already False due to short-circuiting. The tests should run first and then AND with all_passed. Change to all_passed = all_passed and await test() to ensure all tests execute.

Suggested change
all_passed = await test_check_win_id_mapping() and all_passed
except Exception as e:
print(f"❌ FAIL: test_check_win_id_mapping - {e}")
all_passed = False
try:
all_passed = await test_check_win_loss_scenario() and all_passed
except Exception as e:
print(f"❌ FAIL: test_check_win_loss_scenario - {e}")
all_passed = False
try:
all_passed = await test_check_win_fallback_without_mapping() and all_passed
all_passed = all_passed and await test_check_win_id_mapping()
except Exception as e:
print(f"❌ FAIL: test_check_win_id_mapping - {e}")
all_passed = False
try:
all_passed = all_passed and await test_check_win_loss_scenario()
except Exception as e:
print(f"❌ FAIL: test_check_win_loss_scenario - {e}")
all_passed = False
try:
all_passed = all_passed and await test_check_win_fallback_without_mapping()

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +224
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect short-circuit evaluation order. The current order await test() and all_passed will skip tests if all_passed is already False due to short-circuiting. The tests should run first and then AND with all_passed. Change to all_passed = all_passed and await test() to ensure all tests execute.

Suggested change
all_passed = await test_check_win_id_mapping() and all_passed
except Exception as e:
print(f"❌ FAIL: test_check_win_id_mapping - {e}")
all_passed = False
try:
all_passed = await test_check_win_loss_scenario() and all_passed
except Exception as e:
print(f"❌ FAIL: test_check_win_loss_scenario - {e}")
all_passed = False
try:
all_passed = await test_check_win_fallback_without_mapping() and all_passed
all_passed = all_passed and await test_check_win_id_mapping()
except Exception as e:
print(f"❌ FAIL: test_check_win_id_mapping - {e}")
all_passed = False
try:
all_passed = all_passed and await test_check_win_loss_scenario()
except Exception as e:
print(f"❌ FAIL: test_check_win_loss_scenario - {e}")
all_passed = False
try:
all_passed = all_passed and await test_check_win_fallback_without_mapping()

Copilot uses AI. Check for mistakes.
except Exception as e:
print(f"❌ FAIL: test_check_win_fallback_without_mapping - {e}")
all_passed = False

print("\n" + "=" * 50)
if all_passed:
print("🎉 ALL CHECK_WIN TESTS PASSED!")
else:
print("❌ SOME TESTS FAILED")

return all_passed


if __name__ == "__main__":
asyncio.run(run_all_tests())
Loading