@t2000/sdk 0.15.3 → 0.16.1
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/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/{index-BykavuDO.d.cts → index-BOkO4S7r.d.cts} +87 -1
- package/dist/{index-BykavuDO.d.ts → index-BOkO4S7r.d.ts} +87 -1
- package/dist/index.cjs +602 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -5
- package/dist/index.d.ts +113 -5
- package/dist/index.js +601 -11
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
|
|
|
4
4
|
import { normalizeSuiAddress, isValidSuiAddress, normalizeStructTag } from '@mysten/sui/utils';
|
|
5
5
|
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
6
6
|
import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
|
|
7
|
-
import { createHash, randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
|
|
7
|
+
import { createHash, randomUUID, randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
|
|
8
8
|
import { access, mkdir, writeFile, readFile } from 'fs/promises';
|
|
9
9
|
import { join, dirname, resolve } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
@@ -85,6 +85,26 @@ var INVESTMENT_ASSETS = {
|
|
|
85
85
|
BTC: SUPPORTED_ASSETS.BTC,
|
|
86
86
|
ETH: SUPPORTED_ASSETS.ETH
|
|
87
87
|
};
|
|
88
|
+
var DEFAULT_STRATEGIES = {
|
|
89
|
+
bluechip: {
|
|
90
|
+
name: "Bluechip / Large-Cap",
|
|
91
|
+
allocations: { BTC: 50, ETH: 30, SUI: 20 },
|
|
92
|
+
description: "Large-cap crypto index",
|
|
93
|
+
custom: false
|
|
94
|
+
},
|
|
95
|
+
layer1: {
|
|
96
|
+
name: "Smart Contract Platforms",
|
|
97
|
+
allocations: { ETH: 50, SUI: 50 },
|
|
98
|
+
description: "Smart contract platforms",
|
|
99
|
+
custom: false
|
|
100
|
+
},
|
|
101
|
+
"sui-heavy": {
|
|
102
|
+
name: "Sui-Weighted Portfolio",
|
|
103
|
+
allocations: { BTC: 20, ETH: 20, SUI: 60 },
|
|
104
|
+
description: "Sui-weighted portfolio",
|
|
105
|
+
custom: false
|
|
106
|
+
}
|
|
107
|
+
};
|
|
88
108
|
var PERPS_MARKETS = ["SUI-PERP"];
|
|
89
109
|
var DEFAULT_MAX_LEVERAGE = 5;
|
|
90
110
|
var DEFAULT_MAX_POSITION_SIZE = 1e3;
|
|
@@ -2816,7 +2836,7 @@ var ContactManager = class {
|
|
|
2816
2836
|
}
|
|
2817
2837
|
};
|
|
2818
2838
|
function emptyData() {
|
|
2819
|
-
return { positions: {}, realizedPnL: 0 };
|
|
2839
|
+
return { positions: {}, strategies: {}, realizedPnL: 0 };
|
|
2820
2840
|
}
|
|
2821
2841
|
var PortfolioManager = class {
|
|
2822
2842
|
data = emptyData();
|
|
@@ -2831,6 +2851,7 @@ var PortfolioManager = class {
|
|
|
2831
2851
|
try {
|
|
2832
2852
|
if (existsSync(this.filePath)) {
|
|
2833
2853
|
this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
2854
|
+
if (!this.data.strategies) this.data.strategies = {};
|
|
2834
2855
|
}
|
|
2835
2856
|
} catch {
|
|
2836
2857
|
this.data = emptyData();
|
|
@@ -2916,6 +2937,298 @@ var PortfolioManager = class {
|
|
|
2916
2937
|
this.load();
|
|
2917
2938
|
return this.data.realizedPnL;
|
|
2918
2939
|
}
|
|
2940
|
+
// --- Strategy position tracking ---
|
|
2941
|
+
recordStrategyBuy(strategyKey, trade) {
|
|
2942
|
+
this.load();
|
|
2943
|
+
if (!this.data.strategies[strategyKey]) {
|
|
2944
|
+
this.data.strategies[strategyKey] = {};
|
|
2945
|
+
}
|
|
2946
|
+
const bucket = this.data.strategies[strategyKey];
|
|
2947
|
+
const pos = bucket[trade.asset] ?? { totalAmount: 0, costBasis: 0, avgPrice: 0, trades: [] };
|
|
2948
|
+
pos.totalAmount += trade.amount;
|
|
2949
|
+
pos.costBasis += trade.usdValue;
|
|
2950
|
+
pos.avgPrice = pos.costBasis / pos.totalAmount;
|
|
2951
|
+
pos.trades.push(trade);
|
|
2952
|
+
bucket[trade.asset] = pos;
|
|
2953
|
+
this.save();
|
|
2954
|
+
}
|
|
2955
|
+
recordStrategySell(strategyKey, trade) {
|
|
2956
|
+
this.load();
|
|
2957
|
+
const bucket = this.data.strategies[strategyKey];
|
|
2958
|
+
if (!bucket) {
|
|
2959
|
+
throw new T2000Error("STRATEGY_NOT_FOUND", `No positions for strategy '${strategyKey}'`);
|
|
2960
|
+
}
|
|
2961
|
+
const pos = bucket[trade.asset];
|
|
2962
|
+
if (!pos || pos.totalAmount <= 0) {
|
|
2963
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position in strategy '${strategyKey}'`);
|
|
2964
|
+
}
|
|
2965
|
+
const sellAmount = Math.min(trade.amount, pos.totalAmount);
|
|
2966
|
+
const costOfSold = pos.avgPrice * sellAmount;
|
|
2967
|
+
const realizedPnL = trade.usdValue - costOfSold;
|
|
2968
|
+
pos.totalAmount -= sellAmount;
|
|
2969
|
+
pos.costBasis -= costOfSold;
|
|
2970
|
+
if (pos.totalAmount < 1e-6) {
|
|
2971
|
+
pos.totalAmount = 0;
|
|
2972
|
+
pos.costBasis = 0;
|
|
2973
|
+
pos.avgPrice = 0;
|
|
2974
|
+
}
|
|
2975
|
+
pos.trades.push(trade);
|
|
2976
|
+
this.data.realizedPnL += realizedPnL;
|
|
2977
|
+
bucket[trade.asset] = pos;
|
|
2978
|
+
const hasPositions = Object.values(bucket).some((p) => p.totalAmount > 0);
|
|
2979
|
+
if (!hasPositions) {
|
|
2980
|
+
delete this.data.strategies[strategyKey];
|
|
2981
|
+
}
|
|
2982
|
+
this.save();
|
|
2983
|
+
return realizedPnL;
|
|
2984
|
+
}
|
|
2985
|
+
getStrategyPositions(strategyKey) {
|
|
2986
|
+
this.load();
|
|
2987
|
+
const bucket = this.data.strategies[strategyKey];
|
|
2988
|
+
if (!bucket) return [];
|
|
2989
|
+
return Object.entries(bucket).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
|
|
2990
|
+
}
|
|
2991
|
+
getAllStrategyKeys() {
|
|
2992
|
+
this.load();
|
|
2993
|
+
return Object.keys(this.data.strategies);
|
|
2994
|
+
}
|
|
2995
|
+
hasStrategyPositions(strategyKey) {
|
|
2996
|
+
this.load();
|
|
2997
|
+
const bucket = this.data.strategies[strategyKey];
|
|
2998
|
+
if (!bucket) return false;
|
|
2999
|
+
return Object.values(bucket).some((p) => p.totalAmount > 0);
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
function emptyData2() {
|
|
3003
|
+
return { strategies: {} };
|
|
3004
|
+
}
|
|
3005
|
+
var StrategyManager = class {
|
|
3006
|
+
data = emptyData2();
|
|
3007
|
+
filePath;
|
|
3008
|
+
dir;
|
|
3009
|
+
seeded = false;
|
|
3010
|
+
constructor(configDir) {
|
|
3011
|
+
this.dir = configDir ?? join(homedir(), ".t2000");
|
|
3012
|
+
this.filePath = join(this.dir, "strategies.json");
|
|
3013
|
+
this.load();
|
|
3014
|
+
}
|
|
3015
|
+
load() {
|
|
3016
|
+
try {
|
|
3017
|
+
if (existsSync(this.filePath)) {
|
|
3018
|
+
this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
3019
|
+
}
|
|
3020
|
+
} catch {
|
|
3021
|
+
this.data = emptyData2();
|
|
3022
|
+
}
|
|
3023
|
+
if (!this.seeded) {
|
|
3024
|
+
this.seedDefaults();
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
save() {
|
|
3028
|
+
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
3029
|
+
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
3030
|
+
}
|
|
3031
|
+
seedDefaults() {
|
|
3032
|
+
this.seeded = true;
|
|
3033
|
+
let changed = false;
|
|
3034
|
+
for (const [key, def] of Object.entries(DEFAULT_STRATEGIES)) {
|
|
3035
|
+
if (!this.data.strategies[key]) {
|
|
3036
|
+
this.data.strategies[key] = { ...def, allocations: { ...def.allocations } };
|
|
3037
|
+
changed = true;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
if (changed) this.save();
|
|
3041
|
+
}
|
|
3042
|
+
getAll() {
|
|
3043
|
+
this.load();
|
|
3044
|
+
return { ...this.data.strategies };
|
|
3045
|
+
}
|
|
3046
|
+
get(name) {
|
|
3047
|
+
this.load();
|
|
3048
|
+
const strategy = this.data.strategies[name];
|
|
3049
|
+
if (!strategy) {
|
|
3050
|
+
throw new T2000Error("STRATEGY_NOT_FOUND", `Strategy '${name}' not found`);
|
|
3051
|
+
}
|
|
3052
|
+
return strategy;
|
|
3053
|
+
}
|
|
3054
|
+
create(params) {
|
|
3055
|
+
this.load();
|
|
3056
|
+
const key = params.name.toLowerCase().replace(/\s+/g, "-");
|
|
3057
|
+
if (this.data.strategies[key]) {
|
|
3058
|
+
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Strategy '${key}' already exists`);
|
|
3059
|
+
}
|
|
3060
|
+
this.validateAllocations(params.allocations);
|
|
3061
|
+
const definition = {
|
|
3062
|
+
name: params.name,
|
|
3063
|
+
allocations: { ...params.allocations },
|
|
3064
|
+
description: params.description ?? `Custom strategy: ${params.name}`,
|
|
3065
|
+
custom: true
|
|
3066
|
+
};
|
|
3067
|
+
this.data.strategies[key] = definition;
|
|
3068
|
+
this.save();
|
|
3069
|
+
return definition;
|
|
3070
|
+
}
|
|
3071
|
+
delete(name) {
|
|
3072
|
+
this.load();
|
|
3073
|
+
const strategy = this.data.strategies[name];
|
|
3074
|
+
if (!strategy) {
|
|
3075
|
+
throw new T2000Error("STRATEGY_NOT_FOUND", `Strategy '${name}' not found`);
|
|
3076
|
+
}
|
|
3077
|
+
if (!strategy.custom) {
|
|
3078
|
+
throw new T2000Error("STRATEGY_BUILTIN", `Cannot delete built-in strategy '${name}'`);
|
|
3079
|
+
}
|
|
3080
|
+
delete this.data.strategies[name];
|
|
3081
|
+
this.save();
|
|
3082
|
+
}
|
|
3083
|
+
validateAllocations(allocations) {
|
|
3084
|
+
const total = Object.values(allocations).reduce((sum, pct) => sum + pct, 0);
|
|
3085
|
+
if (Math.abs(total - 100) > 0.01) {
|
|
3086
|
+
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Allocations must sum to 100 (got ${total})`);
|
|
3087
|
+
}
|
|
3088
|
+
for (const asset of Object.keys(allocations)) {
|
|
3089
|
+
if (!(asset in INVESTMENT_ASSETS)) {
|
|
3090
|
+
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `${asset} is not an investment asset`);
|
|
3091
|
+
}
|
|
3092
|
+
if (allocations[asset] <= 0) {
|
|
3093
|
+
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Allocation for ${asset} must be > 0`);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
validateMinAmount(allocations, totalUsd) {
|
|
3098
|
+
const smallestPct = Math.min(...Object.values(allocations));
|
|
3099
|
+
const minRequired = Math.ceil(100 / smallestPct);
|
|
3100
|
+
if (totalUsd < minRequired) {
|
|
3101
|
+
const smallestAsset = Object.entries(allocations).find(([, p]) => p === smallestPct)?.[0] ?? "?";
|
|
3102
|
+
throw new T2000Error(
|
|
3103
|
+
"STRATEGY_MIN_AMOUNT",
|
|
3104
|
+
`Minimum $${minRequired} for this strategy (${smallestAsset} at ${smallestPct}% needs at least $1)`
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
};
|
|
3109
|
+
function emptyData3() {
|
|
3110
|
+
return { schedules: [] };
|
|
3111
|
+
}
|
|
3112
|
+
function computeNextRun(frequency, dayOfWeek, dayOfMonth, from) {
|
|
3113
|
+
const base = /* @__PURE__ */ new Date();
|
|
3114
|
+
const next = new Date(base);
|
|
3115
|
+
switch (frequency) {
|
|
3116
|
+
case "daily":
|
|
3117
|
+
next.setDate(next.getDate() + 1);
|
|
3118
|
+
next.setHours(0, 0, 0, 0);
|
|
3119
|
+
break;
|
|
3120
|
+
case "weekly": {
|
|
3121
|
+
const dow = dayOfWeek ?? 1;
|
|
3122
|
+
next.setDate(next.getDate() + ((7 - next.getDay() + dow) % 7 || 7));
|
|
3123
|
+
next.setHours(0, 0, 0, 0);
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
3126
|
+
case "monthly": {
|
|
3127
|
+
const dom = dayOfMonth ?? 1;
|
|
3128
|
+
next.setMonth(next.getMonth() + 1, dom);
|
|
3129
|
+
next.setHours(0, 0, 0, 0);
|
|
3130
|
+
break;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return next.toISOString();
|
|
3134
|
+
}
|
|
3135
|
+
var AutoInvestManager = class {
|
|
3136
|
+
data = emptyData3();
|
|
3137
|
+
filePath;
|
|
3138
|
+
dir;
|
|
3139
|
+
constructor(configDir) {
|
|
3140
|
+
this.dir = configDir ?? join(homedir(), ".t2000");
|
|
3141
|
+
this.filePath = join(this.dir, "auto-invest.json");
|
|
3142
|
+
this.load();
|
|
3143
|
+
}
|
|
3144
|
+
load() {
|
|
3145
|
+
try {
|
|
3146
|
+
if (existsSync(this.filePath)) {
|
|
3147
|
+
this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
3148
|
+
}
|
|
3149
|
+
} catch {
|
|
3150
|
+
this.data = emptyData3();
|
|
3151
|
+
}
|
|
3152
|
+
if (!this.data.schedules) {
|
|
3153
|
+
this.data.schedules = [];
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
save() {
|
|
3157
|
+
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
3158
|
+
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
3159
|
+
}
|
|
3160
|
+
setup(params) {
|
|
3161
|
+
this.load();
|
|
3162
|
+
if (!params.strategy && !params.asset) {
|
|
3163
|
+
throw new T2000Error("AUTO_INVEST_NOT_FOUND", "Either strategy or asset must be specified");
|
|
3164
|
+
}
|
|
3165
|
+
if (params.amount < 1) {
|
|
3166
|
+
throw new T2000Error("AUTO_INVEST_INSUFFICIENT", "Auto-invest amount must be at least $1");
|
|
3167
|
+
}
|
|
3168
|
+
const schedule = {
|
|
3169
|
+
id: randomUUID().slice(0, 8),
|
|
3170
|
+
strategy: params.strategy,
|
|
3171
|
+
asset: params.asset,
|
|
3172
|
+
amount: params.amount,
|
|
3173
|
+
frequency: params.frequency,
|
|
3174
|
+
dayOfWeek: params.dayOfWeek,
|
|
3175
|
+
dayOfMonth: params.dayOfMonth,
|
|
3176
|
+
nextRun: computeNextRun(params.frequency, params.dayOfWeek, params.dayOfMonth),
|
|
3177
|
+
enabled: true,
|
|
3178
|
+
totalInvested: 0,
|
|
3179
|
+
runCount: 0
|
|
3180
|
+
};
|
|
3181
|
+
this.data.schedules.push(schedule);
|
|
3182
|
+
this.save();
|
|
3183
|
+
return schedule;
|
|
3184
|
+
}
|
|
3185
|
+
getStatus() {
|
|
3186
|
+
this.load();
|
|
3187
|
+
const now = /* @__PURE__ */ new Date();
|
|
3188
|
+
const pending = this.data.schedules.filter(
|
|
3189
|
+
(s) => s.enabled && new Date(s.nextRun) <= now
|
|
3190
|
+
);
|
|
3191
|
+
return {
|
|
3192
|
+
schedules: [...this.data.schedules],
|
|
3193
|
+
pendingRuns: pending
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
getSchedule(id) {
|
|
3197
|
+
this.load();
|
|
3198
|
+
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3199
|
+
if (!schedule) {
|
|
3200
|
+
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3201
|
+
}
|
|
3202
|
+
return schedule;
|
|
3203
|
+
}
|
|
3204
|
+
recordRun(id, amountInvested) {
|
|
3205
|
+
this.load();
|
|
3206
|
+
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3207
|
+
if (!schedule) return;
|
|
3208
|
+
schedule.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
3209
|
+
schedule.nextRun = computeNextRun(schedule.frequency, schedule.dayOfWeek, schedule.dayOfMonth);
|
|
3210
|
+
schedule.totalInvested += amountInvested;
|
|
3211
|
+
schedule.runCount += 1;
|
|
3212
|
+
this.save();
|
|
3213
|
+
}
|
|
3214
|
+
stop(id) {
|
|
3215
|
+
this.load();
|
|
3216
|
+
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3217
|
+
if (!schedule) {
|
|
3218
|
+
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3219
|
+
}
|
|
3220
|
+
schedule.enabled = false;
|
|
3221
|
+
this.save();
|
|
3222
|
+
}
|
|
3223
|
+
remove(id) {
|
|
3224
|
+
this.load();
|
|
3225
|
+
const idx = this.data.schedules.findIndex((s) => s.id === id);
|
|
3226
|
+
if (idx === -1) {
|
|
3227
|
+
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3228
|
+
}
|
|
3229
|
+
this.data.schedules.splice(idx, 1);
|
|
3230
|
+
this.save();
|
|
3231
|
+
}
|
|
2919
3232
|
};
|
|
2920
3233
|
var DEFAULT_CONFIG_DIR = join(homedir(), ".t2000");
|
|
2921
3234
|
var T2000 = class _T2000 extends EventEmitter {
|
|
@@ -2926,6 +3239,8 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
2926
3239
|
enforcer;
|
|
2927
3240
|
contacts;
|
|
2928
3241
|
portfolio;
|
|
3242
|
+
strategies;
|
|
3243
|
+
autoInvest;
|
|
2929
3244
|
constructor(keypair, client, registry, configDir) {
|
|
2930
3245
|
super();
|
|
2931
3246
|
this.keypair = keypair;
|
|
@@ -2936,6 +3251,8 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
2936
3251
|
this.enforcer.load();
|
|
2937
3252
|
this.contacts = new ContactManager(configDir);
|
|
2938
3253
|
this.portfolio = new PortfolioManager(configDir);
|
|
3254
|
+
this.strategies = new StrategyManager(configDir);
|
|
3255
|
+
this.autoInvest = new AutoInvestManager(configDir);
|
|
2939
3256
|
}
|
|
2940
3257
|
static createDefaultRegistry(client) {
|
|
2941
3258
|
const registry = new ProtocolRegistry();
|
|
@@ -3109,6 +3426,14 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
3109
3426
|
investmentCostBasis += pos.costBasis;
|
|
3110
3427
|
}
|
|
3111
3428
|
}
|
|
3429
|
+
for (const key of this.portfolio.getAllStrategyKeys()) {
|
|
3430
|
+
for (const sp of this.portfolio.getStrategyPositions(key)) {
|
|
3431
|
+
if (!(sp.asset in INVESTMENT_ASSETS)) continue;
|
|
3432
|
+
const price = assetPrices[sp.asset] ?? 0;
|
|
3433
|
+
investmentValue += sp.totalAmount * price;
|
|
3434
|
+
investmentCostBasis += sp.costBasis;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3112
3437
|
bal.investment = investmentValue;
|
|
3113
3438
|
bal.investmentPnL = investmentValue - investmentCostBasis;
|
|
3114
3439
|
} catch {
|
|
@@ -4005,6 +4330,252 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4005
4330
|
gasMethod: gasResult.gasMethod
|
|
4006
4331
|
};
|
|
4007
4332
|
}
|
|
4333
|
+
// -- Strategies --
|
|
4334
|
+
async investStrategy(params) {
|
|
4335
|
+
this.enforcer.assertNotLocked();
|
|
4336
|
+
const definition = this.strategies.get(params.strategy);
|
|
4337
|
+
this.strategies.validateMinAmount(definition.allocations, params.usdAmount);
|
|
4338
|
+
if (!params.usdAmount || params.usdAmount <= 0) {
|
|
4339
|
+
throw new T2000Error("INVALID_AMOUNT", "Strategy investment must be > $0");
|
|
4340
|
+
}
|
|
4341
|
+
const bal = await queryBalance(this.client, this._address);
|
|
4342
|
+
if (bal.available < params.usdAmount) {
|
|
4343
|
+
throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient balance. Available: $${bal.available.toFixed(2)}, requested: $${params.usdAmount.toFixed(2)}`);
|
|
4344
|
+
}
|
|
4345
|
+
const buys = [];
|
|
4346
|
+
if (params.dryRun) {
|
|
4347
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
4348
|
+
for (const [asset, pct] of Object.entries(definition.allocations)) {
|
|
4349
|
+
const assetUsd = params.usdAmount * (pct / 100);
|
|
4350
|
+
let estAmount = 0;
|
|
4351
|
+
let estPrice = 0;
|
|
4352
|
+
try {
|
|
4353
|
+
if (swapAdapter) {
|
|
4354
|
+
const quote = await swapAdapter.getQuote("USDC", asset, assetUsd);
|
|
4355
|
+
estAmount = quote.expectedOutput;
|
|
4356
|
+
estPrice = assetUsd / estAmount;
|
|
4357
|
+
}
|
|
4358
|
+
} catch {
|
|
4359
|
+
}
|
|
4360
|
+
buys.push({ asset, usdAmount: assetUsd, amount: estAmount, price: estPrice, tx: "" });
|
|
4361
|
+
}
|
|
4362
|
+
return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: 0, gasMethod: "self-funded" };
|
|
4363
|
+
}
|
|
4364
|
+
let totalGas = 0;
|
|
4365
|
+
let gasMethod = "self-funded";
|
|
4366
|
+
for (const [asset, pct] of Object.entries(definition.allocations)) {
|
|
4367
|
+
const assetUsd = params.usdAmount * (pct / 100);
|
|
4368
|
+
const result = await this.investBuy({ asset, usdAmount: assetUsd });
|
|
4369
|
+
this.portfolio.recordStrategyBuy(params.strategy, {
|
|
4370
|
+
id: `strat_${Date.now()}_${asset}`,
|
|
4371
|
+
type: "buy",
|
|
4372
|
+
asset,
|
|
4373
|
+
amount: result.amount,
|
|
4374
|
+
price: result.price,
|
|
4375
|
+
usdValue: assetUsd,
|
|
4376
|
+
fee: result.fee,
|
|
4377
|
+
tx: result.tx,
|
|
4378
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4379
|
+
});
|
|
4380
|
+
buys.push({ asset, usdAmount: assetUsd, amount: result.amount, price: result.price, tx: result.tx });
|
|
4381
|
+
totalGas += result.gasCost;
|
|
4382
|
+
gasMethod = result.gasMethod;
|
|
4383
|
+
}
|
|
4384
|
+
return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: totalGas, gasMethod };
|
|
4385
|
+
}
|
|
4386
|
+
async sellStrategy(params) {
|
|
4387
|
+
this.enforcer.assertNotLocked();
|
|
4388
|
+
this.strategies.get(params.strategy);
|
|
4389
|
+
const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
4390
|
+
if (stratPositions.length === 0) {
|
|
4391
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
|
|
4392
|
+
}
|
|
4393
|
+
const sells = [];
|
|
4394
|
+
let totalProceeds = 0;
|
|
4395
|
+
let totalPnL = 0;
|
|
4396
|
+
let totalGas = 0;
|
|
4397
|
+
let gasMethod = "self-funded";
|
|
4398
|
+
for (const pos of stratPositions) {
|
|
4399
|
+
const result = await this.investSell({ asset: pos.asset, usdAmount: "all" });
|
|
4400
|
+
const pnl = this.portfolio.recordStrategySell(params.strategy, {
|
|
4401
|
+
id: `strat_sell_${Date.now()}_${pos.asset}`,
|
|
4402
|
+
type: "sell",
|
|
4403
|
+
asset: pos.asset,
|
|
4404
|
+
amount: result.amount,
|
|
4405
|
+
price: result.price,
|
|
4406
|
+
usdValue: result.usdValue,
|
|
4407
|
+
fee: result.fee,
|
|
4408
|
+
tx: result.tx,
|
|
4409
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4410
|
+
});
|
|
4411
|
+
sells.push({ asset: pos.asset, amount: result.amount, usdValue: result.usdValue, realizedPnL: pnl, tx: result.tx });
|
|
4412
|
+
totalProceeds += result.usdValue;
|
|
4413
|
+
totalPnL += pnl;
|
|
4414
|
+
totalGas += result.gasCost;
|
|
4415
|
+
gasMethod = result.gasMethod;
|
|
4416
|
+
}
|
|
4417
|
+
return { success: true, strategy: params.strategy, totalProceeds, realizedPnL: totalPnL, sells, gasCost: totalGas, gasMethod };
|
|
4418
|
+
}
|
|
4419
|
+
async rebalanceStrategy(params) {
|
|
4420
|
+
this.enforcer.assertNotLocked();
|
|
4421
|
+
const definition = this.strategies.get(params.strategy);
|
|
4422
|
+
const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
4423
|
+
if (stratPositions.length === 0) {
|
|
4424
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
|
|
4425
|
+
}
|
|
4426
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
4427
|
+
const prices = {};
|
|
4428
|
+
for (const pos of stratPositions) {
|
|
4429
|
+
try {
|
|
4430
|
+
if (pos.asset === "SUI" && swapAdapter) {
|
|
4431
|
+
prices[pos.asset] = await swapAdapter.getPoolPrice();
|
|
4432
|
+
} else if (swapAdapter) {
|
|
4433
|
+
const q = await swapAdapter.getQuote("USDC", pos.asset, 1);
|
|
4434
|
+
prices[pos.asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
|
|
4435
|
+
}
|
|
4436
|
+
} catch {
|
|
4437
|
+
prices[pos.asset] = 0;
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
const totalValue = stratPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
|
|
4441
|
+
if (totalValue <= 0) {
|
|
4442
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", "Strategy has no value to rebalance");
|
|
4443
|
+
}
|
|
4444
|
+
const currentWeights = {};
|
|
4445
|
+
const beforeWeights = {};
|
|
4446
|
+
for (const pos of stratPositions) {
|
|
4447
|
+
const w = pos.totalAmount * (prices[pos.asset] ?? 0) / totalValue * 100;
|
|
4448
|
+
currentWeights[pos.asset] = w;
|
|
4449
|
+
beforeWeights[pos.asset] = w;
|
|
4450
|
+
}
|
|
4451
|
+
const trades = [];
|
|
4452
|
+
const threshold = 3;
|
|
4453
|
+
for (const [asset, targetPct] of Object.entries(definition.allocations)) {
|
|
4454
|
+
const currentPct = currentWeights[asset] ?? 0;
|
|
4455
|
+
const diff = targetPct - currentPct;
|
|
4456
|
+
if (Math.abs(diff) < threshold) continue;
|
|
4457
|
+
const usdDiff = totalValue * (Math.abs(diff) / 100);
|
|
4458
|
+
if (usdDiff < 1) continue;
|
|
4459
|
+
if (diff > 0) {
|
|
4460
|
+
const result = await this.investBuy({ asset, usdAmount: usdDiff });
|
|
4461
|
+
this.portfolio.recordStrategyBuy(params.strategy, {
|
|
4462
|
+
id: `strat_rebal_${Date.now()}_${asset}`,
|
|
4463
|
+
type: "buy",
|
|
4464
|
+
asset,
|
|
4465
|
+
amount: result.amount,
|
|
4466
|
+
price: result.price,
|
|
4467
|
+
usdValue: usdDiff,
|
|
4468
|
+
fee: result.fee,
|
|
4469
|
+
tx: result.tx,
|
|
4470
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4471
|
+
});
|
|
4472
|
+
trades.push({ action: "buy", asset, usdAmount: usdDiff, amount: result.amount, tx: result.tx });
|
|
4473
|
+
} else {
|
|
4474
|
+
const result = await this.investSell({ asset, usdAmount: usdDiff });
|
|
4475
|
+
this.portfolio.recordStrategySell(params.strategy, {
|
|
4476
|
+
id: `strat_rebal_${Date.now()}_${asset}`,
|
|
4477
|
+
type: "sell",
|
|
4478
|
+
asset,
|
|
4479
|
+
amount: result.amount,
|
|
4480
|
+
price: result.price,
|
|
4481
|
+
usdValue: result.usdValue,
|
|
4482
|
+
fee: result.fee,
|
|
4483
|
+
tx: result.tx,
|
|
4484
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4485
|
+
});
|
|
4486
|
+
trades.push({ action: "sell", asset, usdAmount: result.usdValue, amount: result.amount, tx: result.tx });
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
const afterWeights = {};
|
|
4490
|
+
const updatedPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
4491
|
+
const newTotal = updatedPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
|
|
4492
|
+
for (const p of updatedPositions) {
|
|
4493
|
+
afterWeights[p.asset] = newTotal > 0 ? p.totalAmount * (prices[p.asset] ?? 0) / newTotal * 100 : 0;
|
|
4494
|
+
}
|
|
4495
|
+
return { success: true, strategy: params.strategy, trades, beforeWeights, afterWeights, targetWeights: { ...definition.allocations } };
|
|
4496
|
+
}
|
|
4497
|
+
async getStrategyStatus(name) {
|
|
4498
|
+
const definition = this.strategies.get(name);
|
|
4499
|
+
const stratPositions = this.portfolio.getStrategyPositions(name);
|
|
4500
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
4501
|
+
const prices = {};
|
|
4502
|
+
for (const asset of Object.keys(definition.allocations)) {
|
|
4503
|
+
try {
|
|
4504
|
+
if (asset === "SUI" && swapAdapter) {
|
|
4505
|
+
prices[asset] = await swapAdapter.getPoolPrice();
|
|
4506
|
+
} else if (swapAdapter) {
|
|
4507
|
+
const q = await swapAdapter.getQuote("USDC", asset, 1);
|
|
4508
|
+
prices[asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
|
|
4509
|
+
}
|
|
4510
|
+
} catch {
|
|
4511
|
+
prices[asset] = 0;
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
const positions = stratPositions.map((sp) => {
|
|
4515
|
+
const price = prices[sp.asset] ?? 0;
|
|
4516
|
+
const currentValue = sp.totalAmount * price;
|
|
4517
|
+
const pnl = currentValue - sp.costBasis;
|
|
4518
|
+
return {
|
|
4519
|
+
asset: sp.asset,
|
|
4520
|
+
totalAmount: sp.totalAmount,
|
|
4521
|
+
costBasis: sp.costBasis,
|
|
4522
|
+
avgPrice: sp.avgPrice,
|
|
4523
|
+
currentPrice: price,
|
|
4524
|
+
currentValue,
|
|
4525
|
+
unrealizedPnL: pnl,
|
|
4526
|
+
unrealizedPnLPct: sp.costBasis > 0 ? pnl / sp.costBasis * 100 : 0,
|
|
4527
|
+
trades: sp.trades
|
|
4528
|
+
};
|
|
4529
|
+
});
|
|
4530
|
+
const totalValue = positions.reduce((s, p) => s + p.currentValue, 0);
|
|
4531
|
+
const currentWeights = {};
|
|
4532
|
+
for (const p of positions) {
|
|
4533
|
+
currentWeights[p.asset] = totalValue > 0 ? p.currentValue / totalValue * 100 : 0;
|
|
4534
|
+
}
|
|
4535
|
+
return { definition, positions, currentWeights, totalValue };
|
|
4536
|
+
}
|
|
4537
|
+
// -- Auto-Invest --
|
|
4538
|
+
setupAutoInvest(params) {
|
|
4539
|
+
if (params.strategy) this.strategies.get(params.strategy);
|
|
4540
|
+
if (params.asset && !(params.asset in INVESTMENT_ASSETS)) {
|
|
4541
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not an investment asset`);
|
|
4542
|
+
}
|
|
4543
|
+
return this.autoInvest.setup(params);
|
|
4544
|
+
}
|
|
4545
|
+
getAutoInvestStatus() {
|
|
4546
|
+
return this.autoInvest.getStatus();
|
|
4547
|
+
}
|
|
4548
|
+
async runAutoInvest() {
|
|
4549
|
+
this.enforcer.assertNotLocked();
|
|
4550
|
+
const status = this.autoInvest.getStatus();
|
|
4551
|
+
const executed = [];
|
|
4552
|
+
const skipped = [];
|
|
4553
|
+
for (const schedule of status.pendingRuns) {
|
|
4554
|
+
try {
|
|
4555
|
+
const bal = await queryBalance(this.client, this._address);
|
|
4556
|
+
if (bal.available < schedule.amount) {
|
|
4557
|
+
skipped.push({ scheduleId: schedule.id, reason: `Insufficient balance ($${bal.available.toFixed(2)} < $${schedule.amount})` });
|
|
4558
|
+
continue;
|
|
4559
|
+
}
|
|
4560
|
+
if (schedule.strategy) {
|
|
4561
|
+
const result = await this.investStrategy({ strategy: schedule.strategy, usdAmount: schedule.amount });
|
|
4562
|
+
this.autoInvest.recordRun(schedule.id, schedule.amount);
|
|
4563
|
+
executed.push({ scheduleId: schedule.id, strategy: schedule.strategy, amount: schedule.amount, result });
|
|
4564
|
+
} else if (schedule.asset) {
|
|
4565
|
+
const result = await this.investBuy({ asset: schedule.asset, usdAmount: schedule.amount });
|
|
4566
|
+
this.autoInvest.recordRun(schedule.id, schedule.amount);
|
|
4567
|
+
executed.push({ scheduleId: schedule.id, asset: schedule.asset, amount: schedule.amount, result });
|
|
4568
|
+
}
|
|
4569
|
+
} catch (err) {
|
|
4570
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4571
|
+
skipped.push({ scheduleId: schedule.id, reason: msg });
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
return { executed, skipped };
|
|
4575
|
+
}
|
|
4576
|
+
stopAutoInvest(id) {
|
|
4577
|
+
this.autoInvest.stop(id);
|
|
4578
|
+
}
|
|
4008
4579
|
async getPortfolio() {
|
|
4009
4580
|
const positions = this.portfolio.getPositions();
|
|
4010
4581
|
const realizedPnL = this.portfolio.getRealizedPnL();
|
|
@@ -4022,12 +4593,11 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4022
4593
|
prices[asset] = 0;
|
|
4023
4594
|
}
|
|
4024
4595
|
}
|
|
4025
|
-
const
|
|
4026
|
-
for (const pos of positions) {
|
|
4596
|
+
const enrichPosition = async (pos, adjustWallet) => {
|
|
4027
4597
|
const currentPrice = prices[pos.asset] ?? 0;
|
|
4028
4598
|
let totalAmount = pos.totalAmount;
|
|
4029
4599
|
let costBasis = pos.costBasis;
|
|
4030
|
-
if (pos.asset in INVESTMENT_ASSETS && !pos.earning) {
|
|
4600
|
+
if (adjustWallet && pos.asset in INVESTMENT_ASSETS && !pos.earning) {
|
|
4031
4601
|
try {
|
|
4032
4602
|
const assetInfo = SUPPORTED_ASSETS[pos.asset];
|
|
4033
4603
|
const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
@@ -4045,7 +4615,7 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4045
4615
|
const currentValue = totalAmount * currentPrice;
|
|
4046
4616
|
const unrealizedPnL = currentPrice > 0 ? currentValue - costBasis : 0;
|
|
4047
4617
|
const unrealizedPnLPct = currentPrice > 0 && costBasis > 0 ? unrealizedPnL / costBasis * 100 : 0;
|
|
4048
|
-
|
|
4618
|
+
return {
|
|
4049
4619
|
asset: pos.asset,
|
|
4050
4620
|
totalAmount,
|
|
4051
4621
|
costBasis,
|
|
@@ -4058,13 +4628,29 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4058
4628
|
earning: pos.earning,
|
|
4059
4629
|
earningProtocol: pos.earningProtocol,
|
|
4060
4630
|
earningApy: pos.earningApy
|
|
4061
|
-
}
|
|
4631
|
+
};
|
|
4632
|
+
};
|
|
4633
|
+
const enriched = [];
|
|
4634
|
+
for (const pos of positions) {
|
|
4635
|
+
enriched.push(await enrichPosition(pos, true));
|
|
4636
|
+
}
|
|
4637
|
+
const strategyPositions = {};
|
|
4638
|
+
for (const key of this.portfolio.getAllStrategyKeys()) {
|
|
4639
|
+
const sps = this.portfolio.getStrategyPositions(key);
|
|
4640
|
+
const enrichedStrat = [];
|
|
4641
|
+
for (const sp of sps) {
|
|
4642
|
+
enrichedStrat.push(await enrichPosition(sp, false));
|
|
4643
|
+
}
|
|
4644
|
+
if (enrichedStrat.length > 0) {
|
|
4645
|
+
strategyPositions[key] = enrichedStrat;
|
|
4646
|
+
}
|
|
4062
4647
|
}
|
|
4063
|
-
const
|
|
4064
|
-
const
|
|
4648
|
+
const allPositions = [...enriched, ...Object.values(strategyPositions).flat()];
|
|
4649
|
+
const totalInvested = allPositions.reduce((sum, p) => sum + p.costBasis, 0);
|
|
4650
|
+
const totalValue = allPositions.reduce((sum, p) => sum + p.currentValue, 0);
|
|
4065
4651
|
const totalUnrealizedPnL = totalValue - totalInvested;
|
|
4066
4652
|
const totalUnrealizedPnLPct = totalInvested > 0 ? totalUnrealizedPnL / totalInvested * 100 : 0;
|
|
4067
|
-
|
|
4653
|
+
const result = {
|
|
4068
4654
|
positions: enriched,
|
|
4069
4655
|
totalInvested,
|
|
4070
4656
|
totalValue,
|
|
@@ -4072,6 +4658,10 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4072
4658
|
unrealizedPnLPct: totalUnrealizedPnLPct,
|
|
4073
4659
|
realizedPnL
|
|
4074
4660
|
};
|
|
4661
|
+
if (Object.keys(strategyPositions).length > 0) {
|
|
4662
|
+
result.strategyPositions = strategyPositions;
|
|
4663
|
+
}
|
|
4664
|
+
return result;
|
|
4075
4665
|
}
|
|
4076
4666
|
// -- Info --
|
|
4077
4667
|
async positions() {
|
|
@@ -4562,6 +5152,6 @@ var allDescriptors = [
|
|
|
4562
5152
|
descriptor
|
|
4563
5153
|
];
|
|
4564
5154
|
|
|
4565
|
-
export { BPS_DENOMINATOR, CLOCK_ID, CetusAdapter, ContactManager, DEFAULT_MAX_LEVERAGE, DEFAULT_MAX_POSITION_SIZE, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, GAS_RESERVE_MIN, INVESTMENT_ASSETS, MIST_PER_SUI, NaviAdapter, OUTBOUND_OPS, PERPS_MARKETS, PortfolioManager, ProtocolRegistry, SENTINEL, SUI_DECIMALS, SUPPORTED_ASSETS, SafeguardEnforcer, SafeguardError, SuilendAdapter, T2000, T2000Error, USDC_DECIMALS, addCollectFeeToTx, allDescriptors, calculateFee, descriptor3 as cetusDescriptor, executeAutoTopUp, executeWithGas, exportPrivateKey, formatAssetAmount, formatSui, formatUsd, generateKeypair, getAddress, getDecimals, getGasStatus, getPoolPrice, getRates, getSentinelInfo, keypairFromPrivateKey, listSentinels, loadKey, mapMoveAbortCode, mapWalletError, mistToSui, descriptor2 as naviDescriptor, rawToStable, rawToUsdc, requestAttack, saveKey, attack as sentinelAttack, descriptor as sentinelDescriptor, settleAttack, shouldAutoTopUp, simulateTransaction, solveHashcash, stableToRaw, submitPrompt, suiToMist, descriptor4 as suilendDescriptor, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, walletExists };
|
|
5155
|
+
export { AutoInvestManager, BPS_DENOMINATOR, CLOCK_ID, CetusAdapter, ContactManager, DEFAULT_MAX_LEVERAGE, DEFAULT_MAX_POSITION_SIZE, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, DEFAULT_STRATEGIES, GAS_RESERVE_MIN, INVESTMENT_ASSETS, MIST_PER_SUI, NaviAdapter, OUTBOUND_OPS, PERPS_MARKETS, PortfolioManager, ProtocolRegistry, SENTINEL, SUI_DECIMALS, SUPPORTED_ASSETS, SafeguardEnforcer, SafeguardError, StrategyManager, SuilendAdapter, T2000, T2000Error, USDC_DECIMALS, addCollectFeeToTx, allDescriptors, calculateFee, descriptor3 as cetusDescriptor, executeAutoTopUp, executeWithGas, exportPrivateKey, formatAssetAmount, formatSui, formatUsd, generateKeypair, getAddress, getDecimals, getGasStatus, getPoolPrice, getRates, getSentinelInfo, keypairFromPrivateKey, listSentinels, loadKey, mapMoveAbortCode, mapWalletError, mistToSui, descriptor2 as naviDescriptor, rawToStable, rawToUsdc, requestAttack, saveKey, attack as sentinelAttack, descriptor as sentinelDescriptor, settleAttack, shouldAutoTopUp, simulateTransaction, solveHashcash, stableToRaw, submitPrompt, suiToMist, descriptor4 as suilendDescriptor, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, walletExists };
|
|
4566
5156
|
//# sourceMappingURL=index.js.map
|
|
4567
5157
|
//# sourceMappingURL=index.js.map
|