fivetwenty 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fivetwenty/__init__.py +62 -0
- fivetwenty/_internal/__init__.py +1 -0
- fivetwenty/_internal/environment.py +17 -0
- fivetwenty/_internal/utils.py +113 -0
- fivetwenty/client.py +567 -0
- fivetwenty/configuration.py +215 -0
- fivetwenty/endpoints/__init__.py +1 -0
- fivetwenty/endpoints/accounts.py +183 -0
- fivetwenty/endpoints/instruments.py +106 -0
- fivetwenty/endpoints/orders.py +656 -0
- fivetwenty/endpoints/positions.py +164 -0
- fivetwenty/endpoints/pricing.py +360 -0
- fivetwenty/endpoints/trades.py +248 -0
- fivetwenty/endpoints/transactions.py +271 -0
- fivetwenty/exceptions.py +198 -0
- fivetwenty/models/__init__.py +210 -0
- fivetwenty/models/accounts.py +263 -0
- fivetwenty/models/base.py +58 -0
- fivetwenty/models/enums.py +494 -0
- fivetwenty/models/error_codes.py +225 -0
- fivetwenty/models/error_details.py +290 -0
- fivetwenty/models/instruments.py +100 -0
- fivetwenty/models/orders.py +620 -0
- fivetwenty/models/positions.py +69 -0
- fivetwenty/models/pricing.py +140 -0
- fivetwenty/models/streaming.py +42 -0
- fivetwenty/models/trades.py +99 -0
- fivetwenty/models/transactions.py +540 -0
- fivetwenty/py.typed +1 -0
- fivetwenty-0.1.0.dist-info/METADATA +309 -0
- fivetwenty-0.1.0.dist-info/RECORD +34 -0
- fivetwenty-0.1.0.dist-info/WHEEL +5 -0
- fivetwenty-0.1.0.dist-info/licenses/LICENSE +21 -0
- fivetwenty-0.1.0.dist-info/top_level.txt +1 -0
fivetwenty/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OANDA REST API v20 Python SDK
|
|
3
|
+
|
|
4
|
+
A simple, elegant Python client for OANDA's REST API v20.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from fivetwenty import Client, AsyncClient, Environment, AccountConfig
|
|
8
|
+
|
|
9
|
+
# Method 1: Direct parameters
|
|
10
|
+
async with AsyncClient(token="your-token", environment=Environment.PRACTICE) as client:
|
|
11
|
+
accounts = await client.accounts.list()
|
|
12
|
+
|
|
13
|
+
# Method 2: Configuration object
|
|
14
|
+
config = AccountConfig(
|
|
15
|
+
token="your-token",
|
|
16
|
+
account_id="your-account-id",
|
|
17
|
+
environment=Environment.PRACTICE,
|
|
18
|
+
alias="my_account"
|
|
19
|
+
)
|
|
20
|
+
async with AsyncClient(config=config) as client:
|
|
21
|
+
accounts = await client.accounts.list()
|
|
22
|
+
|
|
23
|
+
# Method 3: Environment variables (fallback)
|
|
24
|
+
# Set FIVETWENTY_OANDA_TOKEN, FIVETWENTY_OANDA_ACCOUNT, etc.
|
|
25
|
+
async with AsyncClient() as client:
|
|
26
|
+
accounts = await client.accounts.list()
|
|
27
|
+
|
|
28
|
+
# Sync wrapper (same patterns)
|
|
29
|
+
with Client(token="your-token") as client:
|
|
30
|
+
accounts = client.accounts.list()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
__version__ = "20.1.0"
|
|
34
|
+
|
|
35
|
+
from ._internal.environment import Environment
|
|
36
|
+
from .client import AsyncClient, Client
|
|
37
|
+
from .configuration import AccountConfig, AccountConfigLoader, ConfigValidator
|
|
38
|
+
from .exceptions import FiveTwentyError, StreamStall
|
|
39
|
+
from .models import ErrorCategory, ErrorDetails, ErrorSeverity, FiveTwentyErrorCode, ValidationViolation
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Configuration
|
|
43
|
+
"AccountConfig",
|
|
44
|
+
"AccountConfigLoader",
|
|
45
|
+
# Main clients
|
|
46
|
+
"AsyncClient",
|
|
47
|
+
"Client",
|
|
48
|
+
"ConfigValidator",
|
|
49
|
+
# Enums
|
|
50
|
+
"Environment",
|
|
51
|
+
# Error handling
|
|
52
|
+
"ErrorCategory",
|
|
53
|
+
"ErrorDetails",
|
|
54
|
+
"ErrorSeverity",
|
|
55
|
+
# Exceptions
|
|
56
|
+
"FiveTwentyError",
|
|
57
|
+
"FiveTwentyErrorCode",
|
|
58
|
+
"StreamStall",
|
|
59
|
+
"ValidationViolation",
|
|
60
|
+
# Version
|
|
61
|
+
"__version__",
|
|
62
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal utilities and helpers."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Environment configuration."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Environment(Enum):
|
|
7
|
+
"""OANDA API environments."""
|
|
8
|
+
|
|
9
|
+
PRACTICE = "practice"
|
|
10
|
+
LIVE = "live"
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def base_url(self) -> str:
|
|
14
|
+
"""Get the base URL for this environment."""
|
|
15
|
+
if self == Environment.LIVE:
|
|
16
|
+
return "https://api-fxtrade.oanda.com/v3"
|
|
17
|
+
return "https://api-fxpractice.oanda.com/v3"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Internal utility functions."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import sys
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from time import monotonic
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .. import __version__
|
|
13
|
+
except ImportError:
|
|
14
|
+
# Handle cases where these aren't available during early setup
|
|
15
|
+
httpx = None # type: ignore
|
|
16
|
+
__version__ = "20.1.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def backoff_with_jitter(attempt: int, base: float = 0.5, cap: float = 8.0) -> float:
|
|
20
|
+
"""
|
|
21
|
+
Calculate exponential backoff with jitter.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
attempt: The attempt number (0-indexed)
|
|
25
|
+
base: Base delay in seconds
|
|
26
|
+
cap: Maximum delay in seconds
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Delay in seconds with jitter applied
|
|
30
|
+
"""
|
|
31
|
+
delay = min(cap, base * (2**attempt))
|
|
32
|
+
# Add jitter: 50% to 100% of calculated delay
|
|
33
|
+
jitter = random.random() / 2.0 # 0 to 0.5
|
|
34
|
+
result: float = delay * (0.5 + jitter)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_user_agent() -> str:
|
|
39
|
+
"""Build User-Agent string with version info."""
|
|
40
|
+
import os
|
|
41
|
+
|
|
42
|
+
# Base user agent
|
|
43
|
+
base = f"fivetwenty/{__version__} (python-{sys.version_info[0]}.{sys.version_info[1]}"
|
|
44
|
+
if httpx:
|
|
45
|
+
base += f"; httpx-{httpx.__version__}"
|
|
46
|
+
base += ")"
|
|
47
|
+
|
|
48
|
+
# Optional extra from environment
|
|
49
|
+
extra = os.environ.get("FIVETWENTY_USER_AGENT_EXTRA")
|
|
50
|
+
return f"{base} {extra}" if extra else base
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def stringify_decimals(obj: Any) -> Any:
|
|
54
|
+
"""
|
|
55
|
+
Recursively convert all Decimals to strings in a data structure.
|
|
56
|
+
|
|
57
|
+
This prevents future misses when new Decimal fields appear in the API.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
obj: The object to process
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The object with all Decimals converted to strings
|
|
64
|
+
"""
|
|
65
|
+
if isinstance(obj, Decimal):
|
|
66
|
+
return format(obj, "f")
|
|
67
|
+
if isinstance(obj, list):
|
|
68
|
+
return [stringify_decimals(item) for item in obj]
|
|
69
|
+
if isinstance(obj, dict):
|
|
70
|
+
return {key: stringify_decimals(value) for key, value in obj.items()}
|
|
71
|
+
return obj
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def quantize_price(precision: int, value: Decimal) -> Decimal:
|
|
75
|
+
"""
|
|
76
|
+
Round a price to the specified precision.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
precision: Number of decimal places
|
|
80
|
+
value: The price to quantize
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The quantized price
|
|
84
|
+
"""
|
|
85
|
+
quantizer = Decimal(10) ** (-precision)
|
|
86
|
+
return value.quantize(quantizer)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class MonotonicTimeout:
|
|
90
|
+
"""Helper for timeout tracking using monotonic time."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, timeout_seconds: float):
|
|
93
|
+
self.timeout_seconds = timeout_seconds
|
|
94
|
+
self.start_time = monotonic()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def elapsed(self) -> float:
|
|
98
|
+
"""Get elapsed time in seconds."""
|
|
99
|
+
return monotonic() - self.start_time
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def remaining(self) -> float:
|
|
103
|
+
"""Get remaining time in seconds (may be negative if expired)."""
|
|
104
|
+
return self.timeout_seconds - self.elapsed
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def expired(self) -> bool:
|
|
108
|
+
"""Check if timeout has expired."""
|
|
109
|
+
return self.elapsed >= self.timeout_seconds
|
|
110
|
+
|
|
111
|
+
def sleep_remaining(self, max_sleep: float = 1.0) -> float:
|
|
112
|
+
"""Get sleep time, capped at max_sleep."""
|
|
113
|
+
return min(max_sleep, max(0, self.remaining))
|