@t2000/sdk 0.15.2 → 0.16.0
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 +615 -11
- 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 +614 -13
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/LICENSE +0 -21
package/dist/index.cjs
CHANGED
|
@@ -87,6 +87,26 @@ var INVESTMENT_ASSETS = {
|
|
|
87
87
|
BTC: SUPPORTED_ASSETS.BTC,
|
|
88
88
|
ETH: SUPPORTED_ASSETS.ETH
|
|
89
89
|
};
|
|
90
|
+
var DEFAULT_STRATEGIES = {
|
|
91
|
+
bluechip: {
|
|
92
|
+
name: "Bluechip / Large-Cap",
|
|
93
|
+
allocations: { BTC: 50, ETH: 30, SUI: 20 },
|
|
94
|
+
description: "Large-cap crypto index",
|
|
95
|
+
custom: false
|
|
96
|
+
},
|
|
97
|
+
layer1: {
|
|
98
|
+
name: "Smart Contract Platforms",
|
|
99
|
+
allocations: { ETH: 50, SUI: 50 },
|
|
100
|
+
description: "Smart contract platforms",
|
|
101
|
+
custom: false
|
|
102
|
+
},
|
|
103
|
+
"sui-heavy": {
|
|
104
|
+
name: "Sui-Weighted Portfolio",
|
|
105
|
+
allocations: { BTC: 20, ETH: 20, SUI: 60 },
|
|
106
|
+
description: "Sui-weighted portfolio",
|
|
107
|
+
custom: false
|
|
108
|
+
}
|
|
109
|
+
};
|
|
90
110
|
var PERPS_MARKETS = ["SUI-PERP"];
|
|
91
111
|
var DEFAULT_MAX_LEVERAGE = 5;
|
|
92
112
|
var DEFAULT_MAX_POSITION_SIZE = 1e3;
|
|
@@ -2818,7 +2838,7 @@ var ContactManager = class {
|
|
|
2818
2838
|
}
|
|
2819
2839
|
};
|
|
2820
2840
|
function emptyData() {
|
|
2821
|
-
return { positions: {}, realizedPnL: 0 };
|
|
2841
|
+
return { positions: {}, strategies: {}, realizedPnL: 0 };
|
|
2822
2842
|
}
|
|
2823
2843
|
var PortfolioManager = class {
|
|
2824
2844
|
data = emptyData();
|
|
@@ -2833,6 +2853,7 @@ var PortfolioManager = class {
|
|
|
2833
2853
|
try {
|
|
2834
2854
|
if (fs.existsSync(this.filePath)) {
|
|
2835
2855
|
this.data = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
|
|
2856
|
+
if (!this.data.strategies) this.data.strategies = {};
|
|
2836
2857
|
}
|
|
2837
2858
|
} catch {
|
|
2838
2859
|
this.data = emptyData();
|
|
@@ -2918,6 +2939,299 @@ var PortfolioManager = class {
|
|
|
2918
2939
|
this.load();
|
|
2919
2940
|
return this.data.realizedPnL;
|
|
2920
2941
|
}
|
|
2942
|
+
// --- Strategy position tracking ---
|
|
2943
|
+
recordStrategyBuy(strategyKey, trade) {
|
|
2944
|
+
this.load();
|
|
2945
|
+
if (!this.data.strategies[strategyKey]) {
|
|
2946
|
+
this.data.strategies[strategyKey] = {};
|
|
2947
|
+
}
|
|
2948
|
+
const bucket = this.data.strategies[strategyKey];
|
|
2949
|
+
const pos = bucket[trade.asset] ?? { totalAmount: 0, costBasis: 0, avgPrice: 0, trades: [] };
|
|
2950
|
+
pos.totalAmount += trade.amount;
|
|
2951
|
+
pos.costBasis += trade.usdValue;
|
|
2952
|
+
pos.avgPrice = pos.costBasis / pos.totalAmount;
|
|
2953
|
+
pos.trades.push(trade);
|
|
2954
|
+
bucket[trade.asset] = pos;
|
|
2955
|
+
this.save();
|
|
2956
|
+
}
|
|
2957
|
+
recordStrategySell(strategyKey, trade) {
|
|
2958
|
+
this.load();
|
|
2959
|
+
const bucket = this.data.strategies[strategyKey];
|
|
2960
|
+
if (!bucket) {
|
|
2961
|
+
throw new T2000Error("STRATEGY_NOT_FOUND", `No positions for strategy '${strategyKey}'`);
|
|
2962
|
+
}
|
|
2963
|
+
const pos = bucket[trade.asset];
|
|
2964
|
+
if (!pos || pos.totalAmount <= 0) {
|
|
2965
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position in strategy '${strategyKey}'`);
|
|
2966
|
+
}
|
|
2967
|
+
const sellAmount = Math.min(trade.amount, pos.totalAmount);
|
|
2968
|
+
const costOfSold = pos.avgPrice * sellAmount;
|
|
2969
|
+
const realizedPnL = trade.usdValue - costOfSold;
|
|
2970
|
+
pos.totalAmount -= sellAmount;
|
|
2971
|
+
pos.costBasis -= costOfSold;
|
|
2972
|
+
if (pos.totalAmount < 1e-6) {
|
|
2973
|
+
pos.totalAmount = 0;
|
|
2974
|
+
pos.costBasis = 0;
|
|
2975
|
+
pos.avgPrice = 0;
|
|
2976
|
+
}
|
|
2977
|
+
pos.trades.push(trade);
|
|
2978
|
+
this.data.realizedPnL += realizedPnL;
|
|
2979
|
+
bucket[trade.asset] = pos;
|
|
2980
|
+
const hasPositions = Object.values(bucket).some((p) => p.totalAmount > 0);
|
|
2981
|
+
if (!hasPositions) {
|
|
2982
|
+
delete this.data.strategies[strategyKey];
|
|
2983
|
+
}
|
|
2984
|
+
this.save();
|
|
2985
|
+
return realizedPnL;
|
|
2986
|
+
}
|
|
2987
|
+
getStrategyPositions(strategyKey) {
|
|
2988
|
+
this.load();
|
|
2989
|
+
const bucket = this.data.strategies[strategyKey];
|
|
2990
|
+
if (!bucket) return [];
|
|
2991
|
+
return Object.entries(bucket).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
|
|
2992
|
+
}
|
|
2993
|
+
getAllStrategyKeys() {
|
|
2994
|
+
this.load();
|
|
2995
|
+
return Object.keys(this.data.strategies);
|
|
2996
|
+
}
|
|
2997
|
+
hasStrategyPositions(strategyKey) {
|
|
2998
|
+
this.load();
|
|
2999
|
+
const bucket = this.data.strategies[strategyKey];
|
|
3000
|
+
if (!bucket) return false;
|
|
3001
|
+
return Object.values(bucket).some((p) => p.totalAmount > 0);
|
|
3002
|
+
}
|
|
3003
|
+
};
|
|
3004
|
+
function emptyData2() {
|
|
3005
|
+
return { strategies: {} };
|
|
3006
|
+
}
|
|
3007
|
+
var StrategyManager = class {
|
|
3008
|
+
data = emptyData2();
|
|
3009
|
+
filePath;
|
|
3010
|
+
dir;
|
|
3011
|
+
seeded = false;
|
|
3012
|
+
constructor(configDir) {
|
|
3013
|
+
this.dir = configDir ?? path.join(os.homedir(), ".t2000");
|
|
3014
|
+
this.filePath = path.join(this.dir, "strategies.json");
|
|
3015
|
+
this.load();
|
|
3016
|
+
}
|
|
3017
|
+
load() {
|
|
3018
|
+
try {
|
|
3019
|
+
if (fs.existsSync(this.filePath)) {
|
|
3020
|
+
this.data = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
|
|
3021
|
+
}
|
|
3022
|
+
} catch {
|
|
3023
|
+
this.data = emptyData2();
|
|
3024
|
+
}
|
|
3025
|
+
if (!this.seeded) {
|
|
3026
|
+
this.seedDefaults();
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
save() {
|
|
3030
|
+
if (!fs.existsSync(this.dir)) fs.mkdirSync(this.dir, { recursive: true });
|
|
3031
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
3032
|
+
}
|
|
3033
|
+
seedDefaults() {
|
|
3034
|
+
this.seeded = true;
|
|
3035
|
+
let changed = false;
|
|
3036
|
+
for (const [key, def] of Object.entries(DEFAULT_STRATEGIES)) {
|
|
3037
|
+
if (!this.data.strategies[key]) {
|
|
3038
|
+
this.data.strategies[key] = { ...def, allocations: { ...def.allocations } };
|
|
3039
|
+
changed = true;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (changed) this.save();
|
|
3043
|
+
}
|
|
3044
|
+
getAll() {
|
|
3045
|
+
this.load();
|
|
3046
|
+
return { ...this.data.strategies };
|
|
3047
|
+
}
|
|
3048
|
+
get(name) {
|
|
3049
|
+
this.load();
|
|
3050
|
+
const strategy = this.data.strategies[name];
|
|
3051
|
+
if (!strategy) {
|
|
3052
|
+
throw new T2000Error("STRATEGY_NOT_FOUND", `Strategy '${name}' not found`);
|
|
3053
|
+
}
|
|
3054
|
+
return strategy;
|
|
3055
|
+
}
|
|
3056
|
+
create(params) {
|
|
3057
|
+
this.load();
|
|
3058
|
+
const key = params.name.toLowerCase().replace(/\s+/g, "-");
|
|
3059
|
+
if (this.data.strategies[key]) {
|
|
3060
|
+
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Strategy '${key}' already exists`);
|
|
3061
|
+
}
|
|
3062
|
+
this.validateAllocations(params.allocations);
|
|
3063
|
+
const definition = {
|
|
3064
|
+
name: params.name,
|
|
3065
|
+
allocations: { ...params.allocations },
|
|
3066
|
+
description: params.description ?? `Custom strategy: ${params.name}`,
|
|
3067
|
+
custom: true
|
|
3068
|
+
};
|
|
3069
|
+
this.data.strategies[key] = definition;
|
|
3070
|
+
this.save();
|
|
3071
|
+
return definition;
|
|
3072
|
+
}
|
|
3073
|
+
delete(name) {
|
|
3074
|
+
this.load();
|
|
3075
|
+
const strategy = this.data.strategies[name];
|
|
3076
|
+
if (!strategy) {
|
|
3077
|
+
throw new T2000Error("STRATEGY_NOT_FOUND", `Strategy '${name}' not found`);
|
|
3078
|
+
}
|
|
3079
|
+
if (!strategy.custom) {
|
|
3080
|
+
throw new T2000Error("STRATEGY_BUILTIN", `Cannot delete built-in strategy '${name}'`);
|
|
3081
|
+
}
|
|
3082
|
+
delete this.data.strategies[name];
|
|
3083
|
+
this.save();
|
|
3084
|
+
}
|
|
3085
|
+
validateAllocations(allocations) {
|
|
3086
|
+
const total = Object.values(allocations).reduce((sum, pct) => sum + pct, 0);
|
|
3087
|
+
if (Math.abs(total - 100) > 0.01) {
|
|
3088
|
+
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Allocations must sum to 100 (got ${total})`);
|
|
3089
|
+
}
|
|
3090
|
+
for (const asset of Object.keys(allocations)) {
|
|
3091
|
+
if (!(asset in INVESTMENT_ASSETS)) {
|
|
3092
|
+
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `${asset} is not an investment asset`);
|
|
3093
|
+
}
|
|
3094
|
+
if (allocations[asset] <= 0) {
|
|
3095
|
+
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Allocation for ${asset} must be > 0`);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
validateMinAmount(allocations, totalUsd) {
|
|
3100
|
+
const assetCount = Object.keys(allocations).length;
|
|
3101
|
+
for (const [asset, pct] of Object.entries(allocations)) {
|
|
3102
|
+
const assetUsd = totalUsd * (pct / 100);
|
|
3103
|
+
if (assetUsd < 1) {
|
|
3104
|
+
throw new T2000Error(
|
|
3105
|
+
"STRATEGY_MIN_AMOUNT",
|
|
3106
|
+
`Minimum $1 per asset in strategy. Need at least $${assetCount} for ${assetCount}-asset strategy.`
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
};
|
|
3112
|
+
function emptyData3() {
|
|
3113
|
+
return { schedules: [] };
|
|
3114
|
+
}
|
|
3115
|
+
function computeNextRun(frequency, dayOfWeek, dayOfMonth, from) {
|
|
3116
|
+
const base = /* @__PURE__ */ new Date();
|
|
3117
|
+
const next = new Date(base);
|
|
3118
|
+
switch (frequency) {
|
|
3119
|
+
case "daily":
|
|
3120
|
+
next.setDate(next.getDate() + 1);
|
|
3121
|
+
next.setHours(0, 0, 0, 0);
|
|
3122
|
+
break;
|
|
3123
|
+
case "weekly": {
|
|
3124
|
+
const dow = dayOfWeek ?? 1;
|
|
3125
|
+
next.setDate(next.getDate() + ((7 - next.getDay() + dow) % 7 || 7));
|
|
3126
|
+
next.setHours(0, 0, 0, 0);
|
|
3127
|
+
break;
|
|
3128
|
+
}
|
|
3129
|
+
case "monthly": {
|
|
3130
|
+
const dom = dayOfMonth ?? 1;
|
|
3131
|
+
next.setMonth(next.getMonth() + 1, dom);
|
|
3132
|
+
next.setHours(0, 0, 0, 0);
|
|
3133
|
+
break;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
return next.toISOString();
|
|
3137
|
+
}
|
|
3138
|
+
var AutoInvestManager = class {
|
|
3139
|
+
data = emptyData3();
|
|
3140
|
+
filePath;
|
|
3141
|
+
dir;
|
|
3142
|
+
constructor(configDir) {
|
|
3143
|
+
this.dir = configDir ?? path.join(os.homedir(), ".t2000");
|
|
3144
|
+
this.filePath = path.join(this.dir, "auto-invest.json");
|
|
3145
|
+
this.load();
|
|
3146
|
+
}
|
|
3147
|
+
load() {
|
|
3148
|
+
try {
|
|
3149
|
+
if (fs.existsSync(this.filePath)) {
|
|
3150
|
+
this.data = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
|
|
3151
|
+
}
|
|
3152
|
+
} catch {
|
|
3153
|
+
this.data = emptyData3();
|
|
3154
|
+
}
|
|
3155
|
+
if (!this.data.schedules) {
|
|
3156
|
+
this.data.schedules = [];
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
save() {
|
|
3160
|
+
if (!fs.existsSync(this.dir)) fs.mkdirSync(this.dir, { recursive: true });
|
|
3161
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
3162
|
+
}
|
|
3163
|
+
setup(params) {
|
|
3164
|
+
this.load();
|
|
3165
|
+
if (!params.strategy && !params.asset) {
|
|
3166
|
+
throw new T2000Error("AUTO_INVEST_NOT_FOUND", "Either strategy or asset must be specified");
|
|
3167
|
+
}
|
|
3168
|
+
if (params.amount < 1) {
|
|
3169
|
+
throw new T2000Error("AUTO_INVEST_INSUFFICIENT", "Auto-invest amount must be at least $1");
|
|
3170
|
+
}
|
|
3171
|
+
const schedule = {
|
|
3172
|
+
id: crypto.randomUUID().slice(0, 8),
|
|
3173
|
+
strategy: params.strategy,
|
|
3174
|
+
asset: params.asset,
|
|
3175
|
+
amount: params.amount,
|
|
3176
|
+
frequency: params.frequency,
|
|
3177
|
+
dayOfWeek: params.dayOfWeek,
|
|
3178
|
+
dayOfMonth: params.dayOfMonth,
|
|
3179
|
+
nextRun: computeNextRun(params.frequency, params.dayOfWeek, params.dayOfMonth),
|
|
3180
|
+
enabled: true,
|
|
3181
|
+
totalInvested: 0,
|
|
3182
|
+
runCount: 0
|
|
3183
|
+
};
|
|
3184
|
+
this.data.schedules.push(schedule);
|
|
3185
|
+
this.save();
|
|
3186
|
+
return schedule;
|
|
3187
|
+
}
|
|
3188
|
+
getStatus() {
|
|
3189
|
+
this.load();
|
|
3190
|
+
const now = /* @__PURE__ */ new Date();
|
|
3191
|
+
const pending = this.data.schedules.filter(
|
|
3192
|
+
(s) => s.enabled && new Date(s.nextRun) <= now
|
|
3193
|
+
);
|
|
3194
|
+
return {
|
|
3195
|
+
schedules: [...this.data.schedules],
|
|
3196
|
+
pendingRuns: pending
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
getSchedule(id) {
|
|
3200
|
+
this.load();
|
|
3201
|
+
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3202
|
+
if (!schedule) {
|
|
3203
|
+
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3204
|
+
}
|
|
3205
|
+
return schedule;
|
|
3206
|
+
}
|
|
3207
|
+
recordRun(id, amountInvested) {
|
|
3208
|
+
this.load();
|
|
3209
|
+
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3210
|
+
if (!schedule) return;
|
|
3211
|
+
schedule.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
3212
|
+
schedule.nextRun = computeNextRun(schedule.frequency, schedule.dayOfWeek, schedule.dayOfMonth);
|
|
3213
|
+
schedule.totalInvested += amountInvested;
|
|
3214
|
+
schedule.runCount += 1;
|
|
3215
|
+
this.save();
|
|
3216
|
+
}
|
|
3217
|
+
stop(id) {
|
|
3218
|
+
this.load();
|
|
3219
|
+
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3220
|
+
if (!schedule) {
|
|
3221
|
+
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3222
|
+
}
|
|
3223
|
+
schedule.enabled = false;
|
|
3224
|
+
this.save();
|
|
3225
|
+
}
|
|
3226
|
+
remove(id) {
|
|
3227
|
+
this.load();
|
|
3228
|
+
const idx = this.data.schedules.findIndex((s) => s.id === id);
|
|
3229
|
+
if (idx === -1) {
|
|
3230
|
+
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3231
|
+
}
|
|
3232
|
+
this.data.schedules.splice(idx, 1);
|
|
3233
|
+
this.save();
|
|
3234
|
+
}
|
|
2921
3235
|
};
|
|
2922
3236
|
var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".t2000");
|
|
2923
3237
|
var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
@@ -2928,6 +3242,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
2928
3242
|
enforcer;
|
|
2929
3243
|
contacts;
|
|
2930
3244
|
portfolio;
|
|
3245
|
+
strategies;
|
|
3246
|
+
autoInvest;
|
|
2931
3247
|
constructor(keypair, client, registry, configDir) {
|
|
2932
3248
|
super();
|
|
2933
3249
|
this.keypair = keypair;
|
|
@@ -2938,6 +3254,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
2938
3254
|
this.enforcer.load();
|
|
2939
3255
|
this.contacts = new ContactManager(configDir);
|
|
2940
3256
|
this.portfolio = new PortfolioManager(configDir);
|
|
3257
|
+
this.strategies = new StrategyManager(configDir);
|
|
3258
|
+
this.autoInvest = new AutoInvestManager(configDir);
|
|
2941
3259
|
}
|
|
2942
3260
|
static createDefaultRegistry(client) {
|
|
2943
3261
|
const registry = new ProtocolRegistry();
|
|
@@ -3056,9 +3374,12 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
3056
3374
|
}
|
|
3057
3375
|
async balance() {
|
|
3058
3376
|
const bal = await queryBalance(this.client, this._address);
|
|
3377
|
+
const earningAssets = new Set(
|
|
3378
|
+
this.portfolio.getPositions().filter((p) => p.earning).map((p) => p.asset)
|
|
3379
|
+
);
|
|
3059
3380
|
try {
|
|
3060
3381
|
const positions = await this.positions();
|
|
3061
|
-
const savings = positions.positions.filter((p) => p.type === "save").reduce((sum, p) => sum + p.amount, 0);
|
|
3382
|
+
const savings = positions.positions.filter((p) => p.type === "save").filter((p) => !earningAssets.has(p.asset)).reduce((sum, p) => sum + p.amount, 0);
|
|
3062
3383
|
const debt = positions.positions.filter((p) => p.type === "borrow").reduce((sum, p) => sum + p.amount, 0);
|
|
3063
3384
|
bal.savings = savings;
|
|
3064
3385
|
bal.debt = debt;
|
|
@@ -3086,7 +3407,14 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
3086
3407
|
for (const pos of portfolioPositions) {
|
|
3087
3408
|
if (!(pos.asset in INVESTMENT_ASSETS)) continue;
|
|
3088
3409
|
const price = assetPrices[pos.asset] ?? 0;
|
|
3089
|
-
if (pos.
|
|
3410
|
+
if (pos.earning) {
|
|
3411
|
+
investmentValue += pos.totalAmount * price;
|
|
3412
|
+
investmentCostBasis += pos.costBasis;
|
|
3413
|
+
if (pos.asset === "SUI") {
|
|
3414
|
+
const gasSui = Math.max(0, bal.gasReserve.sui);
|
|
3415
|
+
bal.gasReserve = { sui: gasSui, usdEquiv: gasSui * price };
|
|
3416
|
+
}
|
|
3417
|
+
} else if (pos.asset === "SUI") {
|
|
3090
3418
|
const actualHeld = Math.min(pos.totalAmount, bal.gasReserve.sui);
|
|
3091
3419
|
investmentValue += actualHeld * price;
|
|
3092
3420
|
if (actualHeld < pos.totalAmount && pos.totalAmount > 0) {
|
|
@@ -3101,6 +3429,14 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
3101
3429
|
investmentCostBasis += pos.costBasis;
|
|
3102
3430
|
}
|
|
3103
3431
|
}
|
|
3432
|
+
for (const key of this.portfolio.getAllStrategyKeys()) {
|
|
3433
|
+
for (const sp of this.portfolio.getStrategyPositions(key)) {
|
|
3434
|
+
if (!(sp.asset in INVESTMENT_ASSETS)) continue;
|
|
3435
|
+
const price = assetPrices[sp.asset] ?? 0;
|
|
3436
|
+
investmentValue += sp.totalAmount * price;
|
|
3437
|
+
investmentCostBasis += sp.costBasis;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3104
3440
|
bal.investment = investmentValue;
|
|
3105
3441
|
bal.investmentPnL = investmentValue - investmentCostBasis;
|
|
3106
3442
|
} catch {
|
|
@@ -3997,6 +4333,252 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
3997
4333
|
gasMethod: gasResult.gasMethod
|
|
3998
4334
|
};
|
|
3999
4335
|
}
|
|
4336
|
+
// -- Strategies --
|
|
4337
|
+
async investStrategy(params) {
|
|
4338
|
+
this.enforcer.assertNotLocked();
|
|
4339
|
+
const definition = this.strategies.get(params.strategy);
|
|
4340
|
+
this.strategies.validateMinAmount(definition.allocations, params.usdAmount);
|
|
4341
|
+
if (!params.usdAmount || params.usdAmount <= 0) {
|
|
4342
|
+
throw new T2000Error("INVALID_AMOUNT", "Strategy investment must be > $0");
|
|
4343
|
+
}
|
|
4344
|
+
const bal = await queryBalance(this.client, this._address);
|
|
4345
|
+
if (bal.available < params.usdAmount) {
|
|
4346
|
+
throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient balance. Available: $${bal.available.toFixed(2)}, requested: $${params.usdAmount.toFixed(2)}`);
|
|
4347
|
+
}
|
|
4348
|
+
const buys = [];
|
|
4349
|
+
if (params.dryRun) {
|
|
4350
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
4351
|
+
for (const [asset, pct] of Object.entries(definition.allocations)) {
|
|
4352
|
+
const assetUsd = params.usdAmount * (pct / 100);
|
|
4353
|
+
let estAmount = 0;
|
|
4354
|
+
let estPrice = 0;
|
|
4355
|
+
try {
|
|
4356
|
+
if (swapAdapter) {
|
|
4357
|
+
const quote = await swapAdapter.getQuote("USDC", asset, assetUsd);
|
|
4358
|
+
estAmount = quote.expectedOutput;
|
|
4359
|
+
estPrice = assetUsd / estAmount;
|
|
4360
|
+
}
|
|
4361
|
+
} catch {
|
|
4362
|
+
}
|
|
4363
|
+
buys.push({ asset, usdAmount: assetUsd, amount: estAmount, price: estPrice, tx: "" });
|
|
4364
|
+
}
|
|
4365
|
+
return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: 0, gasMethod: "self-funded" };
|
|
4366
|
+
}
|
|
4367
|
+
let totalGas = 0;
|
|
4368
|
+
let gasMethod = "self-funded";
|
|
4369
|
+
for (const [asset, pct] of Object.entries(definition.allocations)) {
|
|
4370
|
+
const assetUsd = params.usdAmount * (pct / 100);
|
|
4371
|
+
const result = await this.investBuy({ asset, usdAmount: assetUsd });
|
|
4372
|
+
this.portfolio.recordStrategyBuy(params.strategy, {
|
|
4373
|
+
id: `strat_${Date.now()}_${asset}`,
|
|
4374
|
+
type: "buy",
|
|
4375
|
+
asset,
|
|
4376
|
+
amount: result.amount,
|
|
4377
|
+
price: result.price,
|
|
4378
|
+
usdValue: assetUsd,
|
|
4379
|
+
fee: result.fee,
|
|
4380
|
+
tx: result.tx,
|
|
4381
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4382
|
+
});
|
|
4383
|
+
buys.push({ asset, usdAmount: assetUsd, amount: result.amount, price: result.price, tx: result.tx });
|
|
4384
|
+
totalGas += result.gasCost;
|
|
4385
|
+
gasMethod = result.gasMethod;
|
|
4386
|
+
}
|
|
4387
|
+
return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: totalGas, gasMethod };
|
|
4388
|
+
}
|
|
4389
|
+
async sellStrategy(params) {
|
|
4390
|
+
this.enforcer.assertNotLocked();
|
|
4391
|
+
this.strategies.get(params.strategy);
|
|
4392
|
+
const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
4393
|
+
if (stratPositions.length === 0) {
|
|
4394
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
|
|
4395
|
+
}
|
|
4396
|
+
const sells = [];
|
|
4397
|
+
let totalProceeds = 0;
|
|
4398
|
+
let totalPnL = 0;
|
|
4399
|
+
let totalGas = 0;
|
|
4400
|
+
let gasMethod = "self-funded";
|
|
4401
|
+
for (const pos of stratPositions) {
|
|
4402
|
+
const result = await this.investSell({ asset: pos.asset, usdAmount: "all" });
|
|
4403
|
+
const pnl = this.portfolio.recordStrategySell(params.strategy, {
|
|
4404
|
+
id: `strat_sell_${Date.now()}_${pos.asset}`,
|
|
4405
|
+
type: "sell",
|
|
4406
|
+
asset: pos.asset,
|
|
4407
|
+
amount: result.amount,
|
|
4408
|
+
price: result.price,
|
|
4409
|
+
usdValue: result.usdValue,
|
|
4410
|
+
fee: result.fee,
|
|
4411
|
+
tx: result.tx,
|
|
4412
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4413
|
+
});
|
|
4414
|
+
sells.push({ asset: pos.asset, amount: result.amount, usdValue: result.usdValue, realizedPnL: pnl, tx: result.tx });
|
|
4415
|
+
totalProceeds += result.usdValue;
|
|
4416
|
+
totalPnL += pnl;
|
|
4417
|
+
totalGas += result.gasCost;
|
|
4418
|
+
gasMethod = result.gasMethod;
|
|
4419
|
+
}
|
|
4420
|
+
return { success: true, strategy: params.strategy, totalProceeds, realizedPnL: totalPnL, sells, gasCost: totalGas, gasMethod };
|
|
4421
|
+
}
|
|
4422
|
+
async rebalanceStrategy(params) {
|
|
4423
|
+
this.enforcer.assertNotLocked();
|
|
4424
|
+
const definition = this.strategies.get(params.strategy);
|
|
4425
|
+
const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
4426
|
+
if (stratPositions.length === 0) {
|
|
4427
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
|
|
4428
|
+
}
|
|
4429
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
4430
|
+
const prices = {};
|
|
4431
|
+
for (const pos of stratPositions) {
|
|
4432
|
+
try {
|
|
4433
|
+
if (pos.asset === "SUI" && swapAdapter) {
|
|
4434
|
+
prices[pos.asset] = await swapAdapter.getPoolPrice();
|
|
4435
|
+
} else if (swapAdapter) {
|
|
4436
|
+
const q = await swapAdapter.getQuote("USDC", pos.asset, 1);
|
|
4437
|
+
prices[pos.asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
|
|
4438
|
+
}
|
|
4439
|
+
} catch {
|
|
4440
|
+
prices[pos.asset] = 0;
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
const totalValue = stratPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
|
|
4444
|
+
if (totalValue <= 0) {
|
|
4445
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", "Strategy has no value to rebalance");
|
|
4446
|
+
}
|
|
4447
|
+
const currentWeights = {};
|
|
4448
|
+
const beforeWeights = {};
|
|
4449
|
+
for (const pos of stratPositions) {
|
|
4450
|
+
const w = pos.totalAmount * (prices[pos.asset] ?? 0) / totalValue * 100;
|
|
4451
|
+
currentWeights[pos.asset] = w;
|
|
4452
|
+
beforeWeights[pos.asset] = w;
|
|
4453
|
+
}
|
|
4454
|
+
const trades = [];
|
|
4455
|
+
const threshold = 3;
|
|
4456
|
+
for (const [asset, targetPct] of Object.entries(definition.allocations)) {
|
|
4457
|
+
const currentPct = currentWeights[asset] ?? 0;
|
|
4458
|
+
const diff = targetPct - currentPct;
|
|
4459
|
+
if (Math.abs(diff) < threshold) continue;
|
|
4460
|
+
const usdDiff = totalValue * (Math.abs(diff) / 100);
|
|
4461
|
+
if (usdDiff < 1) continue;
|
|
4462
|
+
if (diff > 0) {
|
|
4463
|
+
const result = await this.investBuy({ asset, usdAmount: usdDiff });
|
|
4464
|
+
this.portfolio.recordStrategyBuy(params.strategy, {
|
|
4465
|
+
id: `strat_rebal_${Date.now()}_${asset}`,
|
|
4466
|
+
type: "buy",
|
|
4467
|
+
asset,
|
|
4468
|
+
amount: result.amount,
|
|
4469
|
+
price: result.price,
|
|
4470
|
+
usdValue: usdDiff,
|
|
4471
|
+
fee: result.fee,
|
|
4472
|
+
tx: result.tx,
|
|
4473
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4474
|
+
});
|
|
4475
|
+
trades.push({ action: "buy", asset, usdAmount: usdDiff, amount: result.amount, tx: result.tx });
|
|
4476
|
+
} else {
|
|
4477
|
+
const result = await this.investSell({ asset, usdAmount: usdDiff });
|
|
4478
|
+
this.portfolio.recordStrategySell(params.strategy, {
|
|
4479
|
+
id: `strat_rebal_${Date.now()}_${asset}`,
|
|
4480
|
+
type: "sell",
|
|
4481
|
+
asset,
|
|
4482
|
+
amount: result.amount,
|
|
4483
|
+
price: result.price,
|
|
4484
|
+
usdValue: result.usdValue,
|
|
4485
|
+
fee: result.fee,
|
|
4486
|
+
tx: result.tx,
|
|
4487
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4488
|
+
});
|
|
4489
|
+
trades.push({ action: "sell", asset, usdAmount: result.usdValue, amount: result.amount, tx: result.tx });
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
const afterWeights = {};
|
|
4493
|
+
const updatedPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
4494
|
+
const newTotal = updatedPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
|
|
4495
|
+
for (const p of updatedPositions) {
|
|
4496
|
+
afterWeights[p.asset] = newTotal > 0 ? p.totalAmount * (prices[p.asset] ?? 0) / newTotal * 100 : 0;
|
|
4497
|
+
}
|
|
4498
|
+
return { success: true, strategy: params.strategy, trades, beforeWeights, afterWeights, targetWeights: { ...definition.allocations } };
|
|
4499
|
+
}
|
|
4500
|
+
async getStrategyStatus(name) {
|
|
4501
|
+
const definition = this.strategies.get(name);
|
|
4502
|
+
const stratPositions = this.portfolio.getStrategyPositions(name);
|
|
4503
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
4504
|
+
const prices = {};
|
|
4505
|
+
for (const asset of Object.keys(definition.allocations)) {
|
|
4506
|
+
try {
|
|
4507
|
+
if (asset === "SUI" && swapAdapter) {
|
|
4508
|
+
prices[asset] = await swapAdapter.getPoolPrice();
|
|
4509
|
+
} else if (swapAdapter) {
|
|
4510
|
+
const q = await swapAdapter.getQuote("USDC", asset, 1);
|
|
4511
|
+
prices[asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
|
|
4512
|
+
}
|
|
4513
|
+
} catch {
|
|
4514
|
+
prices[asset] = 0;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
const positions = stratPositions.map((sp) => {
|
|
4518
|
+
const price = prices[sp.asset] ?? 0;
|
|
4519
|
+
const currentValue = sp.totalAmount * price;
|
|
4520
|
+
const pnl = currentValue - sp.costBasis;
|
|
4521
|
+
return {
|
|
4522
|
+
asset: sp.asset,
|
|
4523
|
+
totalAmount: sp.totalAmount,
|
|
4524
|
+
costBasis: sp.costBasis,
|
|
4525
|
+
avgPrice: sp.avgPrice,
|
|
4526
|
+
currentPrice: price,
|
|
4527
|
+
currentValue,
|
|
4528
|
+
unrealizedPnL: pnl,
|
|
4529
|
+
unrealizedPnLPct: sp.costBasis > 0 ? pnl / sp.costBasis * 100 : 0,
|
|
4530
|
+
trades: sp.trades
|
|
4531
|
+
};
|
|
4532
|
+
});
|
|
4533
|
+
const totalValue = positions.reduce((s, p) => s + p.currentValue, 0);
|
|
4534
|
+
const currentWeights = {};
|
|
4535
|
+
for (const p of positions) {
|
|
4536
|
+
currentWeights[p.asset] = totalValue > 0 ? p.currentValue / totalValue * 100 : 0;
|
|
4537
|
+
}
|
|
4538
|
+
return { definition, positions, currentWeights, totalValue };
|
|
4539
|
+
}
|
|
4540
|
+
// -- Auto-Invest --
|
|
4541
|
+
setupAutoInvest(params) {
|
|
4542
|
+
if (params.strategy) this.strategies.get(params.strategy);
|
|
4543
|
+
if (params.asset && !(params.asset in INVESTMENT_ASSETS)) {
|
|
4544
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not an investment asset`);
|
|
4545
|
+
}
|
|
4546
|
+
return this.autoInvest.setup(params);
|
|
4547
|
+
}
|
|
4548
|
+
getAutoInvestStatus() {
|
|
4549
|
+
return this.autoInvest.getStatus();
|
|
4550
|
+
}
|
|
4551
|
+
async runAutoInvest() {
|
|
4552
|
+
this.enforcer.assertNotLocked();
|
|
4553
|
+
const status = this.autoInvest.getStatus();
|
|
4554
|
+
const executed = [];
|
|
4555
|
+
const skipped = [];
|
|
4556
|
+
for (const schedule of status.pendingRuns) {
|
|
4557
|
+
try {
|
|
4558
|
+
const bal = await queryBalance(this.client, this._address);
|
|
4559
|
+
if (bal.available < schedule.amount) {
|
|
4560
|
+
skipped.push({ scheduleId: schedule.id, reason: `Insufficient balance ($${bal.available.toFixed(2)} < $${schedule.amount})` });
|
|
4561
|
+
continue;
|
|
4562
|
+
}
|
|
4563
|
+
if (schedule.strategy) {
|
|
4564
|
+
const result = await this.investStrategy({ strategy: schedule.strategy, usdAmount: schedule.amount });
|
|
4565
|
+
this.autoInvest.recordRun(schedule.id, schedule.amount);
|
|
4566
|
+
executed.push({ scheduleId: schedule.id, strategy: schedule.strategy, amount: schedule.amount, result });
|
|
4567
|
+
} else if (schedule.asset) {
|
|
4568
|
+
const result = await this.investBuy({ asset: schedule.asset, usdAmount: schedule.amount });
|
|
4569
|
+
this.autoInvest.recordRun(schedule.id, schedule.amount);
|
|
4570
|
+
executed.push({ scheduleId: schedule.id, asset: schedule.asset, amount: schedule.amount, result });
|
|
4571
|
+
}
|
|
4572
|
+
} catch (err) {
|
|
4573
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4574
|
+
skipped.push({ scheduleId: schedule.id, reason: msg });
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
return { executed, skipped };
|
|
4578
|
+
}
|
|
4579
|
+
stopAutoInvest(id) {
|
|
4580
|
+
this.autoInvest.stop(id);
|
|
4581
|
+
}
|
|
4000
4582
|
async getPortfolio() {
|
|
4001
4583
|
const positions = this.portfolio.getPositions();
|
|
4002
4584
|
const realizedPnL = this.portfolio.getRealizedPnL();
|
|
@@ -4014,12 +4596,11 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4014
4596
|
prices[asset] = 0;
|
|
4015
4597
|
}
|
|
4016
4598
|
}
|
|
4017
|
-
const
|
|
4018
|
-
for (const pos of positions) {
|
|
4599
|
+
const enrichPosition = async (pos, adjustWallet) => {
|
|
4019
4600
|
const currentPrice = prices[pos.asset] ?? 0;
|
|
4020
4601
|
let totalAmount = pos.totalAmount;
|
|
4021
4602
|
let costBasis = pos.costBasis;
|
|
4022
|
-
if (pos.asset in INVESTMENT_ASSETS) {
|
|
4603
|
+
if (adjustWallet && pos.asset in INVESTMENT_ASSETS && !pos.earning) {
|
|
4023
4604
|
try {
|
|
4024
4605
|
const assetInfo = SUPPORTED_ASSETS[pos.asset];
|
|
4025
4606
|
const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
@@ -4037,7 +4618,7 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4037
4618
|
const currentValue = totalAmount * currentPrice;
|
|
4038
4619
|
const unrealizedPnL = currentPrice > 0 ? currentValue - costBasis : 0;
|
|
4039
4620
|
const unrealizedPnLPct = currentPrice > 0 && costBasis > 0 ? unrealizedPnL / costBasis * 100 : 0;
|
|
4040
|
-
|
|
4621
|
+
return {
|
|
4041
4622
|
asset: pos.asset,
|
|
4042
4623
|
totalAmount,
|
|
4043
4624
|
costBasis,
|
|
@@ -4050,13 +4631,29 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4050
4631
|
earning: pos.earning,
|
|
4051
4632
|
earningProtocol: pos.earningProtocol,
|
|
4052
4633
|
earningApy: pos.earningApy
|
|
4053
|
-
}
|
|
4634
|
+
};
|
|
4635
|
+
};
|
|
4636
|
+
const enriched = [];
|
|
4637
|
+
for (const pos of positions) {
|
|
4638
|
+
enriched.push(await enrichPosition(pos, true));
|
|
4639
|
+
}
|
|
4640
|
+
const strategyPositions = {};
|
|
4641
|
+
for (const key of this.portfolio.getAllStrategyKeys()) {
|
|
4642
|
+
const sps = this.portfolio.getStrategyPositions(key);
|
|
4643
|
+
const enrichedStrat = [];
|
|
4644
|
+
for (const sp of sps) {
|
|
4645
|
+
enrichedStrat.push(await enrichPosition(sp, false));
|
|
4646
|
+
}
|
|
4647
|
+
if (enrichedStrat.length > 0) {
|
|
4648
|
+
strategyPositions[key] = enrichedStrat;
|
|
4649
|
+
}
|
|
4054
4650
|
}
|
|
4055
|
-
const
|
|
4056
|
-
const
|
|
4651
|
+
const allPositions = [...enriched, ...Object.values(strategyPositions).flat()];
|
|
4652
|
+
const totalInvested = allPositions.reduce((sum, p) => sum + p.costBasis, 0);
|
|
4653
|
+
const totalValue = allPositions.reduce((sum, p) => sum + p.currentValue, 0);
|
|
4057
4654
|
const totalUnrealizedPnL = totalValue - totalInvested;
|
|
4058
4655
|
const totalUnrealizedPnLPct = totalInvested > 0 ? totalUnrealizedPnL / totalInvested * 100 : 0;
|
|
4059
|
-
|
|
4656
|
+
const result = {
|
|
4060
4657
|
positions: enriched,
|
|
4061
4658
|
totalInvested,
|
|
4062
4659
|
totalValue,
|
|
@@ -4064,6 +4661,10 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
4064
4661
|
unrealizedPnLPct: totalUnrealizedPnLPct,
|
|
4065
4662
|
realizedPnL
|
|
4066
4663
|
};
|
|
4664
|
+
if (Object.keys(strategyPositions).length > 0) {
|
|
4665
|
+
result.strategyPositions = strategyPositions;
|
|
4666
|
+
}
|
|
4667
|
+
return result;
|
|
4067
4668
|
}
|
|
4068
4669
|
// -- Info --
|
|
4069
4670
|
async positions() {
|
|
@@ -4554,6 +5155,7 @@ var allDescriptors = [
|
|
|
4554
5155
|
descriptor
|
|
4555
5156
|
];
|
|
4556
5157
|
|
|
5158
|
+
exports.AutoInvestManager = AutoInvestManager;
|
|
4557
5159
|
exports.BPS_DENOMINATOR = BPS_DENOMINATOR;
|
|
4558
5160
|
exports.CLOCK_ID = CLOCK_ID;
|
|
4559
5161
|
exports.CetusAdapter = CetusAdapter;
|
|
@@ -4562,6 +5164,7 @@ exports.DEFAULT_MAX_LEVERAGE = DEFAULT_MAX_LEVERAGE;
|
|
|
4562
5164
|
exports.DEFAULT_MAX_POSITION_SIZE = DEFAULT_MAX_POSITION_SIZE;
|
|
4563
5165
|
exports.DEFAULT_NETWORK = DEFAULT_NETWORK;
|
|
4564
5166
|
exports.DEFAULT_SAFEGUARD_CONFIG = DEFAULT_SAFEGUARD_CONFIG;
|
|
5167
|
+
exports.DEFAULT_STRATEGIES = DEFAULT_STRATEGIES;
|
|
4565
5168
|
exports.GAS_RESERVE_MIN = GAS_RESERVE_MIN;
|
|
4566
5169
|
exports.INVESTMENT_ASSETS = INVESTMENT_ASSETS;
|
|
4567
5170
|
exports.MIST_PER_SUI = MIST_PER_SUI;
|
|
@@ -4575,6 +5178,7 @@ exports.SUI_DECIMALS = SUI_DECIMALS;
|
|
|
4575
5178
|
exports.SUPPORTED_ASSETS = SUPPORTED_ASSETS;
|
|
4576
5179
|
exports.SafeguardEnforcer = SafeguardEnforcer;
|
|
4577
5180
|
exports.SafeguardError = SafeguardError;
|
|
5181
|
+
exports.StrategyManager = StrategyManager;
|
|
4578
5182
|
exports.SuilendAdapter = SuilendAdapter;
|
|
4579
5183
|
exports.T2000 = T2000;
|
|
4580
5184
|
exports.T2000Error = T2000Error;
|