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 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())