@vesper85/strategy-sdk 0.1.0 → 0.1.2
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/README.md +202 -213
- package/dist/clients/polymarket-client.d.ts +0 -16
- package/dist/engine/event-runner.d.ts +33 -3
- package/dist/engine/polymarket-event-runner.d.ts +202 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +661 -176
- package/dist/types/event-types.d.ts +111 -11
- package/dist/types/osiris.d.ts +2 -39
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { PolymarketGammaClient } from 'polymarket-gamma';
|
|
2
2
|
import { ClobClient, Side } from '@polymarket/clob-client';
|
|
3
|
-
import {
|
|
3
|
+
import { ethers } from 'ethers';
|
|
4
4
|
import axios from 'axios';
|
|
5
5
|
import { serializeSignature, createWalletClient, http } from 'viem';
|
|
6
6
|
import { connectMCPServer, removeConnection } from '@osiris-ai/agent-sdk';
|
|
7
7
|
import { Hyperliquid } from 'hyperliquid';
|
|
8
8
|
import { createClient } from 'redis';
|
|
9
9
|
import superjson from 'superjson';
|
|
10
|
-
import { TechnicalAnalysisService } from '@
|
|
10
|
+
import { TechnicalAnalysisService } from '@osiris-ai/technical-indicators';
|
|
11
11
|
import WebSocket from 'ws';
|
|
12
12
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
13
13
|
import { sepolia, base, optimism, arbitrum, polygon, mainnet } from 'viem/chains';
|
|
14
14
|
|
|
15
15
|
// @osiris-ai/strategy-sdk - Strategy SDK for Trading
|
|
16
16
|
|
|
17
|
+
// src/types/event-types.ts
|
|
18
|
+
function isMarketSubscription(sub) {
|
|
19
|
+
return sub.type === "price" || sub.type === "orderbook" || sub.type === "trade" || sub.type === "fill";
|
|
20
|
+
}
|
|
21
|
+
function isWalletSubscription(sub) {
|
|
22
|
+
return sub.type === "wallet";
|
|
23
|
+
}
|
|
24
|
+
function isOpportunitySubscription(sub) {
|
|
25
|
+
return sub.type === "opportunity";
|
|
26
|
+
}
|
|
27
|
+
function isCustomSubscription(sub) {
|
|
28
|
+
return sub.type === "custom";
|
|
29
|
+
}
|
|
17
30
|
var OsirisSigner = class {
|
|
18
31
|
hubBaseUrl;
|
|
19
32
|
accessToken;
|
|
@@ -148,13 +161,14 @@ var OsirisSigner = class {
|
|
|
148
161
|
return void 0;
|
|
149
162
|
}
|
|
150
163
|
};
|
|
151
|
-
var MaxUint256BigInt = BigInt(MaxUint256.toString());
|
|
164
|
+
var MaxUint256BigInt = typeof ethers.constants !== "undefined" && ethers.constants.MaxUint256 ? BigInt(ethers.MaxUint256.toString()) : BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
|
152
165
|
var POLYGON_CHAIN_ID = 137;
|
|
153
166
|
var USDC_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
|
|
154
167
|
var CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
|
|
155
168
|
var CTF_EXCHANGE_ADDRESS = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E";
|
|
156
169
|
var DEFAULT_CLOB_HOST = "https://clob.polymarket.com";
|
|
157
170
|
var POLYGON_RPC_URL = "https://polygon-rpc.com";
|
|
171
|
+
process.env.MCP_GATEWAY_URL ? `${process.env.MCP_GATEWAY_URL}/@osiris/polymarket/mcp` : "https://gateway.backend.osirislabs.xyz/@osiris/polymarket/mcp";
|
|
158
172
|
var ERC20_ABI = [
|
|
159
173
|
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
160
174
|
"function allowance(address owner, address spender) external view returns (uint256)",
|
|
@@ -234,7 +248,6 @@ var PolymarketClient = class {
|
|
|
234
248
|
this.options.clobHost,
|
|
235
249
|
this.options.chainId,
|
|
236
250
|
this.wallet
|
|
237
|
-
// Cast to any for CLOB client v5 compatibility
|
|
238
251
|
);
|
|
239
252
|
this.logger.info("Deriving API credentials...");
|
|
240
253
|
const apiCreds = await tempClient.createOrDeriveApiKey();
|
|
@@ -243,7 +256,6 @@ var PolymarketClient = class {
|
|
|
243
256
|
this.options.clobHost,
|
|
244
257
|
this.options.chainId,
|
|
245
258
|
this.wallet,
|
|
246
|
-
// Cast to any for CLOB client v5 compatibility
|
|
247
259
|
apiCreds,
|
|
248
260
|
signatureType
|
|
249
261
|
);
|
|
@@ -568,7 +580,7 @@ var PolymarketClient = class {
|
|
|
568
580
|
const allowanceResult = await this.callMCPTool("check_allowance", {
|
|
569
581
|
token: tokenType
|
|
570
582
|
});
|
|
571
|
-
|
|
583
|
+
console.log("allowanceResult", allowanceResult);
|
|
572
584
|
const allowanceContent = this.extractMCPContent(allowanceResult);
|
|
573
585
|
const contractMatches = allowanceContent?.match(/0x[a-fA-F0-9]{40}:\s*(true|false)/gi);
|
|
574
586
|
let hasFullAllowance = true;
|
|
@@ -661,18 +673,6 @@ var PolymarketClient = class {
|
|
|
661
673
|
ctfWasAlreadyApproved: false
|
|
662
674
|
};
|
|
663
675
|
try {
|
|
664
|
-
const address = this.userAddress;
|
|
665
|
-
if (!address) {
|
|
666
|
-
throw new Error("userAddress is required for MCP approvals");
|
|
667
|
-
}
|
|
668
|
-
const chooseWalletResult = await this.callMCPTool("choose_wallet", {
|
|
669
|
-
address
|
|
670
|
-
});
|
|
671
|
-
const walletError = this.detectMCPError(chooseWalletResult);
|
|
672
|
-
if (walletError) {
|
|
673
|
-
this.logger.error(`[MCP] Choose wallet failed: ${walletError}`);
|
|
674
|
-
throw new Error(`Choose wallet failed: ${walletError}`);
|
|
675
|
-
}
|
|
676
676
|
const usdcHasAllowance = await this.checkAllowanceViaMCP("USDC");
|
|
677
677
|
if (usdcHasAllowance) {
|
|
678
678
|
result.usdcWasAlreadyApproved = true;
|
|
@@ -753,16 +753,13 @@ var PolymarketClient = class {
|
|
|
753
753
|
if (!address) {
|
|
754
754
|
throw new Error("userAddress is required for MCP order placement");
|
|
755
755
|
}
|
|
756
|
-
this.logger.info(`
|
|
757
|
-
this.logger.debug(`[MCP] Using wallet address: ${address}`);
|
|
758
|
-
this.logger.debug(`[MCP] Calling choose_wallet for address: ${address}`);
|
|
756
|
+
this.logger.info(`Creating limit order via MCP: ${params.side} ${params.size} @ ${params.price}`);
|
|
759
757
|
const chooseWalletResult = await this.callMCPTool("choose_wallet", {
|
|
760
758
|
address
|
|
761
759
|
});
|
|
762
|
-
this.logger.debug(`[MCP] choose_wallet result: ${JSON.stringify(chooseWalletResult, null, 2)}`);
|
|
763
760
|
const walletError = this.detectMCPError(chooseWalletResult);
|
|
764
761
|
if (walletError) {
|
|
765
|
-
this.logger.error(`
|
|
762
|
+
this.logger.error(`Choose wallet failed: ${walletError}`);
|
|
766
763
|
return {
|
|
767
764
|
success: false,
|
|
768
765
|
errorMsg: walletError
|
|
@@ -775,7 +772,7 @@ var PolymarketClient = class {
|
|
|
775
772
|
errorMsg: `Failed to approve ${params.side === "BUY" ? "USDC" : "Conditional Token"}. Please try again.`
|
|
776
773
|
};
|
|
777
774
|
}
|
|
778
|
-
const
|
|
775
|
+
const orderResult = await this.callMCPTool("create_and_post_order", {
|
|
779
776
|
userOrder: {
|
|
780
777
|
tokenID: params.tokenId,
|
|
781
778
|
side: params.side,
|
|
@@ -784,11 +781,7 @@ var PolymarketClient = class {
|
|
|
784
781
|
},
|
|
785
782
|
orderType: params.expiration ? "GTD" : "GTC",
|
|
786
783
|
deferExec: false
|
|
787
|
-
};
|
|
788
|
-
this.logger.debug(`[MCP] Limit order params:`, orderParams);
|
|
789
|
-
this.logger.info(`[MCP] Submitting limit order: ${params.side} ${params.size} @ ${params.price} for token ${params.tokenId.substring(0, 20)}...`);
|
|
790
|
-
const orderResult = await this.callMCPTool("create_and_post_order", orderParams);
|
|
791
|
-
this.logger.debug(`[MCP] Raw limit order result: ${JSON.stringify(orderResult, null, 2)}`);
|
|
784
|
+
});
|
|
792
785
|
const orderError = this.detectMCPError(orderResult);
|
|
793
786
|
if (orderError) {
|
|
794
787
|
this.logger.error(`Limit order via MCP failed: ${orderError}`);
|
|
@@ -799,13 +792,13 @@ var PolymarketClient = class {
|
|
|
799
792
|
}
|
|
800
793
|
const orderDetails = this.extractOrderDetails(orderResult);
|
|
801
794
|
if (!orderDetails) {
|
|
802
|
-
this.logger.error(
|
|
795
|
+
this.logger.error("Failed to extract order details from MCP response");
|
|
803
796
|
return {
|
|
804
797
|
success: false,
|
|
805
798
|
errorMsg: "Failed to extract order details from MCP response"
|
|
806
799
|
};
|
|
807
800
|
}
|
|
808
|
-
this.logger.info(`
|
|
801
|
+
this.logger.info(`Limit order via MCP created: ${orderDetails.orderID}`);
|
|
809
802
|
return {
|
|
810
803
|
success: true,
|
|
811
804
|
orderId: orderDetails.orderID,
|
|
@@ -866,14 +859,10 @@ var PolymarketClient = class {
|
|
|
866
859
|
if (!address) {
|
|
867
860
|
throw new Error("userAddress is required for MCP order placement");
|
|
868
861
|
}
|
|
869
|
-
this.logger.info(`
|
|
870
|
-
this.logger.debug(`[MCP] Using wallet address: ${address}`);
|
|
871
|
-
this.logger.debug(`[MCP] Order details: tokenId=${params.tokenId}, side=${params.side}, amount=${params.amount}`);
|
|
872
|
-
this.logger.debug(`[MCP] Calling choose_wallet for address: ${address}`);
|
|
862
|
+
this.logger.info(`Creating market order via MCP: ${params.side} ${params.amount}`);
|
|
873
863
|
const chooseWalletResult = await this.callMCPTool("choose_wallet", {
|
|
874
864
|
address
|
|
875
865
|
});
|
|
876
|
-
this.logger.debug(`[MCP] choose_wallet result: ${JSON.stringify(chooseWalletResult, null, 2)}`);
|
|
877
866
|
const walletError = this.detectMCPError(chooseWalletResult);
|
|
878
867
|
if (walletError) {
|
|
879
868
|
this.logger.error(`Choose wallet failed: ${walletError}`);
|
|
@@ -889,19 +878,16 @@ var PolymarketClient = class {
|
|
|
889
878
|
errorMsg: `Failed to approve ${params.side === "BUY" ? "USDC" : "Conditional Token"}. Please try again.`
|
|
890
879
|
};
|
|
891
880
|
}
|
|
892
|
-
const
|
|
881
|
+
const orderResult = await this.callMCPTool("create_and_post_market_order", {
|
|
893
882
|
userMarketOrder: {
|
|
894
883
|
tokenID: params.tokenId,
|
|
895
884
|
side: params.side,
|
|
896
|
-
amount: params.amount
|
|
885
|
+
amount: params.amount,
|
|
886
|
+
orderType: params.slippageTolerance ? "FOK" : "FOK"
|
|
887
|
+
// Fill or Kill
|
|
897
888
|
},
|
|
898
889
|
orderType: "FOK"
|
|
899
|
-
|
|
900
|
-
};
|
|
901
|
-
this.logger.debug(`[MCP] Market order params:`, orderParams);
|
|
902
|
-
this.logger.info(`[MCP] Submitting market order: ${params.side} ${params.amount} USDC for token ${params.tokenId.substring(0, 20)}...`);
|
|
903
|
-
const orderResult = await this.callMCPTool("create_and_post_market_order", orderParams);
|
|
904
|
-
this.logger.debug(`[MCP] Raw order result: ${JSON.stringify(orderResult, null, 2)}`);
|
|
890
|
+
});
|
|
905
891
|
const orderError = this.detectMCPError(orderResult);
|
|
906
892
|
if (orderError) {
|
|
907
893
|
this.logger.error(`Market order via MCP failed: ${orderError}`);
|
|
@@ -912,7 +898,7 @@ var PolymarketClient = class {
|
|
|
912
898
|
}
|
|
913
899
|
const orderDetails = this.extractOrderDetails(orderResult);
|
|
914
900
|
if (!orderDetails) {
|
|
915
|
-
this.logger.error(
|
|
901
|
+
this.logger.error("Failed to extract order details from MCP response");
|
|
916
902
|
return {
|
|
917
903
|
success: false,
|
|
918
904
|
errorMsg: "Failed to extract order details from MCP response"
|
|
@@ -1027,9 +1013,8 @@ var PolymarketClient = class {
|
|
|
1027
1013
|
// ============================================
|
|
1028
1014
|
async buy(tokenId, size) {
|
|
1029
1015
|
if (!this.tradingInitialized) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
);
|
|
1016
|
+
this.logger.warning(`[Dummy] Buying ${size} of ${tokenId} - trading client not initialized`);
|
|
1017
|
+
return;
|
|
1033
1018
|
}
|
|
1034
1019
|
const response = await this.createMarketOrder({
|
|
1035
1020
|
tokenId,
|
|
@@ -1042,9 +1027,8 @@ var PolymarketClient = class {
|
|
|
1042
1027
|
}
|
|
1043
1028
|
async sell(tokenId, size) {
|
|
1044
1029
|
if (!this.tradingInitialized) {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
);
|
|
1030
|
+
this.logger.warning(`[Dummy] Selling ${size} of ${tokenId} - trading client not initialized`);
|
|
1031
|
+
return;
|
|
1048
1032
|
}
|
|
1049
1033
|
const response = await this.createMarketOrder({
|
|
1050
1034
|
tokenId,
|
|
@@ -1056,73 +1040,6 @@ var PolymarketClient = class {
|
|
|
1056
1040
|
}
|
|
1057
1041
|
}
|
|
1058
1042
|
// ============================================
|
|
1059
|
-
// Convenience Trading Methods
|
|
1060
|
-
// ============================================
|
|
1061
|
-
/**
|
|
1062
|
-
* Place a limit buy order
|
|
1063
|
-
*/
|
|
1064
|
-
async buyLimit(tokenId, price, size) {
|
|
1065
|
-
if (!this.tradingInitialized) {
|
|
1066
|
-
throw new Error(
|
|
1067
|
-
`Trading client not initialized. Call initializeTradingClient() before placing orders.`
|
|
1068
|
-
);
|
|
1069
|
-
}
|
|
1070
|
-
return this.createLimitOrder({
|
|
1071
|
-
tokenId,
|
|
1072
|
-
price,
|
|
1073
|
-
size,
|
|
1074
|
-
side: "BUY"
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Place a limit sell order
|
|
1079
|
-
*/
|
|
1080
|
-
async sellLimit(tokenId, price, size) {
|
|
1081
|
-
if (!this.tradingInitialized) {
|
|
1082
|
-
throw new Error(
|
|
1083
|
-
`Trading client not initialized. Call initializeTradingClient() before placing orders.`
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
return this.createLimitOrder({
|
|
1087
|
-
tokenId,
|
|
1088
|
-
price,
|
|
1089
|
-
size,
|
|
1090
|
-
side: "SELL"
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* Place a market buy order
|
|
1095
|
-
*/
|
|
1096
|
-
async buyMarket(tokenId, amount, slippage = 0.05) {
|
|
1097
|
-
if (!this.tradingInitialized) {
|
|
1098
|
-
throw new Error(
|
|
1099
|
-
`Trading client not initialized. Call initializeTradingClient() before placing orders.`
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
return this.createMarketOrder({
|
|
1103
|
-
tokenId,
|
|
1104
|
-
amount,
|
|
1105
|
-
side: "BUY",
|
|
1106
|
-
slippageTolerance: slippage
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
/**
|
|
1110
|
-
* Place a market sell order
|
|
1111
|
-
*/
|
|
1112
|
-
async sellMarket(tokenId, size, slippage = 0.05) {
|
|
1113
|
-
if (!this.tradingInitialized) {
|
|
1114
|
-
throw new Error(
|
|
1115
|
-
`Trading client not initialized. Call initializeTradingClient() before placing orders.`
|
|
1116
|
-
);
|
|
1117
|
-
}
|
|
1118
|
-
return this.createMarketOrder({
|
|
1119
|
-
tokenId,
|
|
1120
|
-
amount: size,
|
|
1121
|
-
side: "SELL",
|
|
1122
|
-
slippageTolerance: slippage
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
// ============================================
|
|
1126
1043
|
// Data Methods
|
|
1127
1044
|
// ============================================
|
|
1128
1045
|
async getPrice(ticker) {
|
|
@@ -1270,26 +1187,8 @@ var PolymarketClient = class {
|
|
|
1270
1187
|
return this.fetchData("/activity", { user, ...params });
|
|
1271
1188
|
}
|
|
1272
1189
|
async getTopHolders(marketId) {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
try {
|
|
1276
|
-
const market = await this.getMarket(marketId);
|
|
1277
|
-
if (market && market.tokens) {
|
|
1278
|
-
return market.tokens.map((token) => ({
|
|
1279
|
-
tokenId: token.token_id,
|
|
1280
|
-
outcome: token.outcome,
|
|
1281
|
-
holders: token.holders || 0
|
|
1282
|
-
}));
|
|
1283
|
-
}
|
|
1284
|
-
} catch (e) {
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
this.logger.debug("getTopHolders: No direct API available, returning market token info if available");
|
|
1288
|
-
return [];
|
|
1289
|
-
} catch (error) {
|
|
1290
|
-
this.logger.error(`Error fetching top holders for ${marketId}`, error);
|
|
1291
|
-
return [];
|
|
1292
|
-
}
|
|
1190
|
+
this.logger.warning("getTopHolders not directly supported by public Data API");
|
|
1191
|
+
return [];
|
|
1293
1192
|
}
|
|
1294
1193
|
async getPortfolioValue(user) {
|
|
1295
1194
|
try {
|
|
@@ -1304,18 +1203,8 @@ var PolymarketClient = class {
|
|
|
1304
1203
|
}
|
|
1305
1204
|
}
|
|
1306
1205
|
async getClosedPositions(user, params) {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const closedPositions = allPositions.filter((pos) => {
|
|
1310
|
-
const size = parseFloat(pos.size || pos.amount || "0");
|
|
1311
|
-
const isClosed = size === 0 || pos.closed === true || pos.status === "closed";
|
|
1312
|
-
return isClosed;
|
|
1313
|
-
});
|
|
1314
|
-
return closedPositions;
|
|
1315
|
-
} catch (error) {
|
|
1316
|
-
this.logger.error("Error fetching closed positions", error);
|
|
1317
|
-
return [];
|
|
1318
|
-
}
|
|
1206
|
+
this.logger.warning("getClosedPositions not directly supported, returning empty");
|
|
1207
|
+
return [];
|
|
1319
1208
|
}
|
|
1320
1209
|
async getTradedMarketsCount(user) {
|
|
1321
1210
|
try {
|
|
@@ -1338,21 +1227,8 @@ var PolymarketClient = class {
|
|
|
1338
1227
|
}
|
|
1339
1228
|
// Account methods
|
|
1340
1229
|
async getPosition(ticker) {
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
this.logger.error("getPosition requires userAddress to be set");
|
|
1344
|
-
return null;
|
|
1345
|
-
}
|
|
1346
|
-
const positions = await this.getPositions(this.userAddress);
|
|
1347
|
-
const matchingPosition = positions.find((pos) => {
|
|
1348
|
-
const marketMatch = pos.market === ticker || pos.slug === ticker || pos.condition_id === ticker || pos.asset_id === ticker || pos.token_id === ticker;
|
|
1349
|
-
return marketMatch && parseFloat(pos.size || pos.amount || "0") > 0;
|
|
1350
|
-
});
|
|
1351
|
-
return matchingPosition || null;
|
|
1352
|
-
} catch (error) {
|
|
1353
|
-
this.logger.error(`Error fetching position for ${ticker}`, error);
|
|
1354
|
-
return null;
|
|
1355
|
-
}
|
|
1230
|
+
this.logger.warning("getPosition(ticker) not implemented. Use getPositions(user) instead.");
|
|
1231
|
+
return null;
|
|
1356
1232
|
}
|
|
1357
1233
|
};
|
|
1358
1234
|
var HyperliquidClient = class {
|
|
@@ -1803,7 +1679,8 @@ function validateStrategy(strategy) {
|
|
|
1803
1679
|
);
|
|
1804
1680
|
}
|
|
1805
1681
|
}
|
|
1806
|
-
var
|
|
1682
|
+
var POLYMARKET_RTDS_URL = "wss://ws-live-data.polymarket.com";
|
|
1683
|
+
var PolymarketEventRunner = class {
|
|
1807
1684
|
constructor(strategy, config, context) {
|
|
1808
1685
|
this.strategy = strategy;
|
|
1809
1686
|
this.config = config;
|
|
@@ -1817,6 +1694,464 @@ var EventRunner = class {
|
|
|
1817
1694
|
this.maxReconnectAttempts = config.reconnect?.maxAttempts ?? 10;
|
|
1818
1695
|
this.baseReconnectDelay = config.reconnect?.delayMs ?? 1e3;
|
|
1819
1696
|
this.maxReconnectDelay = config.reconnect?.maxDelayMs ?? 3e4;
|
|
1697
|
+
this.pingIntervalMs = config.pingIntervalMs ?? 5e3;
|
|
1698
|
+
}
|
|
1699
|
+
ws = null;
|
|
1700
|
+
isRunning = false;
|
|
1701
|
+
reconnectAttempts = 0;
|
|
1702
|
+
reconnectTimeout = null;
|
|
1703
|
+
pingInterval = null;
|
|
1704
|
+
isIntentionallyClosed = false;
|
|
1705
|
+
// Default reconnection settings
|
|
1706
|
+
maxReconnectAttempts;
|
|
1707
|
+
baseReconnectDelay;
|
|
1708
|
+
maxReconnectDelay;
|
|
1709
|
+
pingIntervalMs;
|
|
1710
|
+
/**
|
|
1711
|
+
* Start listening to events
|
|
1712
|
+
* Connects to Polymarket RTDS and subscribes to events
|
|
1713
|
+
*/
|
|
1714
|
+
start() {
|
|
1715
|
+
if (this.isRunning) {
|
|
1716
|
+
this.config.logger.warning("Polymarket event runner is already running");
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
this.isRunning = true;
|
|
1720
|
+
this.isIntentionallyClosed = false;
|
|
1721
|
+
this.reconnectAttempts = 0;
|
|
1722
|
+
const strategyName = this.config.strategyId || "unnamed-strategy";
|
|
1723
|
+
this.config.logger.info(`Starting Polymarket event runner for strategy ${strategyName}`);
|
|
1724
|
+
this.config.logger.info(`Connecting to Polymarket RTDS: ${POLYMARKET_RTDS_URL}`);
|
|
1725
|
+
this.connect();
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Stop listening and disconnect from WebSocket
|
|
1729
|
+
*/
|
|
1730
|
+
stop() {
|
|
1731
|
+
if (!this.isRunning) {
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
this.isRunning = false;
|
|
1735
|
+
this.isIntentionallyClosed = true;
|
|
1736
|
+
if (this.pingInterval) {
|
|
1737
|
+
clearInterval(this.pingInterval);
|
|
1738
|
+
this.pingInterval = null;
|
|
1739
|
+
}
|
|
1740
|
+
if (this.reconnectTimeout) {
|
|
1741
|
+
clearTimeout(this.reconnectTimeout);
|
|
1742
|
+
this.reconnectTimeout = null;
|
|
1743
|
+
}
|
|
1744
|
+
if (this.ws) {
|
|
1745
|
+
try {
|
|
1746
|
+
this.unsubscribeFromEvents();
|
|
1747
|
+
this.ws.close(1e3, "Intentional close");
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
}
|
|
1750
|
+
this.ws = null;
|
|
1751
|
+
}
|
|
1752
|
+
this.config.logger.info("Polymarket event runner stopped");
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Check if the runner is currently active
|
|
1756
|
+
*/
|
|
1757
|
+
isActive() {
|
|
1758
|
+
return this.isRunning;
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Get current connection status
|
|
1762
|
+
*/
|
|
1763
|
+
getStatus() {
|
|
1764
|
+
return {
|
|
1765
|
+
running: this.isRunning,
|
|
1766
|
+
connected: this.ws?.readyState === WebSocket.OPEN,
|
|
1767
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
1768
|
+
subscriptionCount: this.strategy.subscriptions?.length ?? 0
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Connect to Polymarket RTDS WebSocket
|
|
1773
|
+
*/
|
|
1774
|
+
connect() {
|
|
1775
|
+
if (this.isIntentionallyClosed) {
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
try {
|
|
1779
|
+
this.ws = new WebSocket(POLYMARKET_RTDS_URL);
|
|
1780
|
+
this.ws.on("open", () => {
|
|
1781
|
+
this.config.logger.info("Connected to Polymarket RTDS");
|
|
1782
|
+
this.reconnectAttempts = 0;
|
|
1783
|
+
this.startPingInterval();
|
|
1784
|
+
this.subscribeToEvents();
|
|
1785
|
+
});
|
|
1786
|
+
this.ws.on("message", (data) => {
|
|
1787
|
+
this.handleMessage(data);
|
|
1788
|
+
});
|
|
1789
|
+
this.ws.on("error", (error) => {
|
|
1790
|
+
this.config.logger.error(`Polymarket RTDS WebSocket error: ${error.message}`, error);
|
|
1791
|
+
});
|
|
1792
|
+
this.ws.on("close", (code, reason) => {
|
|
1793
|
+
const reasonStr = reason.toString() || "none";
|
|
1794
|
+
this.config.logger.info(`Polymarket RTDS WebSocket closed (code: ${code}, reason: ${reasonStr})`);
|
|
1795
|
+
if (this.pingInterval) {
|
|
1796
|
+
clearInterval(this.pingInterval);
|
|
1797
|
+
this.pingInterval = null;
|
|
1798
|
+
}
|
|
1799
|
+
if (!this.isIntentionallyClosed && this.isRunning) {
|
|
1800
|
+
this.attemptReconnect();
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
this.ws.on("pong", () => {
|
|
1804
|
+
this.config.logger.debug("Received PONG from Polymarket RTDS");
|
|
1805
|
+
});
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
this.config.logger.error(`Failed to connect to Polymarket RTDS: ${error.message}`, error);
|
|
1808
|
+
if (this.isRunning && !this.isIntentionallyClosed) {
|
|
1809
|
+
this.attemptReconnect();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Start ping interval to maintain connection (Polymarket recommends 5 seconds)
|
|
1815
|
+
*/
|
|
1816
|
+
startPingInterval() {
|
|
1817
|
+
if (this.pingInterval) {
|
|
1818
|
+
clearInterval(this.pingInterval);
|
|
1819
|
+
}
|
|
1820
|
+
this.pingInterval = setInterval(() => {
|
|
1821
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1822
|
+
try {
|
|
1823
|
+
this.ws.send("PING");
|
|
1824
|
+
this.config.logger.debug("Sent PING to Polymarket RTDS");
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
this.config.logger.warning("Failed to send PING");
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}, this.pingIntervalMs);
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Attempt to reconnect with exponential backoff
|
|
1833
|
+
*/
|
|
1834
|
+
attemptReconnect() {
|
|
1835
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1836
|
+
this.config.logger.error(`Max reconnection attempts (${this.maxReconnectAttempts}) reached`);
|
|
1837
|
+
this.isRunning = false;
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
this.reconnectAttempts++;
|
|
1841
|
+
const delay = Math.min(
|
|
1842
|
+
this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1) + Math.random() * 1e3,
|
|
1843
|
+
this.maxReconnectDelay
|
|
1844
|
+
);
|
|
1845
|
+
this.config.logger.info(
|
|
1846
|
+
`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
1847
|
+
);
|
|
1848
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
1849
|
+
this.reconnectTimeout = null;
|
|
1850
|
+
this.connect();
|
|
1851
|
+
}, delay);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Map strategy subscriptions to Polymarket RTDS subscriptions
|
|
1855
|
+
* Only MarketSubscription types are supported by Polymarket RTDS
|
|
1856
|
+
* WalletSubscription and OpportunitySubscription are Osiris-only features
|
|
1857
|
+
*/
|
|
1858
|
+
mapToRTDSSubscriptions() {
|
|
1859
|
+
if (!this.strategy.subscriptions) {
|
|
1860
|
+
return [];
|
|
1861
|
+
}
|
|
1862
|
+
const rtdsSubscriptions = [];
|
|
1863
|
+
for (const sub of this.strategy.subscriptions) {
|
|
1864
|
+
let rtdsSub = null;
|
|
1865
|
+
if (isMarketSubscription(sub)) {
|
|
1866
|
+
const topic = this.mapEventTypeToTopic(sub.type);
|
|
1867
|
+
rtdsSub = {
|
|
1868
|
+
topic,
|
|
1869
|
+
type: this.getMessageTypeForTopic(topic)
|
|
1870
|
+
};
|
|
1871
|
+
} else if (isCustomSubscription(sub)) {
|
|
1872
|
+
const validRTDSTopics = [
|
|
1873
|
+
"crypto_prices",
|
|
1874
|
+
"comments",
|
|
1875
|
+
"activity",
|
|
1876
|
+
"orders",
|
|
1877
|
+
"trades",
|
|
1878
|
+
"profile"
|
|
1879
|
+
];
|
|
1880
|
+
if (validRTDSTopics.includes(sub.topic)) {
|
|
1881
|
+
const topic = sub.topic;
|
|
1882
|
+
rtdsSub = {
|
|
1883
|
+
topic,
|
|
1884
|
+
type: this.getMessageTypeForTopic(topic)
|
|
1885
|
+
};
|
|
1886
|
+
} else {
|
|
1887
|
+
this.config.logger.warning(
|
|
1888
|
+
`Custom subscription topic '${sub.topic}' is not a valid Polymarket RTDS topic. Valid topics: ${validRTDSTopics.join(", ")}`
|
|
1889
|
+
);
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
} else {
|
|
1893
|
+
this.config.logger.warning(
|
|
1894
|
+
`Subscription type '${sub.type}' is not supported by Polymarket RTDS. Only market subscriptions (price, orderbook, trade, fill) and custom subscriptions with valid RTDS topics are supported.`
|
|
1895
|
+
);
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
if (!rtdsSub) continue;
|
|
1899
|
+
if (this.config.clobAuth && ["orders", "trades"].includes(rtdsSub.topic)) {
|
|
1900
|
+
rtdsSub.clob_auth = this.config.clobAuth;
|
|
1901
|
+
}
|
|
1902
|
+
if (this.config.walletAddress && ["activity", "profile"].includes(rtdsSub.topic)) {
|
|
1903
|
+
rtdsSub.gamma_auth = {
|
|
1904
|
+
address: this.config.walletAddress
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
rtdsSubscriptions.push(rtdsSub);
|
|
1908
|
+
}
|
|
1909
|
+
return rtdsSubscriptions;
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Get the message type for a given RTDS topic
|
|
1913
|
+
* Each topic has specific message types as per the Polymarket RTDS documentation
|
|
1914
|
+
*/
|
|
1915
|
+
getMessageTypeForTopic(topic) {
|
|
1916
|
+
switch (topic) {
|
|
1917
|
+
case "crypto_prices":
|
|
1918
|
+
return "update";
|
|
1919
|
+
case "comments":
|
|
1920
|
+
return "comment_created";
|
|
1921
|
+
// Subscribes to all comment events
|
|
1922
|
+
case "trades":
|
|
1923
|
+
return "orders_matched";
|
|
1924
|
+
case "orders":
|
|
1925
|
+
return "order_placed";
|
|
1926
|
+
// Can also be 'order_cancelled', 'order_filled'
|
|
1927
|
+
case "activity":
|
|
1928
|
+
return "trade";
|
|
1929
|
+
// Can also be 'offer', etc.
|
|
1930
|
+
case "profile":
|
|
1931
|
+
return "update";
|
|
1932
|
+
default:
|
|
1933
|
+
return "update";
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Map EventType to Polymarket RTDS topic
|
|
1938
|
+
*/
|
|
1939
|
+
mapEventTypeToTopic(eventType) {
|
|
1940
|
+
switch (eventType) {
|
|
1941
|
+
case "price":
|
|
1942
|
+
return "crypto_prices";
|
|
1943
|
+
case "trade":
|
|
1944
|
+
return "trades";
|
|
1945
|
+
case "orderbook":
|
|
1946
|
+
case "fill":
|
|
1947
|
+
return "orders";
|
|
1948
|
+
case "custom":
|
|
1949
|
+
return "activity";
|
|
1950
|
+
default:
|
|
1951
|
+
return "activity";
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Subscribe to events based on strategy's subscriptions
|
|
1956
|
+
*/
|
|
1957
|
+
subscribeToEvents() {
|
|
1958
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1959
|
+
this.config.logger.warning("Cannot subscribe - WebSocket not connected");
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (!this.strategy.subscriptions || this.strategy.subscriptions.length === 0) {
|
|
1963
|
+
this.config.logger.warning("No subscriptions defined for strategy");
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
const rtdsSubscriptions = this.mapToRTDSSubscriptions();
|
|
1967
|
+
const subscribeMessage = {
|
|
1968
|
+
action: "subscribe",
|
|
1969
|
+
subscriptions: rtdsSubscriptions
|
|
1970
|
+
};
|
|
1971
|
+
try {
|
|
1972
|
+
this.ws.send(JSON.stringify(subscribeMessage));
|
|
1973
|
+
this.config.logger.info(
|
|
1974
|
+
`Subscribed to ${rtdsSubscriptions.length} Polymarket RTDS topic(s): ` + rtdsSubscriptions.map((s) => s.topic).join(", ")
|
|
1975
|
+
);
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
this.config.logger.error(`Failed to send subscription: ${error.message}`, error);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Unsubscribe from events before disconnecting
|
|
1982
|
+
*/
|
|
1983
|
+
unsubscribeFromEvents() {
|
|
1984
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (!this.strategy.subscriptions || this.strategy.subscriptions.length === 0) {
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
const rtdsSubscriptions = this.mapToRTDSSubscriptions();
|
|
1991
|
+
const unsubscribeMessage = {
|
|
1992
|
+
action: "unsubscribe",
|
|
1993
|
+
subscriptions: rtdsSubscriptions
|
|
1994
|
+
};
|
|
1995
|
+
try {
|
|
1996
|
+
this.ws.send(JSON.stringify(unsubscribeMessage));
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Handle incoming WebSocket messages
|
|
2002
|
+
*/
|
|
2003
|
+
handleMessage(data) {
|
|
2004
|
+
try {
|
|
2005
|
+
const rawData = typeof data === "string" ? data : data.toString();
|
|
2006
|
+
if (rawData === "PING") {
|
|
2007
|
+
this.ws?.send("PONG");
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (rawData === "PONG" || rawData.trim() === "") {
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
let message;
|
|
2014
|
+
try {
|
|
2015
|
+
message = JSON.parse(rawData);
|
|
2016
|
+
} catch (parseError) {
|
|
2017
|
+
this.config.logger.warning(`Received non-JSON message: ${rawData.slice(0, 100)}`);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
if (!this.isValidRTDSMessage(message)) {
|
|
2021
|
+
this.config.logger.debug(`Received non-event message: ${JSON.stringify(message).slice(0, 200)}`);
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
const strategyEvent = this.convertToStrategyEvent(message);
|
|
2025
|
+
this.dispatchEvent(strategyEvent);
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
this.config.logger.error(`Error handling message: ${error.message}`, error);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Validate that a message is a valid Polymarket RTDS message
|
|
2032
|
+
*/
|
|
2033
|
+
isValidRTDSMessage(message) {
|
|
2034
|
+
return message && typeof message === "object" && typeof message.topic === "string" && typeof message.type === "string" && typeof message.timestamp === "number" && typeof message.payload === "object";
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Convert Polymarket RTDS message to StrategyEvent
|
|
2038
|
+
*/
|
|
2039
|
+
convertToStrategyEvent(message) {
|
|
2040
|
+
const eventType = this.mapTopicToEventType(message.topic);
|
|
2041
|
+
const eventData = this.extractEventData(message);
|
|
2042
|
+
return {
|
|
2043
|
+
type: eventType,
|
|
2044
|
+
timestamp: message.timestamp,
|
|
2045
|
+
market: eventData.market,
|
|
2046
|
+
data: eventData
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Map Polymarket RTDS topic to EventType
|
|
2051
|
+
*/
|
|
2052
|
+
mapTopicToEventType(topic) {
|
|
2053
|
+
switch (topic) {
|
|
2054
|
+
case "crypto_prices":
|
|
2055
|
+
return "price";
|
|
2056
|
+
case "trades":
|
|
2057
|
+
return "trade";
|
|
2058
|
+
case "orders":
|
|
2059
|
+
return "orderbook";
|
|
2060
|
+
case "activity":
|
|
2061
|
+
case "comments":
|
|
2062
|
+
case "profile":
|
|
2063
|
+
default:
|
|
2064
|
+
return "custom";
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Extract event data from Polymarket RTDS payload
|
|
2069
|
+
*/
|
|
2070
|
+
extractEventData(message) {
|
|
2071
|
+
const payload = message.payload;
|
|
2072
|
+
const data = {
|
|
2073
|
+
raw: {
|
|
2074
|
+
...payload,
|
|
2075
|
+
topic: message.topic,
|
|
2076
|
+
// Include topic for easier filtering
|
|
2077
|
+
type: message.type
|
|
2078
|
+
// Include message type
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
switch (message.topic) {
|
|
2082
|
+
case "crypto_prices":
|
|
2083
|
+
data.price = payload.price ?? payload.value;
|
|
2084
|
+
data.previousPrice = payload.previousPrice ?? payload.previous;
|
|
2085
|
+
data.changePercent = payload.changePercent ?? payload.change;
|
|
2086
|
+
break;
|
|
2087
|
+
case "trades":
|
|
2088
|
+
data.price = payload.price;
|
|
2089
|
+
data.size = payload.size ?? payload.amount;
|
|
2090
|
+
data.side = payload.side;
|
|
2091
|
+
data.tradeId = payload.tradeId ?? payload.id;
|
|
2092
|
+
break;
|
|
2093
|
+
case "orders":
|
|
2094
|
+
data.bestBid = payload.bestBid ?? payload.bid;
|
|
2095
|
+
data.bestAsk = payload.bestAsk ?? payload.ask;
|
|
2096
|
+
data.spread = payload.spread;
|
|
2097
|
+
data.orderId = payload.orderId ?? payload.id;
|
|
2098
|
+
break;
|
|
2099
|
+
case "comments":
|
|
2100
|
+
data.commentText = payload.text ?? payload.comment;
|
|
2101
|
+
data.author = payload.author ?? payload.user;
|
|
2102
|
+
data.market = payload.market ?? payload.slug;
|
|
2103
|
+
data.commentType = payload.type;
|
|
2104
|
+
data.reactions = payload.reactions;
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
2107
|
+
if (payload.market) data.market = payload.market;
|
|
2108
|
+
if (payload.asset) data.market = payload.asset;
|
|
2109
|
+
if (payload.tokenId) data.market = payload.tokenId;
|
|
2110
|
+
if (payload.conditionId) data.market = payload.conditionId;
|
|
2111
|
+
return data;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Dispatch an event to the strategy's onEvent handler
|
|
2115
|
+
*/
|
|
2116
|
+
async dispatchEvent(event) {
|
|
2117
|
+
const eventStart = Date.now();
|
|
2118
|
+
this.config.logger.debug(
|
|
2119
|
+
`Dispatching ${event.type} event${event.market ? ` for ${event.market}` : ""}`
|
|
2120
|
+
);
|
|
2121
|
+
try {
|
|
2122
|
+
if (this.strategy.onEvent) {
|
|
2123
|
+
await this.strategy.onEvent(event, this.context);
|
|
2124
|
+
}
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
this.config.logger.error(
|
|
2127
|
+
`Error in strategy onEvent handler: ${error.message}`,
|
|
2128
|
+
error
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
const eventDuration = Date.now() - eventStart;
|
|
2132
|
+
this.config.logger.debug(`Event handling completed in ${eventDuration}ms`);
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
function createPolymarketEventRunner(strategy, config, context) {
|
|
2136
|
+
return new PolymarketEventRunner(strategy, config, context);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// src/engine/event-runner.ts
|
|
2140
|
+
var OsirisEventRunner = class {
|
|
2141
|
+
constructor(strategy, config, context) {
|
|
2142
|
+
this.strategy = strategy;
|
|
2143
|
+
this.config = config;
|
|
2144
|
+
this.context = context;
|
|
2145
|
+
if (!this.strategy.subscriptions || this.strategy.subscriptions.length === 0) {
|
|
2146
|
+
throw new Error("Strategy must have subscriptions defined for event-based execution");
|
|
2147
|
+
}
|
|
2148
|
+
if (typeof this.strategy.onEvent !== "function") {
|
|
2149
|
+
throw new Error("Strategy must implement onEvent method for event-based execution");
|
|
2150
|
+
}
|
|
2151
|
+
this.maxReconnectAttempts = config.reconnect?.maxAttempts ?? 10;
|
|
2152
|
+
this.baseReconnectDelay = config.reconnect?.delayMs ?? 1e3;
|
|
2153
|
+
this.maxReconnectDelay = config.reconnect?.maxDelayMs ?? 3e4;
|
|
2154
|
+
if (!config.eventSourceUrl) ;
|
|
1820
2155
|
}
|
|
1821
2156
|
ws = null;
|
|
1822
2157
|
isRunning = false;
|
|
@@ -1892,6 +2227,9 @@ var EventRunner = class {
|
|
|
1892
2227
|
return;
|
|
1893
2228
|
}
|
|
1894
2229
|
try {
|
|
2230
|
+
if (!this.config.eventSourceUrl) {
|
|
2231
|
+
throw new Error("eventSourceUrl is required for OsirisEventRunner");
|
|
2232
|
+
}
|
|
1895
2233
|
this.ws = new WebSocket(this.config.eventSourceUrl);
|
|
1896
2234
|
this.ws.on("open", () => {
|
|
1897
2235
|
this.config.logger.info("Connected to event source");
|
|
@@ -1943,6 +2281,10 @@ var EventRunner = class {
|
|
|
1943
2281
|
/**
|
|
1944
2282
|
* Subscribe to events based on strategy's subscriptions
|
|
1945
2283
|
*/
|
|
2284
|
+
/**
|
|
2285
|
+
* Subscribe to events based on strategy's subscriptions
|
|
2286
|
+
* Maps SDK subscriptions to Osiris Pub/Sub protocol
|
|
2287
|
+
*/
|
|
1946
2288
|
subscribeToEvents() {
|
|
1947
2289
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1948
2290
|
this.config.logger.warning("Cannot subscribe - WebSocket not connected");
|
|
@@ -1952,19 +2294,63 @@ var EventRunner = class {
|
|
|
1952
2294
|
this.config.logger.warning("No subscriptions defined for strategy");
|
|
1953
2295
|
return;
|
|
1954
2296
|
}
|
|
2297
|
+
const topics = [];
|
|
2298
|
+
const filters = {};
|
|
2299
|
+
for (const sub of this.strategy.subscriptions) {
|
|
2300
|
+
const topic = this.mapSubscriptionToTopic(sub);
|
|
2301
|
+
if (topic) {
|
|
2302
|
+
topics.push(topic);
|
|
2303
|
+
if (sub.conditions) {
|
|
2304
|
+
filters[topic] = sub.conditions;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (topics.length === 0) {
|
|
2309
|
+
this.config.logger.warning("No valid topics mapped from subscriptions");
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
1955
2312
|
const subscribeMessage = {
|
|
1956
2313
|
action: "subscribe",
|
|
1957
|
-
|
|
2314
|
+
topics,
|
|
2315
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0
|
|
1958
2316
|
};
|
|
1959
2317
|
try {
|
|
1960
2318
|
this.ws.send(JSON.stringify(subscribeMessage));
|
|
1961
2319
|
this.config.logger.info(
|
|
1962
|
-
`Subscribed to ${
|
|
2320
|
+
`Subscribed to ${topics.length} topics: ${topics.join(", ")}`
|
|
1963
2321
|
);
|
|
1964
2322
|
} catch (error) {
|
|
1965
2323
|
this.config.logger.error(`Failed to send subscription: ${error.message}`, error);
|
|
1966
2324
|
}
|
|
1967
2325
|
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Map EventSubscription to Osiris Pub/Sub topic string
|
|
2328
|
+
* Uses discriminated union to properly handle each subscription type
|
|
2329
|
+
*/
|
|
2330
|
+
mapSubscriptionToTopic(sub) {
|
|
2331
|
+
switch (sub.type) {
|
|
2332
|
+
// Market subscriptions → market:{market}
|
|
2333
|
+
case "price":
|
|
2334
|
+
case "orderbook":
|
|
2335
|
+
case "trade":
|
|
2336
|
+
case "fill":
|
|
2337
|
+
return sub.market ? `market:${sub.market}` : null;
|
|
2338
|
+
// Wallet subscriptions → wallet:{wallet}
|
|
2339
|
+
case "wallet":
|
|
2340
|
+
return sub.wallet ? `wallet:${sub.wallet}` : null;
|
|
2341
|
+
// Opportunity subscriptions → opps:{filter}
|
|
2342
|
+
case "opportunity":
|
|
2343
|
+
return `opps:${sub.filter || "all"}`;
|
|
2344
|
+
// Custom subscriptions → use topic directly
|
|
2345
|
+
case "custom":
|
|
2346
|
+
return sub.topic || null;
|
|
2347
|
+
default:
|
|
2348
|
+
return null;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Unsubscribe from events before disconnecting
|
|
2353
|
+
*/
|
|
1968
2354
|
/**
|
|
1969
2355
|
* Unsubscribe from events before disconnecting
|
|
1970
2356
|
*/
|
|
@@ -1975,9 +2361,11 @@ var EventRunner = class {
|
|
|
1975
2361
|
if (!this.strategy.subscriptions || this.strategy.subscriptions.length === 0) {
|
|
1976
2362
|
return;
|
|
1977
2363
|
}
|
|
2364
|
+
const topics = this.strategy.subscriptions.map((s) => this.mapSubscriptionToTopic(s)).filter((t) => t !== null);
|
|
2365
|
+
if (topics.length === 0) return;
|
|
1978
2366
|
const unsubscribeMessage = {
|
|
1979
2367
|
action: "unsubscribe",
|
|
1980
|
-
|
|
2368
|
+
topics
|
|
1981
2369
|
};
|
|
1982
2370
|
try {
|
|
1983
2371
|
this.ws.send(JSON.stringify(unsubscribeMessage));
|
|
@@ -2000,28 +2388,103 @@ var EventRunner = class {
|
|
|
2000
2388
|
this.config.logger.warning(`Received non-JSON message: ${rawData.slice(0, 100)}`);
|
|
2001
2389
|
return;
|
|
2002
2390
|
}
|
|
2003
|
-
if (
|
|
2004
|
-
this.config.logger.
|
|
2391
|
+
if (message.type === "connected") {
|
|
2392
|
+
this.config.logger.info(`Osiris Pub/Sub: ${message.message}`);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
if (message.type === "subscribe.result") {
|
|
2396
|
+
if (message.success) {
|
|
2397
|
+
this.config.logger.info(`Successfully subscribed to: ${message.subscribed.join(", ")}`);
|
|
2398
|
+
} else {
|
|
2399
|
+
this.config.logger.error(`Subscription failed: ${JSON.stringify(message.errors)}`);
|
|
2400
|
+
}
|
|
2005
2401
|
return;
|
|
2006
2402
|
}
|
|
2007
|
-
|
|
2403
|
+
if (message.type === "error") {
|
|
2404
|
+
this.config.logger.error(`Osiris Pub/Sub Error: ${message.error}`);
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
if (message.type === "pong") {
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
const event = this.mapMessageToStrategyEvent(message);
|
|
2411
|
+
if (event) {
|
|
2412
|
+
this.dispatchEvent(event);
|
|
2413
|
+
} else {
|
|
2414
|
+
this.config.logger.debug(`Unhandled message type: ${message.type}`);
|
|
2415
|
+
}
|
|
2008
2416
|
} catch (error) {
|
|
2009
2417
|
this.config.logger.error(`Error handling message: ${error.message}`, error);
|
|
2010
2418
|
}
|
|
2011
2419
|
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Map incoming Osiris message to StrategyEvent
|
|
2422
|
+
* Uses proper fields for each event type (market vs wallet)
|
|
2423
|
+
*/
|
|
2424
|
+
mapMessageToStrategyEvent(message) {
|
|
2425
|
+
const timestamp = message.ts || Date.now();
|
|
2426
|
+
switch (message.type) {
|
|
2427
|
+
case "market.analysis":
|
|
2428
|
+
return {
|
|
2429
|
+
type: "price",
|
|
2430
|
+
// Treating analysis as 'price/market' update
|
|
2431
|
+
market: message.slug,
|
|
2432
|
+
timestamp,
|
|
2433
|
+
data: {
|
|
2434
|
+
...message.data,
|
|
2435
|
+
raw: message
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
case "wallet.analysis":
|
|
2439
|
+
return {
|
|
2440
|
+
type: "wallet",
|
|
2441
|
+
wallet: message.address,
|
|
2442
|
+
// Use wallet field, not market
|
|
2443
|
+
timestamp,
|
|
2444
|
+
data: {
|
|
2445
|
+
...message.data,
|
|
2446
|
+
raw: message
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
case "opportunity":
|
|
2450
|
+
return {
|
|
2451
|
+
type: "opportunity",
|
|
2452
|
+
market: message.slug,
|
|
2453
|
+
timestamp,
|
|
2454
|
+
data: {
|
|
2455
|
+
...message.data,
|
|
2456
|
+
raw: message
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
default:
|
|
2460
|
+
if (message.type && message.data) {
|
|
2461
|
+
return {
|
|
2462
|
+
type: "custom",
|
|
2463
|
+
market: message.slug || message.topic,
|
|
2464
|
+
timestamp,
|
|
2465
|
+
data: {
|
|
2466
|
+
...message.data,
|
|
2467
|
+
raw: message
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
return null;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2012
2474
|
/**
|
|
2013
2475
|
* Validate that a message is a valid StrategyEvent
|
|
2014
2476
|
*/
|
|
2015
2477
|
isValidEvent(message) {
|
|
2016
|
-
return message && typeof message === "object" && typeof message.type === "string"
|
|
2478
|
+
return message && typeof message === "object" && typeof message.type === "string";
|
|
2017
2479
|
}
|
|
2018
2480
|
/**
|
|
2019
2481
|
* Dispatch an event to the strategy's onEvent handler
|
|
2020
2482
|
*/
|
|
2021
2483
|
async dispatchEvent(event) {
|
|
2022
2484
|
const eventStart = Date.now();
|
|
2485
|
+
const target = event.market || event.wallet || "";
|
|
2023
2486
|
this.config.logger.debug(
|
|
2024
|
-
`Dispatching ${event.type} event${
|
|
2487
|
+
`Dispatching ${event.type} event${target ? ` for ${target}` : ""}`
|
|
2025
2488
|
);
|
|
2026
2489
|
try {
|
|
2027
2490
|
if (this.strategy.onEvent) {
|
|
@@ -2038,7 +2501,29 @@ var EventRunner = class {
|
|
|
2038
2501
|
}
|
|
2039
2502
|
};
|
|
2040
2503
|
function createEventRunner(strategy, config, context) {
|
|
2041
|
-
|
|
2504
|
+
const hasPolymarketSubscription = strategy.subscriptions?.some(
|
|
2505
|
+
(sub) => sub.eventSource === "polymarket"
|
|
2506
|
+
);
|
|
2507
|
+
const hasOsirisSubscription = strategy.subscriptions?.some(
|
|
2508
|
+
(sub) => !sub.eventSource || sub.eventSource === "osiris"
|
|
2509
|
+
);
|
|
2510
|
+
if (hasPolymarketSubscription) {
|
|
2511
|
+
if (hasOsirisSubscription) {
|
|
2512
|
+
config.logger.warning(
|
|
2513
|
+
"Strategy has both Polymarket and generic Osiris subscriptions. Only Polymarket events will be processed by this runner. Mixed sources are not currently supported in a single runner."
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
return createPolymarketEventRunner(strategy, {
|
|
2517
|
+
logger: config.logger,
|
|
2518
|
+
strategyId: config.strategyId,
|
|
2519
|
+
clobAuth: config.polymarket?.clobAuth,
|
|
2520
|
+
walletAddress: config.polymarket?.walletAddress,
|
|
2521
|
+
pingIntervalMs: config.polymarket?.pingIntervalMs,
|
|
2522
|
+
reconnect: config.reconnect
|
|
2523
|
+
}, context);
|
|
2524
|
+
}
|
|
2525
|
+
if (!config.eventSourceUrl) ;
|
|
2526
|
+
return new OsirisEventRunner(strategy, config, context);
|
|
2042
2527
|
}
|
|
2043
2528
|
var CHAIN_MAP = {
|
|
2044
2529
|
"evm:eip155:1": mainnet,
|
|
@@ -2190,6 +2675,6 @@ var Signer = class {
|
|
|
2190
2675
|
}
|
|
2191
2676
|
};
|
|
2192
2677
|
|
|
2193
|
-
export {
|
|
2678
|
+
export { HyperliquidClient, MemoryStateManager, OsirisEventRunner, OsirisSigner, PolymarketClient, PolymarketEventRunner, PrivateKeySigner, RedisStateManager, Signer, StrategyEngine, createConsoleLogger, createEventRunner, createOsirisContext, createPolymarketEventRunner, createStrategyEngine, isCustomSubscription, isEventBasedStrategy, isMarketSubscription, isOpportunitySubscription, isTickBasedStrategy, isWalletSubscription, runStrategy, validateStrategy };
|
|
2194
2679
|
//# sourceMappingURL=index.js.map
|
|
2195
2680
|
//# sourceMappingURL=index.js.map
|