ctrader-api-client 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.
- ctrader_api_client/__init__.py +64 -0
- ctrader_api_client/_internal/__init__.py +26 -0
- ctrader_api_client/_internal/messages.py +348 -0
- ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +42 -0
- ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +30 -0
- ctrader_api_client/_internal/proto/OpenApiMessages.py +1112 -0
- ctrader_api_client/_internal/proto/OpenApiModelMessages.py +802 -0
- ctrader_api_client/_internal/proto/__init__.py +320 -0
- ctrader_api_client/_internal/serialization.py +84 -0
- ctrader_api_client/api/__init__.py +21 -0
- ctrader_api_client/api/accounts.py +71 -0
- ctrader_api_client/api/market_data.py +424 -0
- ctrader_api_client/api/symbols.py +171 -0
- ctrader_api_client/api/trading.py +506 -0
- ctrader_api_client/auth/__init__.py +14 -0
- ctrader_api_client/auth/credentials.py +72 -0
- ctrader_api_client/auth/manager.py +511 -0
- ctrader_api_client/client.py +475 -0
- ctrader_api_client/config.py +56 -0
- ctrader_api_client/connection/__init__.py +16 -0
- ctrader_api_client/connection/heartbeat.py +120 -0
- ctrader_api_client/connection/protocol.py +366 -0
- ctrader_api_client/connection/transport.py +123 -0
- ctrader_api_client/enums.py +138 -0
- ctrader_api_client/events/__init__.py +65 -0
- ctrader_api_client/events/emitter.py +254 -0
- ctrader_api_client/events/router.py +400 -0
- ctrader_api_client/events/types.py +340 -0
- ctrader_api_client/exceptions.py +231 -0
- ctrader_api_client/models/__init__.py +50 -0
- ctrader_api_client/models/_base.py +19 -0
- ctrader_api_client/models/account.py +177 -0
- ctrader_api_client/models/deal.py +242 -0
- ctrader_api_client/models/market_data.py +192 -0
- ctrader_api_client/models/order.py +262 -0
- ctrader_api_client/models/position.py +209 -0
- ctrader_api_client/models/requests.py +299 -0
- ctrader_api_client/models/symbol.py +194 -0
- ctrader_api_client/py.typed +0 -0
- ctrader_api_client-0.1.0.dist-info/METADATA +252 -0
- ctrader_api_client-0.1.0.dist-info/RECORD +43 -0
- ctrader_api_client-0.1.0.dist-info/WHEEL +4 -0
- ctrader_api_client-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .._internal.proto import ProtoOAAccessRights, ProtoOAAccountType
|
|
8
|
+
from ..enums import AccessRights, AccountType
|
|
9
|
+
from ._base import FrozenModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .._internal.proto import ProtoOACtidTraderAccount, ProtoOATrader
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _timestamp_to_datetime(timestamp_ms: int) -> datetime:
|
|
17
|
+
"""Convert millisecond timestamp to datetime."""
|
|
18
|
+
return datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_ACCOUNT_TYPE_MAP: dict[int, AccountType] = {
|
|
22
|
+
ProtoOAAccountType.HEDGED: AccountType.HEDGED,
|
|
23
|
+
ProtoOAAccountType.NETTED: AccountType.NETTED,
|
|
24
|
+
ProtoOAAccountType.SPREAD_BETTING: AccountType.SPREAD_BETTING,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_ACCESS_RIGHTS_MAP: dict[int, AccessRights] = {
|
|
28
|
+
ProtoOAAccessRights.FULL_ACCESS: AccessRights.FULL_ACCESS,
|
|
29
|
+
ProtoOAAccessRights.CLOSE_ONLY: AccessRights.CLOSE_ONLY,
|
|
30
|
+
ProtoOAAccessRights.NO_TRADING: AccessRights.NO_TRADING,
|
|
31
|
+
ProtoOAAccessRights.NO_LOGIN: AccessRights.NO_LOGIN,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AccountSummary(FrozenModel):
|
|
36
|
+
"""Summary of a trading account from account list.
|
|
37
|
+
|
|
38
|
+
This is the lightweight representation returned when listing accounts
|
|
39
|
+
associated with an access token. Use Account for full details after
|
|
40
|
+
authorization.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
account_id: The cTID trader account ID.
|
|
44
|
+
is_live: True if this is a live account, False for demo.
|
|
45
|
+
trader_login: The trader's login number.
|
|
46
|
+
broker_name: Short name of the broker.
|
|
47
|
+
last_closing_deal_timestamp: Timestamp of last closing deal, or None.
|
|
48
|
+
last_balance_update_timestamp: Timestamp of last balance update, or None.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
account_id: int
|
|
52
|
+
is_live: bool
|
|
53
|
+
trader_login: int
|
|
54
|
+
broker_name: str
|
|
55
|
+
last_closing_deal_timestamp: datetime | None = None
|
|
56
|
+
last_balance_update_timestamp: datetime | None = None
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_proto(cls, proto: ProtoOACtidTraderAccount) -> AccountSummary:
|
|
60
|
+
"""Create AccountSummary from proto message.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
proto: The proto message to convert.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A new AccountSummary instance.
|
|
67
|
+
"""
|
|
68
|
+
return cls(
|
|
69
|
+
account_id=proto.ctid_trader_account_id,
|
|
70
|
+
is_live=proto.is_live,
|
|
71
|
+
trader_login=proto.trader_login,
|
|
72
|
+
broker_name=proto.broker_title_short or "",
|
|
73
|
+
last_closing_deal_timestamp=(
|
|
74
|
+
_timestamp_to_datetime(proto.last_closing_deal_timestamp) if proto.last_closing_deal_timestamp else None
|
|
75
|
+
),
|
|
76
|
+
last_balance_update_timestamp=(
|
|
77
|
+
_timestamp_to_datetime(proto.last_balance_update_timestamp)
|
|
78
|
+
if proto.last_balance_update_timestamp
|
|
79
|
+
else None
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Account(FrozenModel):
|
|
85
|
+
"""Full trading account details.
|
|
86
|
+
|
|
87
|
+
Retrieved after authorizing a specific account. Contains balance,
|
|
88
|
+
leverage, and other trading parameters.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
account_id: The cTID trader account ID.
|
|
92
|
+
trader_login: The trader's login number.
|
|
93
|
+
balance: Account balance (raw integer, use get_balance() for Decimal).
|
|
94
|
+
money_digits: Decimal places for monetary values.
|
|
95
|
+
leverage_in_cents: Account leverage in cents (e.g., 10000 = 1:100).
|
|
96
|
+
account_type: Account type (HEDGED, NETTED, SPREAD_BETTING).
|
|
97
|
+
access_rights: Current access rights for the account.
|
|
98
|
+
broker_name: Name of the broker.
|
|
99
|
+
deposit_asset_id: Asset ID of the deposit currency.
|
|
100
|
+
swap_free: Whether account is swap-free (Islamic Banking).
|
|
101
|
+
is_limited_risk: Whether account has limited risk mode.
|
|
102
|
+
registration_timestamp: When the account was registered.
|
|
103
|
+
max_leverage: Maximum allowed leverage.
|
|
104
|
+
balance_version: Version number for balance updates.
|
|
105
|
+
manager_bonus: Manager bonus amount (raw integer).
|
|
106
|
+
ib_bonus: IB bonus amount (raw integer).
|
|
107
|
+
non_withdrawable_bonus: Non-withdrawable bonus amount (raw integer).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
account_id: int
|
|
111
|
+
trader_login: int
|
|
112
|
+
balance: int
|
|
113
|
+
money_digits: int
|
|
114
|
+
leverage_in_cents: int
|
|
115
|
+
account_type: AccountType
|
|
116
|
+
access_rights: AccessRights
|
|
117
|
+
broker_name: str
|
|
118
|
+
deposit_asset_id: int
|
|
119
|
+
swap_free: bool
|
|
120
|
+
is_limited_risk: bool
|
|
121
|
+
registration_timestamp: datetime | None = None
|
|
122
|
+
|
|
123
|
+
# Optional fields
|
|
124
|
+
max_leverage: int | None = None
|
|
125
|
+
balance_version: int | None = None
|
|
126
|
+
manager_bonus: int | None = None
|
|
127
|
+
ib_bonus: int | None = None
|
|
128
|
+
non_withdrawable_bonus: int | None = None
|
|
129
|
+
|
|
130
|
+
def get_balance(self) -> Decimal:
|
|
131
|
+
"""Get balance as Decimal.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Balance divided by 10^money_digits.
|
|
135
|
+
"""
|
|
136
|
+
return Decimal(self.balance) / Decimal(10**self.money_digits)
|
|
137
|
+
|
|
138
|
+
def get_leverage(self) -> str:
|
|
139
|
+
"""Get leverage as human-readable string.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Leverage string like "1:100".
|
|
143
|
+
"""
|
|
144
|
+
leverage = self.leverage_in_cents // 100
|
|
145
|
+
return f"1:{leverage}"
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def from_proto(cls, proto: ProtoOATrader) -> Account:
|
|
149
|
+
"""Create Account from proto message.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
proto: The proto message to convert.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
A new Account instance.
|
|
156
|
+
"""
|
|
157
|
+
return cls(
|
|
158
|
+
account_id=proto.ctid_trader_account_id,
|
|
159
|
+
trader_login=proto.trader_login,
|
|
160
|
+
balance=proto.balance,
|
|
161
|
+
money_digits=proto.money_digits if proto.money_digits else 2,
|
|
162
|
+
leverage_in_cents=proto.leverage_in_cents,
|
|
163
|
+
account_type=_ACCOUNT_TYPE_MAP.get(proto.account_type, AccountType.HEDGED),
|
|
164
|
+
access_rights=_ACCESS_RIGHTS_MAP.get(proto.access_rights, AccessRights.FULL_ACCESS),
|
|
165
|
+
broker_name=proto.broker_name or "",
|
|
166
|
+
deposit_asset_id=proto.deposit_asset_id,
|
|
167
|
+
swap_free=proto.swap_free,
|
|
168
|
+
is_limited_risk=proto.is_limited_risk,
|
|
169
|
+
registration_timestamp=(
|
|
170
|
+
_timestamp_to_datetime(proto.registration_timestamp) if proto.registration_timestamp else None
|
|
171
|
+
),
|
|
172
|
+
max_leverage=proto.max_leverage if proto.max_leverage else None,
|
|
173
|
+
balance_version=proto.balance_version if proto.balance_version else None,
|
|
174
|
+
manager_bonus=proto.manager_bonus if proto.manager_bonus else None,
|
|
175
|
+
ib_bonus=proto.ib_bonus if proto.ib_bonus else None,
|
|
176
|
+
non_withdrawable_bonus=proto.non_withdrawable_bonus if proto.non_withdrawable_bonus else None,
|
|
177
|
+
)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .._internal.proto import ProtoOADealStatus
|
|
8
|
+
from ..enums import DealStatus, OrderSide
|
|
9
|
+
from ._base import FrozenModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .._internal.proto import ProtoOAClosePositionDetail, ProtoOADeal
|
|
14
|
+
from .symbol import Symbol
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _timestamp_to_datetime(timestamp_ms: int) -> datetime:
|
|
18
|
+
"""Convert millisecond timestamp to datetime."""
|
|
19
|
+
return datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_DEAL_STATUS_MAP: dict[int, DealStatus] = {
|
|
23
|
+
ProtoOADealStatus.FILLED: DealStatus.FILLED,
|
|
24
|
+
ProtoOADealStatus.PARTIALLY_FILLED: DealStatus.PARTIALLY_FILLED,
|
|
25
|
+
ProtoOADealStatus.REJECTED: DealStatus.REJECTED,
|
|
26
|
+
ProtoOADealStatus.INTERNALLY_REJECTED: DealStatus.INTERNALLY_REJECTED,
|
|
27
|
+
ProtoOADealStatus.ERROR: DealStatus.ERROR,
|
|
28
|
+
ProtoOADealStatus.MISSED: DealStatus.MISSED,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CloseDetail(FrozenModel):
|
|
33
|
+
"""Details about position closure from a deal.
|
|
34
|
+
|
|
35
|
+
Present only when a deal results in closing (fully or partially)
|
|
36
|
+
a position.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
entry_price: Original entry price of the position.
|
|
40
|
+
closed_volume: Volume that was closed.
|
|
41
|
+
gross_profit: Gross profit/loss (raw integer).
|
|
42
|
+
swap: Swap charged (raw integer).
|
|
43
|
+
commission: Commission charged (raw integer).
|
|
44
|
+
balance: Account balance after close (raw integer).
|
|
45
|
+
money_digits: Decimal places for monetary values.
|
|
46
|
+
pnl_conversion_fee: Fee for P&L currency conversion (raw integer).
|
|
47
|
+
quote_to_deposit_rate: Conversion rate from quote to deposit currency.
|
|
48
|
+
balance_version: Version number for balance updates.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
entry_price: float
|
|
52
|
+
closed_volume: int
|
|
53
|
+
gross_profit: int
|
|
54
|
+
swap: int
|
|
55
|
+
commission: int
|
|
56
|
+
balance: int
|
|
57
|
+
money_digits: int
|
|
58
|
+
pnl_conversion_fee: int = 0
|
|
59
|
+
quote_to_deposit_rate: float | None = None
|
|
60
|
+
balance_version: int | None = None
|
|
61
|
+
|
|
62
|
+
def get_gross_profit(self) -> Decimal:
|
|
63
|
+
"""Get gross profit as Decimal.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Gross profit divided by 10^money_digits.
|
|
67
|
+
"""
|
|
68
|
+
return Decimal(self.gross_profit) / Decimal(10**self.money_digits)
|
|
69
|
+
|
|
70
|
+
def get_net_profit(self) -> Decimal:
|
|
71
|
+
"""Get net profit (gross - swap - commission - fee) as Decimal.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Net profit divided by 10^money_digits.
|
|
75
|
+
"""
|
|
76
|
+
net = self.gross_profit - self.swap - self.commission - self.pnl_conversion_fee
|
|
77
|
+
return Decimal(net) / Decimal(10**self.money_digits)
|
|
78
|
+
|
|
79
|
+
def get_swap(self) -> Decimal:
|
|
80
|
+
"""Get swap as Decimal.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Swap divided by 10^money_digits.
|
|
84
|
+
"""
|
|
85
|
+
return Decimal(self.swap) / Decimal(10**self.money_digits)
|
|
86
|
+
|
|
87
|
+
def get_commission(self) -> Decimal:
|
|
88
|
+
"""Get commission as Decimal.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Commission divided by 10^money_digits.
|
|
92
|
+
"""
|
|
93
|
+
return Decimal(self.commission) / Decimal(10**self.money_digits)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_proto(cls, proto: ProtoOAClosePositionDetail) -> CloseDetail:
|
|
97
|
+
"""Create CloseDetail from proto message.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
proto: The proto message to convert.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
A new CloseDetail instance.
|
|
104
|
+
"""
|
|
105
|
+
return cls(
|
|
106
|
+
entry_price=proto.entry_price,
|
|
107
|
+
closed_volume=proto.closed_volume,
|
|
108
|
+
gross_profit=proto.gross_profit,
|
|
109
|
+
swap=proto.swap,
|
|
110
|
+
commission=proto.commission,
|
|
111
|
+
balance=proto.balance,
|
|
112
|
+
money_digits=proto.money_digits if proto.money_digits else 2,
|
|
113
|
+
pnl_conversion_fee=proto.pnl_conversion_fee if proto.pnl_conversion_fee else 0,
|
|
114
|
+
quote_to_deposit_rate=proto.quote_to_deposit_conversion_rate
|
|
115
|
+
if proto.quote_to_deposit_conversion_rate
|
|
116
|
+
else None,
|
|
117
|
+
balance_version=proto.balance_version if proto.balance_version else None,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Deal(FrozenModel):
|
|
122
|
+
"""A trade execution (deal).
|
|
123
|
+
|
|
124
|
+
Represents a single execution that fills an order. An order may have
|
|
125
|
+
multiple deals if it's filled in parts.
|
|
126
|
+
|
|
127
|
+
Attributes:
|
|
128
|
+
deal_id: Unique deal identifier.
|
|
129
|
+
order_id: The order that was executed.
|
|
130
|
+
position_id: The position affected by this deal.
|
|
131
|
+
symbol_id: The symbol traded.
|
|
132
|
+
side: Trade direction (BUY/SELL).
|
|
133
|
+
volume: Requested volume in cents.
|
|
134
|
+
filled_volume: Actually filled volume in cents.
|
|
135
|
+
execution_price: Price at which the deal was executed.
|
|
136
|
+
execution_timestamp: When the deal was executed.
|
|
137
|
+
status: Deal status.
|
|
138
|
+
commission: Commission charged (raw integer).
|
|
139
|
+
money_digits: Decimal places for monetary values.
|
|
140
|
+
create_timestamp: When the deal was created.
|
|
141
|
+
last_update_timestamp: When the deal was last updated.
|
|
142
|
+
margin_rate: Margin rate for the deal.
|
|
143
|
+
base_to_usd_rate: Conversion rate from base currency to USD.
|
|
144
|
+
close_detail: Details if this deal closed a position, or None.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
deal_id: int
|
|
148
|
+
order_id: int
|
|
149
|
+
position_id: int
|
|
150
|
+
symbol_id: int
|
|
151
|
+
side: OrderSide
|
|
152
|
+
volume: int
|
|
153
|
+
filled_volume: int
|
|
154
|
+
execution_price: float
|
|
155
|
+
execution_timestamp: datetime
|
|
156
|
+
status: DealStatus
|
|
157
|
+
commission: int
|
|
158
|
+
money_digits: int
|
|
159
|
+
|
|
160
|
+
# Optional
|
|
161
|
+
create_timestamp: datetime | None = None
|
|
162
|
+
last_update_timestamp: datetime | None = None
|
|
163
|
+
margin_rate: float | None = None
|
|
164
|
+
base_to_usd_rate: float | None = None
|
|
165
|
+
close_detail: CloseDetail | None = None
|
|
166
|
+
|
|
167
|
+
def get_execution_price(self, symbol: Symbol) -> Decimal:
|
|
168
|
+
"""Get execution price as Decimal.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
symbol: Symbol for price precision.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Execution price with correct decimal places.
|
|
175
|
+
"""
|
|
176
|
+
return Decimal(str(self.execution_price)).quantize(Decimal(10) ** -symbol.digits)
|
|
177
|
+
|
|
178
|
+
def get_commission(self) -> Decimal:
|
|
179
|
+
"""Get commission as Decimal.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Commission divided by 10^money_digits.
|
|
183
|
+
"""
|
|
184
|
+
return Decimal(self.commission) / Decimal(10**self.money_digits)
|
|
185
|
+
|
|
186
|
+
def get_volume_in_lots(self, symbol: Symbol) -> Decimal:
|
|
187
|
+
"""Get filled volume in lots.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
symbol: Symbol for lot conversion.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Volume in lots.
|
|
194
|
+
"""
|
|
195
|
+
return symbol.volume_to_lots(self.filled_volume)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def is_closing_deal(self) -> bool:
|
|
199
|
+
"""Whether this deal closed (or partially closed) a position."""
|
|
200
|
+
if self.close_detail is not None:
|
|
201
|
+
return self.close_detail.balance > 0
|
|
202
|
+
else:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_proto(cls, proto: ProtoOADeal) -> Deal:
|
|
207
|
+
"""Create Deal from proto message.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
proto: The proto message to convert.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
A new Deal instance.
|
|
214
|
+
"""
|
|
215
|
+
close_detail = None
|
|
216
|
+
if proto.close_position_detail:
|
|
217
|
+
close_detail = CloseDetail.from_proto(proto.close_position_detail)
|
|
218
|
+
|
|
219
|
+
# Determine side
|
|
220
|
+
side = OrderSide.BUY if proto.trade_side == 1 else OrderSide.SELL
|
|
221
|
+
|
|
222
|
+
return cls(
|
|
223
|
+
deal_id=proto.deal_id,
|
|
224
|
+
order_id=proto.order_id,
|
|
225
|
+
position_id=proto.position_id,
|
|
226
|
+
symbol_id=proto.symbol_id,
|
|
227
|
+
side=side,
|
|
228
|
+
volume=proto.volume,
|
|
229
|
+
filled_volume=proto.filled_volume,
|
|
230
|
+
execution_price=proto.execution_price,
|
|
231
|
+
execution_timestamp=_timestamp_to_datetime(proto.execution_timestamp),
|
|
232
|
+
status=_DEAL_STATUS_MAP.get(proto.deal_status, DealStatus.FILLED),
|
|
233
|
+
commission=proto.commission if proto.commission else 0,
|
|
234
|
+
money_digits=proto.money_digits if proto.money_digits else 2,
|
|
235
|
+
create_timestamp=_timestamp_to_datetime(proto.create_timestamp) if proto.create_timestamp else None,
|
|
236
|
+
last_update_timestamp=(
|
|
237
|
+
_timestamp_to_datetime(proto.utc_last_update_timestamp) if proto.utc_last_update_timestamp else None
|
|
238
|
+
),
|
|
239
|
+
margin_rate=proto.margin_rate if proto.margin_rate else None,
|
|
240
|
+
base_to_usd_rate=proto.base_to_usd_conversion_rate if proto.base_to_usd_conversion_rate else None,
|
|
241
|
+
close_detail=close_detail,
|
|
242
|
+
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .._internal.proto import ProtoOATrendbarPeriod
|
|
8
|
+
from ..enums import TrendbarPeriod
|
|
9
|
+
from ._base import FrozenModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .._internal.proto import ProtoOATickData, ProtoOATrendbar
|
|
14
|
+
from .symbol import Symbol
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_PERIOD_MAP: dict[int, TrendbarPeriod] = {
|
|
18
|
+
ProtoOATrendbarPeriod.M1: TrendbarPeriod.M1,
|
|
19
|
+
ProtoOATrendbarPeriod.M2: TrendbarPeriod.M2,
|
|
20
|
+
ProtoOATrendbarPeriod.M3: TrendbarPeriod.M3,
|
|
21
|
+
ProtoOATrendbarPeriod.M4: TrendbarPeriod.M4,
|
|
22
|
+
ProtoOATrendbarPeriod.M5: TrendbarPeriod.M5,
|
|
23
|
+
ProtoOATrendbarPeriod.M10: TrendbarPeriod.M10,
|
|
24
|
+
ProtoOATrendbarPeriod.M15: TrendbarPeriod.M15,
|
|
25
|
+
ProtoOATrendbarPeriod.M30: TrendbarPeriod.M30,
|
|
26
|
+
ProtoOATrendbarPeriod.H1: TrendbarPeriod.H1,
|
|
27
|
+
ProtoOATrendbarPeriod.H4: TrendbarPeriod.H4,
|
|
28
|
+
ProtoOATrendbarPeriod.H12: TrendbarPeriod.H12,
|
|
29
|
+
ProtoOATrendbarPeriod.D1: TrendbarPeriod.D1,
|
|
30
|
+
ProtoOATrendbarPeriod.W1: TrendbarPeriod.W1,
|
|
31
|
+
ProtoOATrendbarPeriod.MN1: TrendbarPeriod.MN1,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Trendbar(FrozenModel):
|
|
36
|
+
"""Historical OHLC bar (candlestick).
|
|
37
|
+
|
|
38
|
+
Note: The raw proto stores prices as deltas from low. This model
|
|
39
|
+
exposes computed open/high/close values.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
timestamp: Bar open time.
|
|
43
|
+
period: Bar period (M1, H1, D1, etc.).
|
|
44
|
+
low: Low price (raw integer).
|
|
45
|
+
open: Open price (raw integer).
|
|
46
|
+
high: High price (raw integer).
|
|
47
|
+
close: Close price (raw integer).
|
|
48
|
+
volume: Trade volume.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
timestamp: datetime
|
|
52
|
+
period: TrendbarPeriod
|
|
53
|
+
low: int
|
|
54
|
+
open: int
|
|
55
|
+
high: int
|
|
56
|
+
close: int
|
|
57
|
+
volume: int
|
|
58
|
+
|
|
59
|
+
def get_ohlc(self, symbol: Symbol) -> tuple[Decimal, Decimal, Decimal, Decimal]:
|
|
60
|
+
"""Get OHLC prices as Decimals.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
symbol: Symbol for price conversion.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple of (open, high, low, close) as Decimals.
|
|
67
|
+
"""
|
|
68
|
+
return (
|
|
69
|
+
symbol.price_to_decimal(self.open),
|
|
70
|
+
symbol.price_to_decimal(self.high),
|
|
71
|
+
symbol.price_to_decimal(self.low),
|
|
72
|
+
symbol.price_to_decimal(self.close),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def get_open(self, symbol: Symbol) -> Decimal:
|
|
76
|
+
"""Get open price as Decimal.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
symbol: Symbol for price conversion.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Open price.
|
|
83
|
+
"""
|
|
84
|
+
return symbol.price_to_decimal(self.open)
|
|
85
|
+
|
|
86
|
+
def get_high(self, symbol: Symbol) -> Decimal:
|
|
87
|
+
"""Get high price as Decimal.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
symbol: Symbol for price conversion.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
High price.
|
|
94
|
+
"""
|
|
95
|
+
return symbol.price_to_decimal(self.high)
|
|
96
|
+
|
|
97
|
+
def get_low(self, symbol: Symbol) -> Decimal:
|
|
98
|
+
"""Get low price as Decimal.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
symbol: Symbol for price conversion.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Low price.
|
|
105
|
+
"""
|
|
106
|
+
return symbol.price_to_decimal(self.low)
|
|
107
|
+
|
|
108
|
+
def get_close(self, symbol: Symbol) -> Decimal:
|
|
109
|
+
"""Get close price as Decimal.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
symbol: Symbol for price conversion.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Close price.
|
|
116
|
+
"""
|
|
117
|
+
return symbol.price_to_decimal(self.close)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_proto(cls, proto: ProtoOATrendbar) -> Trendbar:
|
|
121
|
+
"""Create Trendbar from proto message.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
proto: The proto message.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A new Trendbar instance.
|
|
128
|
+
"""
|
|
129
|
+
# Calculate timestamp from utc_timestamp_in_minutes
|
|
130
|
+
ts = datetime.now(UTC)
|
|
131
|
+
if proto.utc_timestamp_in_minutes:
|
|
132
|
+
ts = datetime.fromtimestamp(proto.utc_timestamp_in_minutes * 60, tz=UTC)
|
|
133
|
+
|
|
134
|
+
# Proto stores: low as absolute, others as deltas from low
|
|
135
|
+
low = proto.low
|
|
136
|
+
open_price = low + proto.delta_open
|
|
137
|
+
high = low + proto.delta_high
|
|
138
|
+
close = low + proto.delta_close
|
|
139
|
+
|
|
140
|
+
return cls(
|
|
141
|
+
timestamp=ts,
|
|
142
|
+
period=_PERIOD_MAP.get(proto.period, TrendbarPeriod.M1),
|
|
143
|
+
low=low,
|
|
144
|
+
open=open_price,
|
|
145
|
+
high=high,
|
|
146
|
+
close=close,
|
|
147
|
+
volume=proto.volume,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TickData(FrozenModel):
|
|
152
|
+
"""Historical tick data point.
|
|
153
|
+
|
|
154
|
+
Represents a single price tick from tick history.
|
|
155
|
+
|
|
156
|
+
Attributes:
|
|
157
|
+
timestamp: Tick time.
|
|
158
|
+
price: Tick price (raw integer).
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
timestamp: datetime
|
|
162
|
+
price: int
|
|
163
|
+
|
|
164
|
+
def get_price(self, symbol: Symbol) -> Decimal:
|
|
165
|
+
"""Get price as Decimal.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
symbol: Symbol for price conversion.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Price as Decimal.
|
|
172
|
+
"""
|
|
173
|
+
return symbol.price_to_decimal(self.price)
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def from_proto(cls, proto: ProtoOATickData, base_timestamp_ms: int = 0) -> TickData:
|
|
177
|
+
"""Create TickData from proto message.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
proto: The proto message.
|
|
181
|
+
base_timestamp_ms: Base timestamp in milliseconds. Proto timestamp
|
|
182
|
+
is a delta from this base.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
A new TickData instance.
|
|
186
|
+
"""
|
|
187
|
+
# Proto timestamp is delta from base in milliseconds
|
|
188
|
+
actual_ts = base_timestamp_ms + proto.timestamp
|
|
189
|
+
return cls(
|
|
190
|
+
timestamp=datetime.fromtimestamp(actual_ts / 1000, tz=UTC),
|
|
191
|
+
price=proto.tick,
|
|
192
|
+
)
|