@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/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,298 @@ 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 smallestPct = Math.min(...Object.values(allocations));
3101
+ const minRequired = Math.ceil(100 / smallestPct);
3102
+ if (totalUsd < minRequired) {
3103
+ const smallestAsset = Object.entries(allocations).find(([, p]) => p === smallestPct)?.[0] ?? "?";
3104
+ throw new T2000Error(
3105
+ "STRATEGY_MIN_AMOUNT",
3106
+ `Minimum $${minRequired} for this strategy (${smallestAsset} at ${smallestPct}% needs at least $1)`
3107
+ );
3108
+ }
3109
+ }
3110
+ };
3111
+ function emptyData3() {
3112
+ return { schedules: [] };
3113
+ }
3114
+ function computeNextRun(frequency, dayOfWeek, dayOfMonth, from) {
3115
+ const base = /* @__PURE__ */ new Date();
3116
+ const next = new Date(base);
3117
+ switch (frequency) {
3118
+ case "daily":
3119
+ next.setDate(next.getDate() + 1);
3120
+ next.setHours(0, 0, 0, 0);
3121
+ break;
3122
+ case "weekly": {
3123
+ const dow = dayOfWeek ?? 1;
3124
+ next.setDate(next.getDate() + ((7 - next.getDay() + dow) % 7 || 7));
3125
+ next.setHours(0, 0, 0, 0);
3126
+ break;
3127
+ }
3128
+ case "monthly": {
3129
+ const dom = dayOfMonth ?? 1;
3130
+ next.setMonth(next.getMonth() + 1, dom);
3131
+ next.setHours(0, 0, 0, 0);
3132
+ break;
3133
+ }
3134
+ }
3135
+ return next.toISOString();
3136
+ }
3137
+ var AutoInvestManager = class {
3138
+ data = emptyData3();
3139
+ filePath;
3140
+ dir;
3141
+ constructor(configDir) {
3142
+ this.dir = configDir ?? path.join(os.homedir(), ".t2000");
3143
+ this.filePath = path.join(this.dir, "auto-invest.json");
3144
+ this.load();
3145
+ }
3146
+ load() {
3147
+ try {
3148
+ if (fs.existsSync(this.filePath)) {
3149
+ this.data = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
3150
+ }
3151
+ } catch {
3152
+ this.data = emptyData3();
3153
+ }
3154
+ if (!this.data.schedules) {
3155
+ this.data.schedules = [];
3156
+ }
3157
+ }
3158
+ save() {
3159
+ if (!fs.existsSync(this.dir)) fs.mkdirSync(this.dir, { recursive: true });
3160
+ fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
3161
+ }
3162
+ setup(params) {
3163
+ this.load();
3164
+ if (!params.strategy && !params.asset) {
3165
+ throw new T2000Error("AUTO_INVEST_NOT_FOUND", "Either strategy or asset must be specified");
3166
+ }
3167
+ if (params.amount < 1) {
3168
+ throw new T2000Error("AUTO_INVEST_INSUFFICIENT", "Auto-invest amount must be at least $1");
3169
+ }
3170
+ const schedule = {
3171
+ id: crypto.randomUUID().slice(0, 8),
3172
+ strategy: params.strategy,
3173
+ asset: params.asset,
3174
+ amount: params.amount,
3175
+ frequency: params.frequency,
3176
+ dayOfWeek: params.dayOfWeek,
3177
+ dayOfMonth: params.dayOfMonth,
3178
+ nextRun: computeNextRun(params.frequency, params.dayOfWeek, params.dayOfMonth),
3179
+ enabled: true,
3180
+ totalInvested: 0,
3181
+ runCount: 0
3182
+ };
3183
+ this.data.schedules.push(schedule);
3184
+ this.save();
3185
+ return schedule;
3186
+ }
3187
+ getStatus() {
3188
+ this.load();
3189
+ const now = /* @__PURE__ */ new Date();
3190
+ const pending = this.data.schedules.filter(
3191
+ (s) => s.enabled && new Date(s.nextRun) <= now
3192
+ );
3193
+ return {
3194
+ schedules: [...this.data.schedules],
3195
+ pendingRuns: pending
3196
+ };
3197
+ }
3198
+ getSchedule(id) {
3199
+ this.load();
3200
+ const schedule = this.data.schedules.find((s) => s.id === id);
3201
+ if (!schedule) {
3202
+ throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
3203
+ }
3204
+ return schedule;
3205
+ }
3206
+ recordRun(id, amountInvested) {
3207
+ this.load();
3208
+ const schedule = this.data.schedules.find((s) => s.id === id);
3209
+ if (!schedule) return;
3210
+ schedule.lastRun = (/* @__PURE__ */ new Date()).toISOString();
3211
+ schedule.nextRun = computeNextRun(schedule.frequency, schedule.dayOfWeek, schedule.dayOfMonth);
3212
+ schedule.totalInvested += amountInvested;
3213
+ schedule.runCount += 1;
3214
+ this.save();
3215
+ }
3216
+ stop(id) {
3217
+ this.load();
3218
+ const schedule = this.data.schedules.find((s) => s.id === id);
3219
+ if (!schedule) {
3220
+ throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
3221
+ }
3222
+ schedule.enabled = false;
3223
+ this.save();
3224
+ }
3225
+ remove(id) {
3226
+ this.load();
3227
+ const idx = this.data.schedules.findIndex((s) => s.id === id);
3228
+ if (idx === -1) {
3229
+ throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
3230
+ }
3231
+ this.data.schedules.splice(idx, 1);
3232
+ this.save();
3233
+ }
2921
3234
  };
2922
3235
  var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".t2000");
2923
3236
  var T2000 = class _T2000 extends eventemitter3.EventEmitter {
@@ -2928,6 +3241,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2928
3241
  enforcer;
2929
3242
  contacts;
2930
3243
  portfolio;
3244
+ strategies;
3245
+ autoInvest;
2931
3246
  constructor(keypair, client, registry, configDir) {
2932
3247
  super();
2933
3248
  this.keypair = keypair;
@@ -2938,6 +3253,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2938
3253
  this.enforcer.load();
2939
3254
  this.contacts = new ContactManager(configDir);
2940
3255
  this.portfolio = new PortfolioManager(configDir);
3256
+ this.strategies = new StrategyManager(configDir);
3257
+ this.autoInvest = new AutoInvestManager(configDir);
2941
3258
  }
2942
3259
  static createDefaultRegistry(client) {
2943
3260
  const registry = new ProtocolRegistry();
@@ -3111,6 +3428,14 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
3111
3428
  investmentCostBasis += pos.costBasis;
3112
3429
  }
3113
3430
  }
3431
+ for (const key of this.portfolio.getAllStrategyKeys()) {
3432
+ for (const sp of this.portfolio.getStrategyPositions(key)) {
3433
+ if (!(sp.asset in INVESTMENT_ASSETS)) continue;
3434
+ const price = assetPrices[sp.asset] ?? 0;
3435
+ investmentValue += sp.totalAmount * price;
3436
+ investmentCostBasis += sp.costBasis;
3437
+ }
3438
+ }
3114
3439
  bal.investment = investmentValue;
3115
3440
  bal.investmentPnL = investmentValue - investmentCostBasis;
3116
3441
  } catch {
@@ -4007,6 +4332,252 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4007
4332
  gasMethod: gasResult.gasMethod
4008
4333
  };
4009
4334
  }
4335
+ // -- Strategies --
4336
+ async investStrategy(params) {
4337
+ this.enforcer.assertNotLocked();
4338
+ const definition = this.strategies.get(params.strategy);
4339
+ this.strategies.validateMinAmount(definition.allocations, params.usdAmount);
4340
+ if (!params.usdAmount || params.usdAmount <= 0) {
4341
+ throw new T2000Error("INVALID_AMOUNT", "Strategy investment must be > $0");
4342
+ }
4343
+ const bal = await queryBalance(this.client, this._address);
4344
+ if (bal.available < params.usdAmount) {
4345
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient balance. Available: $${bal.available.toFixed(2)}, requested: $${params.usdAmount.toFixed(2)}`);
4346
+ }
4347
+ const buys = [];
4348
+ if (params.dryRun) {
4349
+ const swapAdapter = this.registry.listSwap()[0];
4350
+ for (const [asset, pct] of Object.entries(definition.allocations)) {
4351
+ const assetUsd = params.usdAmount * (pct / 100);
4352
+ let estAmount = 0;
4353
+ let estPrice = 0;
4354
+ try {
4355
+ if (swapAdapter) {
4356
+ const quote = await swapAdapter.getQuote("USDC", asset, assetUsd);
4357
+ estAmount = quote.expectedOutput;
4358
+ estPrice = assetUsd / estAmount;
4359
+ }
4360
+ } catch {
4361
+ }
4362
+ buys.push({ asset, usdAmount: assetUsd, amount: estAmount, price: estPrice, tx: "" });
4363
+ }
4364
+ return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: 0, gasMethod: "self-funded" };
4365
+ }
4366
+ let totalGas = 0;
4367
+ let gasMethod = "self-funded";
4368
+ for (const [asset, pct] of Object.entries(definition.allocations)) {
4369
+ const assetUsd = params.usdAmount * (pct / 100);
4370
+ const result = await this.investBuy({ asset, usdAmount: assetUsd });
4371
+ this.portfolio.recordStrategyBuy(params.strategy, {
4372
+ id: `strat_${Date.now()}_${asset}`,
4373
+ type: "buy",
4374
+ asset,
4375
+ amount: result.amount,
4376
+ price: result.price,
4377
+ usdValue: assetUsd,
4378
+ fee: result.fee,
4379
+ tx: result.tx,
4380
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4381
+ });
4382
+ buys.push({ asset, usdAmount: assetUsd, amount: result.amount, price: result.price, tx: result.tx });
4383
+ totalGas += result.gasCost;
4384
+ gasMethod = result.gasMethod;
4385
+ }
4386
+ return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: totalGas, gasMethod };
4387
+ }
4388
+ async sellStrategy(params) {
4389
+ this.enforcer.assertNotLocked();
4390
+ this.strategies.get(params.strategy);
4391
+ const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
4392
+ if (stratPositions.length === 0) {
4393
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
4394
+ }
4395
+ const sells = [];
4396
+ let totalProceeds = 0;
4397
+ let totalPnL = 0;
4398
+ let totalGas = 0;
4399
+ let gasMethod = "self-funded";
4400
+ for (const pos of stratPositions) {
4401
+ const result = await this.investSell({ asset: pos.asset, usdAmount: "all" });
4402
+ const pnl = this.portfolio.recordStrategySell(params.strategy, {
4403
+ id: `strat_sell_${Date.now()}_${pos.asset}`,
4404
+ type: "sell",
4405
+ asset: pos.asset,
4406
+ amount: result.amount,
4407
+ price: result.price,
4408
+ usdValue: result.usdValue,
4409
+ fee: result.fee,
4410
+ tx: result.tx,
4411
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4412
+ });
4413
+ sells.push({ asset: pos.asset, amount: result.amount, usdValue: result.usdValue, realizedPnL: pnl, tx: result.tx });
4414
+ totalProceeds += result.usdValue;
4415
+ totalPnL += pnl;
4416
+ totalGas += result.gasCost;
4417
+ gasMethod = result.gasMethod;
4418
+ }
4419
+ return { success: true, strategy: params.strategy, totalProceeds, realizedPnL: totalPnL, sells, gasCost: totalGas, gasMethod };
4420
+ }
4421
+ async rebalanceStrategy(params) {
4422
+ this.enforcer.assertNotLocked();
4423
+ const definition = this.strategies.get(params.strategy);
4424
+ const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
4425
+ if (stratPositions.length === 0) {
4426
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
4427
+ }
4428
+ const swapAdapter = this.registry.listSwap()[0];
4429
+ const prices = {};
4430
+ for (const pos of stratPositions) {
4431
+ try {
4432
+ if (pos.asset === "SUI" && swapAdapter) {
4433
+ prices[pos.asset] = await swapAdapter.getPoolPrice();
4434
+ } else if (swapAdapter) {
4435
+ const q = await swapAdapter.getQuote("USDC", pos.asset, 1);
4436
+ prices[pos.asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
4437
+ }
4438
+ } catch {
4439
+ prices[pos.asset] = 0;
4440
+ }
4441
+ }
4442
+ const totalValue = stratPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
4443
+ if (totalValue <= 0) {
4444
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", "Strategy has no value to rebalance");
4445
+ }
4446
+ const currentWeights = {};
4447
+ const beforeWeights = {};
4448
+ for (const pos of stratPositions) {
4449
+ const w = pos.totalAmount * (prices[pos.asset] ?? 0) / totalValue * 100;
4450
+ currentWeights[pos.asset] = w;
4451
+ beforeWeights[pos.asset] = w;
4452
+ }
4453
+ const trades = [];
4454
+ const threshold = 3;
4455
+ for (const [asset, targetPct] of Object.entries(definition.allocations)) {
4456
+ const currentPct = currentWeights[asset] ?? 0;
4457
+ const diff = targetPct - currentPct;
4458
+ if (Math.abs(diff) < threshold) continue;
4459
+ const usdDiff = totalValue * (Math.abs(diff) / 100);
4460
+ if (usdDiff < 1) continue;
4461
+ if (diff > 0) {
4462
+ const result = await this.investBuy({ asset, usdAmount: usdDiff });
4463
+ this.portfolio.recordStrategyBuy(params.strategy, {
4464
+ id: `strat_rebal_${Date.now()}_${asset}`,
4465
+ type: "buy",
4466
+ asset,
4467
+ amount: result.amount,
4468
+ price: result.price,
4469
+ usdValue: usdDiff,
4470
+ fee: result.fee,
4471
+ tx: result.tx,
4472
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4473
+ });
4474
+ trades.push({ action: "buy", asset, usdAmount: usdDiff, amount: result.amount, tx: result.tx });
4475
+ } else {
4476
+ const result = await this.investSell({ asset, usdAmount: usdDiff });
4477
+ this.portfolio.recordStrategySell(params.strategy, {
4478
+ id: `strat_rebal_${Date.now()}_${asset}`,
4479
+ type: "sell",
4480
+ asset,
4481
+ amount: result.amount,
4482
+ price: result.price,
4483
+ usdValue: result.usdValue,
4484
+ fee: result.fee,
4485
+ tx: result.tx,
4486
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4487
+ });
4488
+ trades.push({ action: "sell", asset, usdAmount: result.usdValue, amount: result.amount, tx: result.tx });
4489
+ }
4490
+ }
4491
+ const afterWeights = {};
4492
+ const updatedPositions = this.portfolio.getStrategyPositions(params.strategy);
4493
+ const newTotal = updatedPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
4494
+ for (const p of updatedPositions) {
4495
+ afterWeights[p.asset] = newTotal > 0 ? p.totalAmount * (prices[p.asset] ?? 0) / newTotal * 100 : 0;
4496
+ }
4497
+ return { success: true, strategy: params.strategy, trades, beforeWeights, afterWeights, targetWeights: { ...definition.allocations } };
4498
+ }
4499
+ async getStrategyStatus(name) {
4500
+ const definition = this.strategies.get(name);
4501
+ const stratPositions = this.portfolio.getStrategyPositions(name);
4502
+ const swapAdapter = this.registry.listSwap()[0];
4503
+ const prices = {};
4504
+ for (const asset of Object.keys(definition.allocations)) {
4505
+ try {
4506
+ if (asset === "SUI" && swapAdapter) {
4507
+ prices[asset] = await swapAdapter.getPoolPrice();
4508
+ } else if (swapAdapter) {
4509
+ const q = await swapAdapter.getQuote("USDC", asset, 1);
4510
+ prices[asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
4511
+ }
4512
+ } catch {
4513
+ prices[asset] = 0;
4514
+ }
4515
+ }
4516
+ const positions = stratPositions.map((sp) => {
4517
+ const price = prices[sp.asset] ?? 0;
4518
+ const currentValue = sp.totalAmount * price;
4519
+ const pnl = currentValue - sp.costBasis;
4520
+ return {
4521
+ asset: sp.asset,
4522
+ totalAmount: sp.totalAmount,
4523
+ costBasis: sp.costBasis,
4524
+ avgPrice: sp.avgPrice,
4525
+ currentPrice: price,
4526
+ currentValue,
4527
+ unrealizedPnL: pnl,
4528
+ unrealizedPnLPct: sp.costBasis > 0 ? pnl / sp.costBasis * 100 : 0,
4529
+ trades: sp.trades
4530
+ };
4531
+ });
4532
+ const totalValue = positions.reduce((s, p) => s + p.currentValue, 0);
4533
+ const currentWeights = {};
4534
+ for (const p of positions) {
4535
+ currentWeights[p.asset] = totalValue > 0 ? p.currentValue / totalValue * 100 : 0;
4536
+ }
4537
+ return { definition, positions, currentWeights, totalValue };
4538
+ }
4539
+ // -- Auto-Invest --
4540
+ setupAutoInvest(params) {
4541
+ if (params.strategy) this.strategies.get(params.strategy);
4542
+ if (params.asset && !(params.asset in INVESTMENT_ASSETS)) {
4543
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not an investment asset`);
4544
+ }
4545
+ return this.autoInvest.setup(params);
4546
+ }
4547
+ getAutoInvestStatus() {
4548
+ return this.autoInvest.getStatus();
4549
+ }
4550
+ async runAutoInvest() {
4551
+ this.enforcer.assertNotLocked();
4552
+ const status = this.autoInvest.getStatus();
4553
+ const executed = [];
4554
+ const skipped = [];
4555
+ for (const schedule of status.pendingRuns) {
4556
+ try {
4557
+ const bal = await queryBalance(this.client, this._address);
4558
+ if (bal.available < schedule.amount) {
4559
+ skipped.push({ scheduleId: schedule.id, reason: `Insufficient balance ($${bal.available.toFixed(2)} < $${schedule.amount})` });
4560
+ continue;
4561
+ }
4562
+ if (schedule.strategy) {
4563
+ const result = await this.investStrategy({ strategy: schedule.strategy, usdAmount: schedule.amount });
4564
+ this.autoInvest.recordRun(schedule.id, schedule.amount);
4565
+ executed.push({ scheduleId: schedule.id, strategy: schedule.strategy, amount: schedule.amount, result });
4566
+ } else if (schedule.asset) {
4567
+ const result = await this.investBuy({ asset: schedule.asset, usdAmount: schedule.amount });
4568
+ this.autoInvest.recordRun(schedule.id, schedule.amount);
4569
+ executed.push({ scheduleId: schedule.id, asset: schedule.asset, amount: schedule.amount, result });
4570
+ }
4571
+ } catch (err) {
4572
+ const msg = err instanceof Error ? err.message : String(err);
4573
+ skipped.push({ scheduleId: schedule.id, reason: msg });
4574
+ }
4575
+ }
4576
+ return { executed, skipped };
4577
+ }
4578
+ stopAutoInvest(id) {
4579
+ this.autoInvest.stop(id);
4580
+ }
4010
4581
  async getPortfolio() {
4011
4582
  const positions = this.portfolio.getPositions();
4012
4583
  const realizedPnL = this.portfolio.getRealizedPnL();
@@ -4024,12 +4595,11 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4024
4595
  prices[asset] = 0;
4025
4596
  }
4026
4597
  }
4027
- const enriched = [];
4028
- for (const pos of positions) {
4598
+ const enrichPosition = async (pos, adjustWallet) => {
4029
4599
  const currentPrice = prices[pos.asset] ?? 0;
4030
4600
  let totalAmount = pos.totalAmount;
4031
4601
  let costBasis = pos.costBasis;
4032
- if (pos.asset in INVESTMENT_ASSETS && !pos.earning) {
4602
+ if (adjustWallet && pos.asset in INVESTMENT_ASSETS && !pos.earning) {
4033
4603
  try {
4034
4604
  const assetInfo = SUPPORTED_ASSETS[pos.asset];
4035
4605
  const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
@@ -4047,7 +4617,7 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4047
4617
  const currentValue = totalAmount * currentPrice;
4048
4618
  const unrealizedPnL = currentPrice > 0 ? currentValue - costBasis : 0;
4049
4619
  const unrealizedPnLPct = currentPrice > 0 && costBasis > 0 ? unrealizedPnL / costBasis * 100 : 0;
4050
- enriched.push({
4620
+ return {
4051
4621
  asset: pos.asset,
4052
4622
  totalAmount,
4053
4623
  costBasis,
@@ -4060,13 +4630,29 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4060
4630
  earning: pos.earning,
4061
4631
  earningProtocol: pos.earningProtocol,
4062
4632
  earningApy: pos.earningApy
4063
- });
4633
+ };
4634
+ };
4635
+ const enriched = [];
4636
+ for (const pos of positions) {
4637
+ enriched.push(await enrichPosition(pos, true));
4638
+ }
4639
+ const strategyPositions = {};
4640
+ for (const key of this.portfolio.getAllStrategyKeys()) {
4641
+ const sps = this.portfolio.getStrategyPositions(key);
4642
+ const enrichedStrat = [];
4643
+ for (const sp of sps) {
4644
+ enrichedStrat.push(await enrichPosition(sp, false));
4645
+ }
4646
+ if (enrichedStrat.length > 0) {
4647
+ strategyPositions[key] = enrichedStrat;
4648
+ }
4064
4649
  }
4065
- const totalInvested = enriched.reduce((sum, p) => sum + p.costBasis, 0);
4066
- const totalValue = enriched.reduce((sum, p) => sum + p.currentValue, 0);
4650
+ const allPositions = [...enriched, ...Object.values(strategyPositions).flat()];
4651
+ const totalInvested = allPositions.reduce((sum, p) => sum + p.costBasis, 0);
4652
+ const totalValue = allPositions.reduce((sum, p) => sum + p.currentValue, 0);
4067
4653
  const totalUnrealizedPnL = totalValue - totalInvested;
4068
4654
  const totalUnrealizedPnLPct = totalInvested > 0 ? totalUnrealizedPnL / totalInvested * 100 : 0;
4069
- return {
4655
+ const result = {
4070
4656
  positions: enriched,
4071
4657
  totalInvested,
4072
4658
  totalValue,
@@ -4074,6 +4660,10 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4074
4660
  unrealizedPnLPct: totalUnrealizedPnLPct,
4075
4661
  realizedPnL
4076
4662
  };
4663
+ if (Object.keys(strategyPositions).length > 0) {
4664
+ result.strategyPositions = strategyPositions;
4665
+ }
4666
+ return result;
4077
4667
  }
4078
4668
  // -- Info --
4079
4669
  async positions() {
@@ -4564,6 +5154,7 @@ var allDescriptors = [
4564
5154
  descriptor
4565
5155
  ];
4566
5156
 
5157
+ exports.AutoInvestManager = AutoInvestManager;
4567
5158
  exports.BPS_DENOMINATOR = BPS_DENOMINATOR;
4568
5159
  exports.CLOCK_ID = CLOCK_ID;
4569
5160
  exports.CetusAdapter = CetusAdapter;
@@ -4572,6 +5163,7 @@ exports.DEFAULT_MAX_LEVERAGE = DEFAULT_MAX_LEVERAGE;
4572
5163
  exports.DEFAULT_MAX_POSITION_SIZE = DEFAULT_MAX_POSITION_SIZE;
4573
5164
  exports.DEFAULT_NETWORK = DEFAULT_NETWORK;
4574
5165
  exports.DEFAULT_SAFEGUARD_CONFIG = DEFAULT_SAFEGUARD_CONFIG;
5166
+ exports.DEFAULT_STRATEGIES = DEFAULT_STRATEGIES;
4575
5167
  exports.GAS_RESERVE_MIN = GAS_RESERVE_MIN;
4576
5168
  exports.INVESTMENT_ASSETS = INVESTMENT_ASSETS;
4577
5169
  exports.MIST_PER_SUI = MIST_PER_SUI;
@@ -4585,6 +5177,7 @@ exports.SUI_DECIMALS = SUI_DECIMALS;
4585
5177
  exports.SUPPORTED_ASSETS = SUPPORTED_ASSETS;
4586
5178
  exports.SafeguardEnforcer = SafeguardEnforcer;
4587
5179
  exports.SafeguardError = SafeguardError;
5180
+ exports.StrategyManager = StrategyManager;
4588
5181
  exports.SuilendAdapter = SuilendAdapter;
4589
5182
  exports.T2000 = T2000;
4590
5183
  exports.T2000Error = T2000Error;