@riocrypto/common-server 1.0.2781 → 1.0.2783
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.
- package/build/clients/cluster-client.d.ts +14 -9
- package/build/clients/cluster-client.js +25 -8
- package/build/helpers/get-fx-trading-policy.d.ts +61 -0
- package/build/helpers/get-fx-trading-policy.js +287 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/models/emarkets-fx-trade.d.ts +2 -4
- package/build/models/emarkets-fx-trade.js +1 -4
- package/build/models/external-trade.d.ts +2 -0
- package/build/models/external-trade.js +3 -0
- package/build/models/fx-trade.d.ts +11 -7
- package/build/models/fx-trade.js +5 -2
- package/build/models/rio-settings.d.ts +3 -15
- package/build/models/rio-settings.js +2 -8
- package/build/models/transnetwork-fx-trade.d.ts +2 -4
- package/build/models/transnetwork-fx-trade.js +1 -4
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Quote, Fiat, Crypto, BitsoBankAccount, Side, Country, Order, CryptoAddress, BankAccount, AuthRole, Auth, ImportOrderData, TreasuryProvider, FXProvider, EmarketsFXTrade, ExternalTradingAlgorithm, ExternalTrade, ExternalTradeType, ExternalTradingProvider, ExternalTradingAlgorithmType, STPMXNWithdrawal, AuthPermission, DeferredPaymentType, TwoWaySettlementType, OrderType, EmarketsSettlementType, EmarketsOrderType, BulkBankPayout, BulkCryptoPayout, BulkBankPayment, BulkCryptoPayment, OrderStatus, TWAPSession, TransnetworkFXTrade, TransnetworkSettlementType, TransnetworkOrderType, AuthMethod, StonexFXTrade } from "@riocrypto/common";
|
|
1
|
+
import { Quote, Fiat, Crypto, BitsoBankAccount, Side, Country, Order, CryptoAddress, BankAccount, AuthRole, Auth, ImportOrderData, TreasuryProvider, FXProvider, EmarketsFXTrade, ExternalTradingAlgorithm, ExternalTrade, ExternalTradeType, ExternalTradingProvider, ExternalTradingAlgorithmType, STPMXNWithdrawal, AuthPermission, DeferredPaymentType, TwoWaySettlementType, OrderType, EmarketsSettlementType, EmarketsOrderType, BulkBankPayout, BulkCryptoPayout, BulkBankPayment, BulkCryptoPayment, OrderStatus, TWAPSession, TransnetworkFXTrade, TransnetworkSettlementType, TransnetworkOrderType, AuthMethod, StonexFXTrade, CancelExternalTradeReason } from "@riocrypto/common";
|
|
2
2
|
import { STPMXNWithdrawalDoc } from "../models/STP-mxn-withdrawal";
|
|
3
3
|
declare class ClusterClient {
|
|
4
4
|
private baseUrl;
|
|
@@ -125,26 +125,28 @@ declare class ClusterClient {
|
|
|
125
125
|
receiverCLABE: string;
|
|
126
126
|
amount: number;
|
|
127
127
|
}): Promise<string>;
|
|
128
|
-
placeEmarketsTrade({ amountToTrade, limitPrice, settlementType, orderType, side,
|
|
128
|
+
placeEmarketsTrade({ externalTradeId, amountToTrade, limitPrice, settlementType, orderType, side, }: {
|
|
129
|
+
externalTradeId: string;
|
|
129
130
|
amountToTrade: number;
|
|
130
131
|
limitPrice: number;
|
|
131
132
|
settlementType: EmarketsSettlementType;
|
|
132
133
|
orderType: EmarketsOrderType;
|
|
133
134
|
side: Side;
|
|
134
|
-
isExternalTrade: boolean;
|
|
135
135
|
}): Promise<EmarketsFXTrade>;
|
|
136
|
-
placeTransnetworkTrade({ amountToTrade, limitPrice, settlementType, orderType, side,
|
|
136
|
+
placeTransnetworkTrade({ externalTradeId, amountToTrade, limitPrice, settlementType, orderType, side, }: {
|
|
137
|
+
externalTradeId: string;
|
|
137
138
|
amountToTrade: number;
|
|
138
139
|
limitPrice: number;
|
|
139
140
|
settlementType: TransnetworkSettlementType;
|
|
140
141
|
orderType: TransnetworkOrderType;
|
|
141
142
|
side: Side;
|
|
142
|
-
isExternalTrade: boolean;
|
|
143
143
|
}): Promise<TransnetworkFXTrade>;
|
|
144
144
|
retryEmarketsTrade(id: string): Promise<void>;
|
|
145
145
|
cancelEmarketsTrade(id: string): Promise<void>;
|
|
146
|
+
cancelTransnetworkTrade(id: string): Promise<void>;
|
|
146
147
|
retryExternalTrade(id: string): Promise<void>;
|
|
147
|
-
|
|
148
|
+
takeExternalTradeQuote(id: string, side: Side): Promise<void>;
|
|
149
|
+
cancelExternalTrade(id: string, reason?: CancelExternalTradeReason): Promise<void>;
|
|
148
150
|
importExternalTrade(data: {
|
|
149
151
|
provider: ExternalTradingProvider;
|
|
150
152
|
originCurrency: Crypto | Fiat;
|
|
@@ -176,16 +178,19 @@ declare class ClusterClient {
|
|
|
176
178
|
stopExternalTradingAlgorithm(id: string): Promise<ExternalTradingAlgorithm>;
|
|
177
179
|
getExternalTradingAlgorithm(id: string): Promise<ExternalTradingAlgorithm>;
|
|
178
180
|
getExternalTrade(id: string): Promise<ExternalTrade>;
|
|
179
|
-
placeExternalTrade({ provider, type, originCurrency, destinationCurrency, requestedOriginAmount, requestedDestinationAmount, limitPrice,
|
|
181
|
+
placeExternalTrade({ provider, type, originCurrency, destinationCurrency, requestedOriginAmount, requestedDestinationAmount, limitPrice, twoWaySettlementDateOffset, arbitrageSessionId, fxTradeId, timeInForce, valueDate, }: {
|
|
180
182
|
provider: ExternalTradingProvider;
|
|
181
183
|
type: ExternalTradeType;
|
|
182
184
|
originCurrency: Crypto | Fiat;
|
|
183
185
|
destinationCurrency: Crypto | Fiat;
|
|
184
186
|
requestedOriginAmount?: number;
|
|
185
187
|
requestedDestinationAmount?: number;
|
|
186
|
-
limitPrice
|
|
187
|
-
|
|
188
|
+
limitPrice?: number;
|
|
189
|
+
twoWaySettlementDateOffset?: number;
|
|
188
190
|
arbitrageSessionId?: string;
|
|
191
|
+
fxTradeId?: string;
|
|
192
|
+
timeInForce?: "day" | "gtc" | "ioc" | "fok";
|
|
193
|
+
valueDate?: string;
|
|
189
194
|
}): Promise<ExternalTrade>;
|
|
190
195
|
generateSTPDepositCLABE(userId: string): Promise<{
|
|
191
196
|
CLABE: string;
|
|
@@ -482,14 +482,14 @@ class ClusterClient {
|
|
|
482
482
|
return (_a = response.data) === null || _a === void 0 ? void 0 : _a.url;
|
|
483
483
|
});
|
|
484
484
|
}
|
|
485
|
-
placeEmarketsTrade({ amountToTrade, limitPrice, settlementType, orderType, side,
|
|
485
|
+
placeEmarketsTrade({ externalTradeId, amountToTrade, limitPrice, settlementType, orderType, side, }) {
|
|
486
486
|
return __awaiter(this, void 0, void 0, function* () {
|
|
487
487
|
const response = yield this.axios.post(`${this.baseUrl}/api/emarkets/bot/trades`, {
|
|
488
|
+
externalTradeId,
|
|
488
489
|
amountToTrade,
|
|
489
490
|
limitPrice,
|
|
490
491
|
orderType,
|
|
491
492
|
side,
|
|
492
|
-
isExternalTrade,
|
|
493
493
|
settlementType,
|
|
494
494
|
}, {
|
|
495
495
|
headers: {
|
|
@@ -499,14 +499,14 @@ class ClusterClient {
|
|
|
499
499
|
return response.data;
|
|
500
500
|
});
|
|
501
501
|
}
|
|
502
|
-
placeTransnetworkTrade({ amountToTrade, limitPrice, settlementType, orderType, side,
|
|
502
|
+
placeTransnetworkTrade({ externalTradeId, amountToTrade, limitPrice, settlementType, orderType, side, }) {
|
|
503
503
|
return __awaiter(this, void 0, void 0, function* () {
|
|
504
504
|
const response = yield this.axios.post(`${this.baseUrl}/api/transnetwork/bot/trades`, {
|
|
505
|
+
externalTradeId,
|
|
505
506
|
amountToTrade,
|
|
506
507
|
limitPrice,
|
|
507
508
|
orderType,
|
|
508
509
|
side,
|
|
509
|
-
isExternalTrade,
|
|
510
510
|
settlementType,
|
|
511
511
|
}, {
|
|
512
512
|
headers: {
|
|
@@ -534,14 +534,28 @@ class ClusterClient {
|
|
|
534
534
|
});
|
|
535
535
|
});
|
|
536
536
|
}
|
|
537
|
+
cancelTransnetworkTrade(id) {
|
|
538
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
539
|
+
yield this.axios.post(`${this.baseUrl}/api/transnetwork/bot/trades/${id}/cancel`, {}, {
|
|
540
|
+
headers: {
|
|
541
|
+
"x-cluster-api-key": this.clusterApiKey,
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
537
546
|
retryExternalTrade(id) {
|
|
538
547
|
return __awaiter(this, void 0, void 0, function* () {
|
|
539
548
|
yield this.axios.post(`${this.baseUrl}/api/trading/external/trades/${id}/retry`, {}, { headers: { "x-cluster-api-key": this.clusterApiKey } });
|
|
540
549
|
});
|
|
541
550
|
}
|
|
542
|
-
|
|
551
|
+
takeExternalTradeQuote(id, side) {
|
|
552
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
553
|
+
yield this.axios.post(`${this.baseUrl}/api/trading/external/trades/${id}/take-quote`, { side }, { headers: { "x-cluster-api-key": this.clusterApiKey } });
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
cancelExternalTrade(id, reason) {
|
|
543
557
|
return __awaiter(this, void 0, void 0, function* () {
|
|
544
|
-
yield this.axios.post(`${this.baseUrl}/api/trading/external/trades/${id}/cancel`, {}, { headers: { "x-cluster-api-key": this.clusterApiKey } });
|
|
558
|
+
yield this.axios.post(`${this.baseUrl}/api/trading/external/trades/${id}/cancel`, reason ? { reason } : {}, { headers: { "x-cluster-api-key": this.clusterApiKey } });
|
|
545
559
|
});
|
|
546
560
|
}
|
|
547
561
|
importExternalTrade(data) {
|
|
@@ -640,7 +654,7 @@ class ClusterClient {
|
|
|
640
654
|
return response.data;
|
|
641
655
|
});
|
|
642
656
|
}
|
|
643
|
-
placeExternalTrade({ provider, type, originCurrency, destinationCurrency, requestedOriginAmount, requestedDestinationAmount, limitPrice,
|
|
657
|
+
placeExternalTrade({ provider, type, originCurrency, destinationCurrency, requestedOriginAmount, requestedDestinationAmount, limitPrice, twoWaySettlementDateOffset, arbitrageSessionId, fxTradeId, timeInForce, valueDate, }) {
|
|
644
658
|
return __awaiter(this, void 0, void 0, function* () {
|
|
645
659
|
const response = yield this.axios.post(`${this.baseUrl}/api/trading/external/trades`, {
|
|
646
660
|
provider,
|
|
@@ -650,8 +664,11 @@ class ClusterClient {
|
|
|
650
664
|
requestedOriginAmount,
|
|
651
665
|
requestedDestinationAmount,
|
|
652
666
|
limitPrice,
|
|
653
|
-
|
|
667
|
+
twoWaySettlementDateOffset,
|
|
654
668
|
arbitrageSessionId,
|
|
669
|
+
fxTradeId,
|
|
670
|
+
timeInForce,
|
|
671
|
+
valueDate,
|
|
655
672
|
}, {
|
|
656
673
|
headers: {
|
|
657
674
|
"x-cluster-api-key": this.clusterApiKey,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Fiat, FXTradingSidePolicy, RioSettings, ResolvedFXPlacement, Side } from "@riocrypto/common";
|
|
2
|
+
/**
|
|
3
|
+
* Minimum trade size (in MXN) below which the Transnetwork bot will reject
|
|
4
|
+
* an order. Kept here because it's a hard operational constraint of the
|
|
5
|
+
* provider, not a per-policy admin setting.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TRANSNETWORK_MIN_MXN_AMOUNT = 1000000;
|
|
8
|
+
type RioSettingsForPolicy = Pick<RioSettings, "fxTradingPolicies">;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the active FX trading side-policy for a given (fiat, side, offset).
|
|
11
|
+
*
|
|
12
|
+
* Resolution order:
|
|
13
|
+
* 1. `fxTradingPolicies[fiat].byOffset[offset][side]` if defined
|
|
14
|
+
* 2. `fxTradingPolicies[fiat].default[side]` if defined
|
|
15
|
+
* 3. An empty policy that routes every trade to manual (Other) with no
|
|
16
|
+
* execution threshold. This is the safe default when no policy is
|
|
17
|
+
* configured for a (fiat, side) - the operator must explicitly opt in
|
|
18
|
+
* to any automated path.
|
|
19
|
+
*
|
|
20
|
+
* Per-side overrides are all-or-nothing: a configured override fully
|
|
21
|
+
* replaces the default; no field-level merging happens. Offsets without
|
|
22
|
+
* an explicit override always fall back to the default for that
|
|
23
|
+
* (fiat, side), regardless of how distant they are from any configured
|
|
24
|
+
* override.
|
|
25
|
+
*
|
|
26
|
+
* A caller that passes `undefined` (e.g. an order whose request body
|
|
27
|
+
* omitted twoWaySettlementType, which means "default / spot") is
|
|
28
|
+
* treated as T+0 so the same byOffset[0] override and provider
|
|
29
|
+
* eligibility apply as an explicit T+0 trade.
|
|
30
|
+
*/
|
|
31
|
+
export declare const getFXTradingPolicy: (rioSettings: RioSettingsForPolicy, fiat: Fiat, side: Side, twoWaySettlementDateOffset: number | undefined) => FXTradingSidePolicy;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the runtime placement for an FX trade given a side-policy and
|
|
34
|
+
* the trade's amount. Returns `{ provider, automatic, executionThresholdMs }`:
|
|
35
|
+
* - First the time range covering "now" in the currency's country-local
|
|
36
|
+
* timezone is selected. If no range covers the current minute,
|
|
37
|
+
* the trade falls back to manual (Other/automatic, threshold=0).
|
|
38
|
+
* - The matched time window's `executionThresholdMs` is always
|
|
39
|
+
* surfaced even when the inner amount-range lookup falls through to
|
|
40
|
+
* Other - aggregation behavior is a property of the time window, not
|
|
41
|
+
* of the band the trade landed in.
|
|
42
|
+
* - Within that time range, the first amount range matching the trade
|
|
43
|
+
* amount wins.
|
|
44
|
+
* - If the matched amount range's provider fails operational eligibility
|
|
45
|
+
* (e.g. Transnetwork below its 1M minimum, StoneX at T+0), falls back
|
|
46
|
+
* to manual (Other/automatic) while preserving the window's threshold.
|
|
47
|
+
* - If no amount range matches, same fallback (preserves threshold).
|
|
48
|
+
* - For providers that don't support cosigner approval (Matching, Other)
|
|
49
|
+
* the `automatic` flag is forced to true regardless of what is stored
|
|
50
|
+
* on the range - the dashboard renders the checkbox forced-on for
|
|
51
|
+
* these providers, but defense-in-depth on the runtime is cheap.
|
|
52
|
+
*
|
|
53
|
+
* `now` defaults to `new Date()` and is exposed so the cron sweep can
|
|
54
|
+
* pin a single timestamp across the whole batch.
|
|
55
|
+
*
|
|
56
|
+
* `Other` returning `automatic: true` is intentional: there is no
|
|
57
|
+
* cosigner gate for manual trades, so they immediately settle without
|
|
58
|
+
* waiting for approval.
|
|
59
|
+
*/
|
|
60
|
+
export declare const resolveFXProviderForAmount: (policy: FXTradingSidePolicy, fiat: Fiat, amountFiat: number, offset: number | undefined, now?: Date) => ResolvedFXPlacement;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveFXProviderForAmount = exports.getFXTradingPolicy = exports.TRANSNETWORK_MIN_MXN_AMOUNT = void 0;
|
|
4
|
+
const common_1 = require("@riocrypto/common");
|
|
5
|
+
/**
|
|
6
|
+
* Minimum trade size (in MXN) below which the Transnetwork bot will reject
|
|
7
|
+
* an order. Kept here because it's a hard operational constraint of the
|
|
8
|
+
* provider, not a per-policy admin setting.
|
|
9
|
+
*/
|
|
10
|
+
exports.TRANSNETWORK_MIN_MXN_AMOUNT = 1000000;
|
|
11
|
+
/**
|
|
12
|
+
* Settlement-offset constraints baked into provider eligibility. The
|
|
13
|
+
* Emarkets and Transnetwork bots only quote spot / very-short-dated MXN
|
|
14
|
+
* (T+0..T+2), and StoneX is RFQ/RFS for forward MXN (T+1+) and refuses
|
|
15
|
+
* same-day (T+0). Keep these in sync with
|
|
16
|
+
* `isProviderSelectableForFiatAndOffset` in the dashboard's
|
|
17
|
+
* fx-trading-policy-card so saved policies match what the runtime will
|
|
18
|
+
* actually pick.
|
|
19
|
+
*/
|
|
20
|
+
const SHORT_DATED_BOT_MAX_OFFSET = 2;
|
|
21
|
+
const STONEX_MIN_OFFSET = 1;
|
|
22
|
+
/**
|
|
23
|
+
* Providers that do not support a cosigner approval workflow. For these
|
|
24
|
+
* providers the `automatic` flag is meaningless and the runtime always
|
|
25
|
+
* treats the band as automatic, regardless of what is persisted on the
|
|
26
|
+
* policy. The dashboard mirrors this by rendering the checkbox forced-on
|
|
27
|
+
* and disabled for these providers.
|
|
28
|
+
*
|
|
29
|
+
* StoneX is intentionally NOT in this set: even though placement is RFQ
|
|
30
|
+
* (not fill-or-kill), internal-fx supports gating a StoneX placement
|
|
31
|
+
* behind the same cosigner webhook the bots use, so operators can
|
|
32
|
+
* require approval before an RFQ stream is opened.
|
|
33
|
+
*/
|
|
34
|
+
const NON_COSIGNER_PROVIDERS = new Set([
|
|
35
|
+
common_1.FXProvider.Matching,
|
|
36
|
+
common_1.FXProvider.Other,
|
|
37
|
+
]);
|
|
38
|
+
const FALLBACK_RESOLUTION = {
|
|
39
|
+
provider: common_1.FXProvider.Other,
|
|
40
|
+
automatic: true,
|
|
41
|
+
// 0ms threshold = place immediately. The Other path completes
|
|
42
|
+
// manually with no aggregation; surfacing a longer threshold here
|
|
43
|
+
// would just pin manual trades in PendingExecutionThreshold for no
|
|
44
|
+
// reason.
|
|
45
|
+
executionThresholdMs: 0,
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Defensive shape-check. Legacy persisted policies use a different shape
|
|
49
|
+
* (`providers[]` / `automaticTradeLimit`, or the pre-per-window-threshold
|
|
50
|
+
* shape that put `executionThresholdMs` on the side policy) - left in
|
|
51
|
+
* place by an out-of-date deploy, by an admin who hasn't saved the new
|
|
52
|
+
* editor yet, or by a typo'd manual DB edit. The resolver MUST NOT
|
|
53
|
+
* crash on that data; instead it treats those slots as "not configured"
|
|
54
|
+
* so the safe Other/automatic fallback applies and trades route to
|
|
55
|
+
* manual until the admin saves the new policy shape.
|
|
56
|
+
*/
|
|
57
|
+
const isWellFormedSidePolicy = (policy) => {
|
|
58
|
+
if (!policy || typeof policy !== "object")
|
|
59
|
+
return false;
|
|
60
|
+
const candidate = policy;
|
|
61
|
+
return Array.isArray(candidate.byTimeRange);
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Defensive shape-check for a single time range. The resolver tolerates
|
|
65
|
+
* a malformed entry by skipping it (so a malformed window doesn't take
|
|
66
|
+
* down the whole side). Note: we do NOT require contiguous coverage at
|
|
67
|
+
* the resolver layer - the rio-settings save route enforces that. Stale
|
|
68
|
+
* data with a gap still routes to manual via fallback.
|
|
69
|
+
*
|
|
70
|
+
* `executionThresholdMs` may be absent on legacy rows that were saved
|
|
71
|
+
* before the threshold moved per-window; we treat that as "place
|
|
72
|
+
* immediately" rather than rejecting the whole window, which matches
|
|
73
|
+
* the safer default and keeps existing routing rules intact for
|
|
74
|
+
* operators who haven't re-saved yet.
|
|
75
|
+
*/
|
|
76
|
+
const isWellFormedTimeRange = (range) => {
|
|
77
|
+
if (!range || typeof range !== "object")
|
|
78
|
+
return false;
|
|
79
|
+
const candidate = range;
|
|
80
|
+
return (typeof candidate.startMinute === "number" &&
|
|
81
|
+
typeof candidate.endMinute === "number" &&
|
|
82
|
+
Array.isArray(candidate.byAmountRange));
|
|
83
|
+
};
|
|
84
|
+
const safeExecutionThresholdMs = (range) => {
|
|
85
|
+
return typeof range.executionThresholdMs === "number" &&
|
|
86
|
+
Number.isFinite(range.executionThresholdMs) &&
|
|
87
|
+
range.executionThresholdMs >= 0
|
|
88
|
+
? range.executionThresholdMs
|
|
89
|
+
: 0;
|
|
90
|
+
};
|
|
91
|
+
const pickPolicyBySide = (policyBySide, side) => {
|
|
92
|
+
if (!policyBySide) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const candidate = policyBySide[side];
|
|
96
|
+
if (!isWellFormedSidePolicy(candidate)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return candidate;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the active FX trading side-policy for a given (fiat, side, offset).
|
|
103
|
+
*
|
|
104
|
+
* Resolution order:
|
|
105
|
+
* 1. `fxTradingPolicies[fiat].byOffset[offset][side]` if defined
|
|
106
|
+
* 2. `fxTradingPolicies[fiat].default[side]` if defined
|
|
107
|
+
* 3. An empty policy that routes every trade to manual (Other) with no
|
|
108
|
+
* execution threshold. This is the safe default when no policy is
|
|
109
|
+
* configured for a (fiat, side) - the operator must explicitly opt in
|
|
110
|
+
* to any automated path.
|
|
111
|
+
*
|
|
112
|
+
* Per-side overrides are all-or-nothing: a configured override fully
|
|
113
|
+
* replaces the default; no field-level merging happens. Offsets without
|
|
114
|
+
* an explicit override always fall back to the default for that
|
|
115
|
+
* (fiat, side), regardless of how distant they are from any configured
|
|
116
|
+
* override.
|
|
117
|
+
*
|
|
118
|
+
* A caller that passes `undefined` (e.g. an order whose request body
|
|
119
|
+
* omitted twoWaySettlementType, which means "default / spot") is
|
|
120
|
+
* treated as T+0 so the same byOffset[0] override and provider
|
|
121
|
+
* eligibility apply as an explicit T+0 trade.
|
|
122
|
+
*/
|
|
123
|
+
const getFXTradingPolicy = (rioSettings, fiat, side, twoWaySettlementDateOffset) => {
|
|
124
|
+
var _a;
|
|
125
|
+
const effectiveOffset = twoWaySettlementDateOffset !== null && twoWaySettlementDateOffset !== void 0 ? twoWaySettlementDateOffset : 0;
|
|
126
|
+
const fiatPolicies = (_a = rioSettings.fxTradingPolicies) === null || _a === void 0 ? void 0 : _a[fiat];
|
|
127
|
+
if (fiatPolicies) {
|
|
128
|
+
if (fiatPolicies.byOffset) {
|
|
129
|
+
const offsetPolicy = pickPolicyBySide(fiatPolicies.byOffset[effectiveOffset], side);
|
|
130
|
+
if (offsetPolicy) {
|
|
131
|
+
return offsetPolicy;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const defaultPolicy = pickPolicyBySide(fiatPolicies.default, side);
|
|
135
|
+
if (defaultPolicy) {
|
|
136
|
+
return defaultPolicy;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { byTimeRange: [] };
|
|
140
|
+
};
|
|
141
|
+
exports.getFXTradingPolicy = getFXTradingPolicy;
|
|
142
|
+
/**
|
|
143
|
+
* Returns true when the given provider can operationally handle the trade.
|
|
144
|
+
* These are hard provider-side constraints, not admin-configurable.
|
|
145
|
+
*
|
|
146
|
+
* `offset === undefined` is interpreted as T+0 (the system convention:
|
|
147
|
+
* "no settlement type specified" == "default" == spot), matching
|
|
148
|
+
* `getFXTradingPolicy`. As a result Emarkets / Transnetwork stay eligible
|
|
149
|
+
* for undefined-offset callers but StoneX (which refuses T+0) does not.
|
|
150
|
+
*/
|
|
151
|
+
const isProviderEligible = (provider, fiat, amountFiat, offset) => {
|
|
152
|
+
if (provider === common_1.FXProvider.StoneX ||
|
|
153
|
+
provider === common_1.FXProvider.Emarkets ||
|
|
154
|
+
provider === common_1.FXProvider.Transnetwork) {
|
|
155
|
+
if (fiat !== common_1.Fiat.MXN)
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (provider === common_1.FXProvider.Transnetwork) {
|
|
159
|
+
if (amountFiat === undefined || amountFiat < exports.TRANSNETWORK_MIN_MXN_AMOUNT) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const effectiveOffset = offset !== null && offset !== void 0 ? offset : 0;
|
|
164
|
+
if ((provider === common_1.FXProvider.Emarkets ||
|
|
165
|
+
provider === common_1.FXProvider.Transnetwork) &&
|
|
166
|
+
effectiveOffset > SHORT_DATED_BOT_MAX_OFFSET) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (provider === common_1.FXProvider.StoneX && effectiveOffset < STONEX_MIN_OFFSET) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Returns true if `amount` falls inside `[range.minAmount, range.maxAmount)`.
|
|
176
|
+
* Both bounds are required by the type, but the resolver defensively
|
|
177
|
+
* treats a missing/non-finite `maxAmount` (legacy data, manual db edit)
|
|
178
|
+
* as a non-match so the trade falls through to the safe Other/manual
|
|
179
|
+
* fallback rather than silently inheriting a previous "open-ended"
|
|
180
|
+
* interpretation.
|
|
181
|
+
*/
|
|
182
|
+
const rangeMatchesAmount = (range, amount) => {
|
|
183
|
+
const min = Math.max(range.minAmount, 0);
|
|
184
|
+
if (amount < min)
|
|
185
|
+
return false;
|
|
186
|
+
if (typeof range.maxAmount !== "number" || !Number.isFinite(range.maxAmount)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return amount < range.maxAmount;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Returns true if `minuteOfDay` (0..1439, in the time range's reference
|
|
193
|
+
* timezone) falls inside `[range.startMinute, range.endMinute)`.
|
|
194
|
+
*/
|
|
195
|
+
const rangeMatchesMinute = (range, minuteOfDay) => {
|
|
196
|
+
if (minuteOfDay < range.startMinute)
|
|
197
|
+
return false;
|
|
198
|
+
return minuteOfDay < range.endMinute;
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Returns the current minute-of-day in the given IANA timezone, in the
|
|
202
|
+
* range [0, 1440). Uses `Intl.DateTimeFormat` so DST transitions are
|
|
203
|
+
* applied automatically by the platform - no manual offset bookkeeping.
|
|
204
|
+
*
|
|
205
|
+
* `now` is injectable for testability and so a single cron tick can
|
|
206
|
+
* snapshot the time once and reuse it across all FX trades being swept.
|
|
207
|
+
*/
|
|
208
|
+
const getMinuteOfDayInTimezone = (now, timezone) => {
|
|
209
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
210
|
+
timeZone: timezone,
|
|
211
|
+
hour12: false,
|
|
212
|
+
hour: "2-digit",
|
|
213
|
+
minute: "2-digit",
|
|
214
|
+
}).formatToParts(now);
|
|
215
|
+
let hour = 0;
|
|
216
|
+
let minute = 0;
|
|
217
|
+
for (const part of parts) {
|
|
218
|
+
if (part.type === "hour")
|
|
219
|
+
hour = parseInt(part.value, 10);
|
|
220
|
+
if (part.type === "minute")
|
|
221
|
+
minute = parseInt(part.value, 10);
|
|
222
|
+
}
|
|
223
|
+
// Intl formats midnight as "24" in some locales/Node versions; clamp
|
|
224
|
+
// defensively so we never return 1440 (which would miss the [0, 1440)
|
|
225
|
+
// last range and fall through to manual).
|
|
226
|
+
if (hour === 24)
|
|
227
|
+
hour = 0;
|
|
228
|
+
return hour * 60 + minute;
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* Resolve the runtime placement for an FX trade given a side-policy and
|
|
232
|
+
* the trade's amount. Returns `{ provider, automatic, executionThresholdMs }`:
|
|
233
|
+
* - First the time range covering "now" in the currency's country-local
|
|
234
|
+
* timezone is selected. If no range covers the current minute,
|
|
235
|
+
* the trade falls back to manual (Other/automatic, threshold=0).
|
|
236
|
+
* - The matched time window's `executionThresholdMs` is always
|
|
237
|
+
* surfaced even when the inner amount-range lookup falls through to
|
|
238
|
+
* Other - aggregation behavior is a property of the time window, not
|
|
239
|
+
* of the band the trade landed in.
|
|
240
|
+
* - Within that time range, the first amount range matching the trade
|
|
241
|
+
* amount wins.
|
|
242
|
+
* - If the matched amount range's provider fails operational eligibility
|
|
243
|
+
* (e.g. Transnetwork below its 1M minimum, StoneX at T+0), falls back
|
|
244
|
+
* to manual (Other/automatic) while preserving the window's threshold.
|
|
245
|
+
* - If no amount range matches, same fallback (preserves threshold).
|
|
246
|
+
* - For providers that don't support cosigner approval (Matching, Other)
|
|
247
|
+
* the `automatic` flag is forced to true regardless of what is stored
|
|
248
|
+
* on the range - the dashboard renders the checkbox forced-on for
|
|
249
|
+
* these providers, but defense-in-depth on the runtime is cheap.
|
|
250
|
+
*
|
|
251
|
+
* `now` defaults to `new Date()` and is exposed so the cron sweep can
|
|
252
|
+
* pin a single timestamp across the whole batch.
|
|
253
|
+
*
|
|
254
|
+
* `Other` returning `automatic: true` is intentional: there is no
|
|
255
|
+
* cosigner gate for manual trades, so they immediately settle without
|
|
256
|
+
* waiting for approval.
|
|
257
|
+
*/
|
|
258
|
+
const resolveFXProviderForAmount = (policy, fiat, amountFiat, offset, now = new Date()) => {
|
|
259
|
+
const country = (0, common_1.getCountryForFiat)(fiat);
|
|
260
|
+
const timezone = (0, common_1.getCountryTimezone)(country);
|
|
261
|
+
const minuteOfDay = getMinuteOfDayInTimezone(now, timezone);
|
|
262
|
+
const matchedTimeRange = policy.byTimeRange.find((range) => isWellFormedTimeRange(range) && rangeMatchesMinute(range, minuteOfDay));
|
|
263
|
+
if (!matchedTimeRange) {
|
|
264
|
+
return FALLBACK_RESOLUTION;
|
|
265
|
+
}
|
|
266
|
+
// Threshold for the active time window. Always returned (even when
|
|
267
|
+
// we fall through to Other below) so callers can decide aggregation
|
|
268
|
+
// based on the window the trade is currently in, not the window when
|
|
269
|
+
// it was created.
|
|
270
|
+
const executionThresholdMs = safeExecutionThresholdMs(matchedTimeRange);
|
|
271
|
+
const matchedAmountRange = matchedTimeRange.byAmountRange.find((range) => rangeMatchesAmount(range, amountFiat));
|
|
272
|
+
if (!matchedAmountRange) {
|
|
273
|
+
return Object.assign(Object.assign({}, FALLBACK_RESOLUTION), { executionThresholdMs });
|
|
274
|
+
}
|
|
275
|
+
if (!isProviderEligible(matchedAmountRange.provider, fiat, amountFiat, offset)) {
|
|
276
|
+
return Object.assign(Object.assign({}, FALLBACK_RESOLUTION), { executionThresholdMs });
|
|
277
|
+
}
|
|
278
|
+
const automatic = NON_COSIGNER_PROVIDERS.has(matchedAmountRange.provider)
|
|
279
|
+
? true
|
|
280
|
+
: matchedAmountRange.automatic;
|
|
281
|
+
return {
|
|
282
|
+
provider: matchedAmountRange.provider,
|
|
283
|
+
automatic,
|
|
284
|
+
executionThresholdMs,
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
exports.resolveFXProviderForAmount = resolveFXProviderForAmount;
|
package/build/index.d.ts
CHANGED
|
@@ -171,3 +171,4 @@ export * from "./helpers/interpolate-forward-curve-rate";
|
|
|
171
171
|
export * from "./helpers/find-user-tier-forward-curve-rate";
|
|
172
172
|
export * from "./helpers/resolve-forward-curve-rate";
|
|
173
173
|
export * from "./helpers/get-resolved-forward-curve-tiers";
|
|
174
|
+
export * from "./helpers/get-fx-trading-policy";
|
package/build/index.js
CHANGED
|
@@ -187,3 +187,4 @@ __exportStar(require("./helpers/interpolate-forward-curve-rate"), exports);
|
|
|
187
187
|
__exportStar(require("./helpers/find-user-tier-forward-curve-rate"), exports);
|
|
188
188
|
__exportStar(require("./helpers/resolve-forward-curve-rate"), exports);
|
|
189
189
|
__exportStar(require("./helpers/get-resolved-forward-curve-tiers"), exports);
|
|
190
|
+
__exportStar(require("./helpers/get-fx-trading-policy"), exports);
|
|
@@ -3,8 +3,7 @@ import { Mongoose, Model, Document, HydratedDocument } from "mongoose";
|
|
|
3
3
|
interface EmarketsFXTradeAttrs {
|
|
4
4
|
createdAt: Date;
|
|
5
5
|
status: EmarketsFXTradeStatus;
|
|
6
|
-
|
|
7
|
-
isExternalTrade: boolean;
|
|
6
|
+
externalTradeId: string;
|
|
8
7
|
side: Side;
|
|
9
8
|
fiat: Fiat;
|
|
10
9
|
amountToTrade: number;
|
|
@@ -20,8 +19,7 @@ interface EmarketsFXTradeDoc extends Document {
|
|
|
20
19
|
createdAt: Date;
|
|
21
20
|
side: Side;
|
|
22
21
|
status: EmarketsFXTradeStatus;
|
|
23
|
-
|
|
24
|
-
isExternalTrade: boolean;
|
|
22
|
+
externalTradeId: string;
|
|
25
23
|
fiat: Fiat;
|
|
26
24
|
amountToTrade: number;
|
|
27
25
|
amountReceived?: number;
|
|
@@ -18,6 +18,7 @@ interface ExternalTradeAttrs {
|
|
|
18
18
|
actualPrice?: number;
|
|
19
19
|
associatedMidmarketPrice?: number;
|
|
20
20
|
status: ExternalTradeStatus;
|
|
21
|
+
fxTradeId?: string;
|
|
21
22
|
providerOptions?: {
|
|
22
23
|
emarkets?: {
|
|
23
24
|
settlementType?: EmarketsSettlementType;
|
|
@@ -58,6 +59,7 @@ interface ExternalTradeDoc extends Document {
|
|
|
58
59
|
actualPrice?: number;
|
|
59
60
|
associatedMidmarketPrice?: number;
|
|
60
61
|
status: ExternalTradeStatus;
|
|
62
|
+
fxTradeId?: string;
|
|
61
63
|
providerOptions?: {
|
|
62
64
|
emarkets?: {
|
|
63
65
|
settlementType?: EmarketsSettlementType;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fiat, FXProvider, FXTradeStatus, Side, Crypto,
|
|
1
|
+
import { Fiat, FXProvider, FXTradeStatus, Side, Crypto, ExternalTradeStatus } from "@riocrypto/common";
|
|
2
2
|
import { Mongoose, Model, Document, HydratedDocument } from "mongoose";
|
|
3
3
|
interface FXTradeAttrs {
|
|
4
4
|
createdAt: Date;
|
|
@@ -6,17 +6,19 @@ interface FXTradeAttrs {
|
|
|
6
6
|
orderIds: string[];
|
|
7
7
|
amount: number;
|
|
8
8
|
fiat: Fiat;
|
|
9
|
-
settlementType?: EmarketsSettlementType | TransnetworkSettlementType;
|
|
10
9
|
averageOfferedPrice: number;
|
|
11
10
|
amountTraded: number;
|
|
12
11
|
price?: number;
|
|
13
12
|
provider: FXProvider;
|
|
13
|
+
twoWaySettlementDate: Date;
|
|
14
|
+
twoWaySettlementDateOffset: number;
|
|
14
15
|
executedTrades: {
|
|
15
16
|
amount: number;
|
|
16
17
|
provider: FXProvider;
|
|
17
|
-
price
|
|
18
|
+
price?: number;
|
|
19
|
+
status: ExternalTradeStatus;
|
|
18
20
|
matchedTradeId?: string;
|
|
19
|
-
|
|
21
|
+
externalTradeId?: string;
|
|
20
22
|
}[];
|
|
21
23
|
isRunning?: boolean;
|
|
22
24
|
status: FXTradeStatus;
|
|
@@ -29,16 +31,18 @@ interface FXTradeDoc extends Document {
|
|
|
29
31
|
amount: number;
|
|
30
32
|
fiat: Fiat;
|
|
31
33
|
amountTraded: number;
|
|
32
|
-
settlementType?: EmarketsSettlementType | TransnetworkSettlementType;
|
|
33
34
|
averageOfferedPrice: number;
|
|
34
35
|
price?: number;
|
|
35
36
|
provider: FXProvider;
|
|
37
|
+
twoWaySettlementDate: Date;
|
|
38
|
+
twoWaySettlementDateOffset: number;
|
|
36
39
|
executedTrades: {
|
|
37
40
|
amount: number;
|
|
38
41
|
provider: FXProvider;
|
|
39
|
-
price
|
|
42
|
+
price?: number;
|
|
43
|
+
status: ExternalTradeStatus;
|
|
40
44
|
matchedTradeId?: string;
|
|
41
|
-
|
|
45
|
+
externalTradeId?: string;
|
|
42
46
|
}[];
|
|
43
47
|
status: FXTradeStatus;
|
|
44
48
|
isRunning?: boolean;
|
package/build/models/fx-trade.js
CHANGED
|
@@ -27,8 +27,11 @@ const buildFXTrade = (mongoose) => {
|
|
|
27
27
|
type: String,
|
|
28
28
|
required: true,
|
|
29
29
|
},
|
|
30
|
-
|
|
31
|
-
type:
|
|
30
|
+
twoWaySettlementDate: {
|
|
31
|
+
type: mongoose.Schema.Types.Date,
|
|
32
|
+
},
|
|
33
|
+
twoWaySettlementDateOffset: {
|
|
34
|
+
type: Number,
|
|
32
35
|
},
|
|
33
36
|
amountTraded: {
|
|
34
37
|
type: Number,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Country, DeferredPaymentType, EmarketsSettlementType, Fiat, FXProvider, Processor, Side, TransnetworkSettlementType } from "@riocrypto/common";
|
|
1
|
+
import { Country, DeferredPaymentType, EmarketsSettlementType, Fiat, FXProvider, FXTradingPolicies, Processor, Side, TransnetworkSettlementType } from "@riocrypto/common";
|
|
2
2
|
import { TVFXDataProvider } from "@riocrypto/common";
|
|
3
3
|
import mongoose, { HydratedDocument } from "mongoose";
|
|
4
4
|
interface RioSettingsAttrs {
|
|
@@ -28,12 +28,6 @@ interface RioSettingsAttrs {
|
|
|
28
28
|
sellOrderGeneralExchangeRateMarkup: {
|
|
29
29
|
[key in Fiat]: number;
|
|
30
30
|
};
|
|
31
|
-
buyOrderAutomaticFXTradeLimit: {
|
|
32
|
-
[key in Fiat]: number;
|
|
33
|
-
};
|
|
34
|
-
sellOrderAutomaticFXTradeLimit: {
|
|
35
|
-
[key in Fiat]: number;
|
|
36
|
-
};
|
|
37
31
|
defaultServiceFee: {
|
|
38
32
|
[key in Country]?: {
|
|
39
33
|
[key in Side]?: number;
|
|
@@ -51,7 +45,7 @@ interface RioSettingsAttrs {
|
|
|
51
45
|
};
|
|
52
46
|
};
|
|
53
47
|
};
|
|
54
|
-
|
|
48
|
+
fxTradingPolicies?: FXTradingPolicies;
|
|
55
49
|
activeFiatPaymentProcessors: {
|
|
56
50
|
[Side.Buy]: Processor[];
|
|
57
51
|
[Side.Sell]: Processor[];
|
|
@@ -117,12 +111,6 @@ interface RioSettingsDoc extends mongoose.Document {
|
|
|
117
111
|
sellOrderAfterHoursExchangeRateMarkup: {
|
|
118
112
|
[key in Fiat]: number;
|
|
119
113
|
};
|
|
120
|
-
buyOrderAutomaticFXTradeLimit: {
|
|
121
|
-
[key in Fiat]: number;
|
|
122
|
-
};
|
|
123
|
-
sellOrderAutomaticFXTradeLimit: {
|
|
124
|
-
[key in Fiat]: number;
|
|
125
|
-
};
|
|
126
114
|
additionalBuyOrderLiquidityUSD?: number;
|
|
127
115
|
additionalSellOrderLiquidityUSD?: number;
|
|
128
116
|
previousDayUSDMXNConversionRate?: number;
|
|
@@ -140,7 +128,7 @@ interface RioSettingsDoc extends mongoose.Document {
|
|
|
140
128
|
};
|
|
141
129
|
};
|
|
142
130
|
};
|
|
143
|
-
|
|
131
|
+
fxTradingPolicies?: FXTradingPolicies;
|
|
144
132
|
activeFiatPaymentProcessors: {
|
|
145
133
|
[Side.Buy]: Processor[];
|
|
146
134
|
[Side.Sell]: Processor[];
|
|
@@ -15,12 +15,6 @@ const buildRioSettings = (mongoose) => {
|
|
|
15
15
|
defaultDeferredPaymentFee: {
|
|
16
16
|
type: Object,
|
|
17
17
|
},
|
|
18
|
-
buyOrderAutomaticFXTradeLimit: {
|
|
19
|
-
type: Object,
|
|
20
|
-
},
|
|
21
|
-
sellOrderAutomaticFXTradeLimit: {
|
|
22
|
-
type: Object,
|
|
23
|
-
},
|
|
24
18
|
isAfterHours: {
|
|
25
19
|
type: Object,
|
|
26
20
|
},
|
|
@@ -48,8 +42,8 @@ const buildRioSettings = (mongoose) => {
|
|
|
48
42
|
fxProviders: {
|
|
49
43
|
type: Object,
|
|
50
44
|
},
|
|
51
|
-
|
|
52
|
-
type:
|
|
45
|
+
fxTradingPolicies: {
|
|
46
|
+
type: Object,
|
|
53
47
|
},
|
|
54
48
|
defaultServiceFee: {
|
|
55
49
|
type: Object,
|
|
@@ -3,8 +3,7 @@ import { Mongoose, Model, Document, HydratedDocument } from "mongoose";
|
|
|
3
3
|
interface TransnetworkFXTradeAttrs {
|
|
4
4
|
createdAt: Date;
|
|
5
5
|
status: TransnetworkFXTradeStatus;
|
|
6
|
-
|
|
7
|
-
isExternalTrade: boolean;
|
|
6
|
+
externalTradeId?: string;
|
|
8
7
|
side: Side;
|
|
9
8
|
fiat: Fiat;
|
|
10
9
|
amountToTrade: number;
|
|
@@ -21,8 +20,7 @@ interface TransnetworkFXTradeDoc extends Document {
|
|
|
21
20
|
createdAt: Date;
|
|
22
21
|
side: Side;
|
|
23
22
|
status: TransnetworkFXTradeStatus;
|
|
24
|
-
|
|
25
|
-
isExternalTrade: boolean;
|
|
23
|
+
externalTradeId?: string;
|
|
26
24
|
fiat: Fiat;
|
|
27
25
|
amountToTrade: number;
|
|
28
26
|
amountReceived?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riocrypto/common-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2783",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@google-cloud/secret-manager": "^5.6.0",
|
|
25
25
|
"@google-cloud/storage": "^7.19.0",
|
|
26
26
|
"@hyperdx/node-opentelemetry": "^0.10.3",
|
|
27
|
-
"@riocrypto/common": "1.0.
|
|
27
|
+
"@riocrypto/common": "1.0.2587",
|
|
28
28
|
"@slack/web-api": "^7.15.0",
|
|
29
29
|
"@types/express": "^4.17.25",
|
|
30
30
|
"axios": "1.13.6",
|