retrade 0.1.1__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.
- retrade/__init__.py +66 -0
- retrade/adapter.py +99 -0
- retrade/cli.py +55 -0
- retrade/client.py +880 -0
- retrade/errors.py +46 -0
- retrade/keys.py +77 -0
- retrade/kit.py +204 -0
- retrade/model.py +450 -0
- retrade/protocol.py +378 -0
- retrade/py.typed +0 -0
- retrade/transport.py +163 -0
- retrade/webhook.py +34 -0
- retrade-0.1.1.dist-info/METADATA +301 -0
- retrade-0.1.1.dist-info/RECORD +17 -0
- retrade-0.1.1.dist-info/WHEEL +4 -0
- retrade-0.1.1.dist-info/entry_points.txt +3 -0
- retrade-0.1.1.dist-info/licenses/LICENSE +21 -0
retrade/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""reTrade's executor-side SDK."""
|
|
2
|
+
|
|
3
|
+
from retrade.adapter import ApprovedOrder, BrokerAdapter, GenericAdapter
|
|
4
|
+
from retrade.client import DEFAULT_BASE_URL, Client, LogRecord, Placement, Result
|
|
5
|
+
from retrade.errors import (
|
|
6
|
+
BrokerRejected,
|
|
7
|
+
ConfigError,
|
|
8
|
+
InvalidInput,
|
|
9
|
+
RequestRefused,
|
|
10
|
+
ReTradeError,
|
|
11
|
+
Unreachable,
|
|
12
|
+
VerificationFailed,
|
|
13
|
+
)
|
|
14
|
+
from retrade.keys import KeyFileError, generate_key, load_key
|
|
15
|
+
from retrade.model import (
|
|
16
|
+
Adjustment,
|
|
17
|
+
AdjustmentKind,
|
|
18
|
+
Fx,
|
|
19
|
+
Intent,
|
|
20
|
+
OpenOrder,
|
|
21
|
+
Order,
|
|
22
|
+
OrderType,
|
|
23
|
+
Position,
|
|
24
|
+
Quote,
|
|
25
|
+
Side,
|
|
26
|
+
Snapshot,
|
|
27
|
+
Tif,
|
|
28
|
+
)
|
|
29
|
+
from retrade.protocol import Mode, Outcome
|
|
30
|
+
from retrade.webhook import verify_webhook
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"DEFAULT_BASE_URL",
|
|
34
|
+
"Adjustment",
|
|
35
|
+
"AdjustmentKind",
|
|
36
|
+
"ApprovedOrder",
|
|
37
|
+
"BrokerAdapter",
|
|
38
|
+
"BrokerRejected",
|
|
39
|
+
"Client",
|
|
40
|
+
"ConfigError",
|
|
41
|
+
"Fx",
|
|
42
|
+
"GenericAdapter",
|
|
43
|
+
"Intent",
|
|
44
|
+
"InvalidInput",
|
|
45
|
+
"KeyFileError",
|
|
46
|
+
"LogRecord",
|
|
47
|
+
"Mode",
|
|
48
|
+
"OpenOrder",
|
|
49
|
+
"Order",
|
|
50
|
+
"OrderType",
|
|
51
|
+
"Outcome",
|
|
52
|
+
"Placement",
|
|
53
|
+
"Position",
|
|
54
|
+
"Quote",
|
|
55
|
+
"ReTradeError",
|
|
56
|
+
"RequestRefused",
|
|
57
|
+
"Result",
|
|
58
|
+
"Side",
|
|
59
|
+
"Snapshot",
|
|
60
|
+
"Tif",
|
|
61
|
+
"Unreachable",
|
|
62
|
+
"VerificationFailed",
|
|
63
|
+
"generate_key",
|
|
64
|
+
"load_key",
|
|
65
|
+
"verify_webhook",
|
|
66
|
+
]
|
retrade/adapter.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""The broker adapter: the SDK's only contact with a broker (DESIGN 5.1). It runs in the client's executor, with the
|
|
2
|
+
client's own broker connection. reTrade never connects to a broker."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
|
|
12
|
+
from retrade.errors import InvalidInput
|
|
13
|
+
from retrade.model import OrderType, Quote, Side, Snapshot, Tif
|
|
14
|
+
|
|
15
|
+
DEFAULT_G_SECONDS = 60
|
|
16
|
+
MAX_G_SECONDS = 3600 # reTrade's account register caps G at one hour (API.md 14.3)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class ApprovedOrder:
|
|
21
|
+
"""The order exactly as reTrade approved it. The SDK builds it from a verified decision; an adapter places these
|
|
22
|
+
fields and nothing else."""
|
|
23
|
+
|
|
24
|
+
instrument: str
|
|
25
|
+
side: Side
|
|
26
|
+
quantity: int
|
|
27
|
+
type: OrderType
|
|
28
|
+
tif: Tif
|
|
29
|
+
limit_price: Decimal | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BrokerAdapter(Protocol):
|
|
33
|
+
"""What the SDK needs from a broker.
|
|
34
|
+
|
|
35
|
+
`g_seconds` is how long the broker takes to show a new order in its account data (ARITHMETIC 2.3, G), 1 to 3600.
|
|
36
|
+
`converts_cash` says whether the broker converts currency automatically when an order needs it. Both must equal
|
|
37
|
+
the account's settings at reTrade, which the owner confirmed at onboarding.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Read-only, so a frozen dataclass such as GenericAdapter, or a class attribute, satisfies the protocol.
|
|
41
|
+
@property
|
|
42
|
+
def g_seconds(self) -> int: ...
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def converts_cash(self) -> bool: ...
|
|
46
|
+
|
|
47
|
+
def snapshot(self, instrument: str | None, adjustments_since: datetime | None) -> tuple[Snapshot, Quote | None]:
|
|
48
|
+
"""Read the account now from the broker and, when `instrument` is given, a quote for it. Called for every
|
|
49
|
+
decision; never built from model output.
|
|
50
|
+
|
|
51
|
+
The snapshot must carry settled cash. It should carry the USDCAD rate when the broker has one, and the
|
|
52
|
+
deposits, withdrawals, dividends, fees, transfers and splits since `adjustments_since` (the account's last
|
|
53
|
+
accepted snapshot; None when there is none). Without them, a large change in the account blocks new buys
|
|
54
|
+
until an owner confirms it.
|
|
55
|
+
|
|
56
|
+
Every resting market buy needs a price, which reTrade takes from a position's mark (ARITHMETIC 3.1, 3.7). For
|
|
57
|
+
a market buy of an instrument the account doesn't hold, include a position of 0 shares with the current mark;
|
|
58
|
+
without one, a buy whose rules count that order is BLOCK `STATE-INCOMPLETE`."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def submit(self, order: ApprovedOrder, tag: str) -> str:
|
|
62
|
+
"""Place `order` with `tag` in the broker's client-order-id field and return the broker order id.
|
|
63
|
+
|
|
64
|
+
Raise BrokerRejected only when the broker definitively refused the order. Raise anything else, or let the
|
|
65
|
+
broker client's own exception through, when the outcome is unknown (a timeout, a dropped connection, a 5xx).
|
|
66
|
+
The SDK never retries a submit."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def find(self, tag: str, order: ApprovedOrder, since: datetime) -> str | None:
|
|
70
|
+
"""Look up an order whose submit outcome is unknown, and return its broker order id, or None if it isn't
|
|
71
|
+
visible. Search by `tag`; where the broker doesn't keep tags, match `order`'s fields among orders placed
|
|
72
|
+
since `since`. None never means "not placed": the broker may not show the order yet."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def cancel(self, broker_order_id: str) -> None:
|
|
76
|
+
"""Cancel a working order. Return once the broker has accepted the cancel request; raise if it refused it or
|
|
77
|
+
the outcome is unknown."""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class GenericAdapter:
|
|
83
|
+
"""An adapter made of four plain functions, so any broker works without a ready-made adapter. Each function has
|
|
84
|
+
the signature of the BrokerAdapter method of the same name. `converts_cash` defaults to False, the cautious
|
|
85
|
+
setting: reTrade then also checks cash in the order's own currency."""
|
|
86
|
+
|
|
87
|
+
snapshot: Callable[[str | None, datetime | None], tuple[Snapshot, Quote | None]]
|
|
88
|
+
submit: Callable[[ApprovedOrder, str], str]
|
|
89
|
+
find: Callable[[str, ApprovedOrder, datetime], str | None]
|
|
90
|
+
cancel: Callable[[str], None]
|
|
91
|
+
g_seconds: int = DEFAULT_G_SECONDS
|
|
92
|
+
converts_cash: bool = False
|
|
93
|
+
|
|
94
|
+
def __post_init__(self) -> None:
|
|
95
|
+
if (not isinstance(self.g_seconds, int) or isinstance(self.g_seconds, bool)
|
|
96
|
+
or not 1 <= self.g_seconds <= MAX_G_SECONDS):
|
|
97
|
+
raise InvalidInput(f"g_seconds must be an int from 1 to {MAX_G_SECONDS}")
|
|
98
|
+
if not isinstance(self.converts_cash, bool):
|
|
99
|
+
raise InvalidInput("converts_cash must be a bool")
|
retrade/cli.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The `retrade` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from retrade.keys import KeyFileError, generate_key
|
|
11
|
+
from retrade.kit import ClaimKit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv: list[str] | None = None) -> int:
|
|
15
|
+
parser = argparse.ArgumentParser(prog="retrade")
|
|
16
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
17
|
+
keygen = commands.add_parser("keygen", help="create an executor key")
|
|
18
|
+
keygen.add_argument("path", help="where to write the private key; it must not exist yet")
|
|
19
|
+
claim = commands.add_parser("claim-kit", help="print the evidence for one decision, for a claim")
|
|
20
|
+
claim.add_argument("decision_id")
|
|
21
|
+
claim.add_argument("--state-dir", default="retrade-state", help="the Client's state_dir (default: %(default)s)")
|
|
22
|
+
args = parser.parse_args(argv)
|
|
23
|
+
|
|
24
|
+
if args.command == "claim-kit":
|
|
25
|
+
return claim_kit(Path(args.state_dir), args.decision_id)
|
|
26
|
+
try:
|
|
27
|
+
public = generate_key(args.path)
|
|
28
|
+
except KeyFileError as error:
|
|
29
|
+
print(f"retrade: {error}", file=sys.stderr)
|
|
30
|
+
return 1
|
|
31
|
+
print(f"Private key written to {args.path}. Keep it secret; it never leaves this machine.")
|
|
32
|
+
print("Register this public key in reTrade's onboarding:")
|
|
33
|
+
print(public)
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def claim_kit(state_dir: Path, decision_id: str) -> int:
|
|
38
|
+
path = state_dir / "claim-kit.sqlite3"
|
|
39
|
+
if not path.exists():
|
|
40
|
+
print(f"retrade: no claim kit at {path}", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
kit = ClaimKit(path)
|
|
43
|
+
try:
|
|
44
|
+
exported = kit.export(decision_id)
|
|
45
|
+
finally:
|
|
46
|
+
kit.close()
|
|
47
|
+
if exported is None:
|
|
48
|
+
print(f"retrade: no decision {decision_id} in {path}", file=sys.stderr)
|
|
49
|
+
return 1
|
|
50
|
+
print(json.dumps(exported, indent=2))
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
sys.exit(main())
|