@vizzor/cli 0.10.0 → 0.11.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/README.md +204 -6
- package/dist/index.js +2125 -188
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -62,18 +62,31 @@ var init_schema = __esm({
|
|
|
62
62
|
api: z.object({
|
|
63
63
|
port: z.number().default(3e3),
|
|
64
64
|
host: z.string().default("0.0.0.0"),
|
|
65
|
-
enableAuth: z.boolean().default(
|
|
66
|
-
corsOrigin: z.string().default("
|
|
65
|
+
enableAuth: z.boolean().default(true),
|
|
66
|
+
corsOrigin: z.string().default("http://localhost:3000")
|
|
67
67
|
}).default(() => ({
|
|
68
68
|
port: 3e3,
|
|
69
69
|
host: "0.0.0.0",
|
|
70
|
-
enableAuth:
|
|
71
|
-
corsOrigin: "
|
|
70
|
+
enableAuth: true,
|
|
71
|
+
corsOrigin: "http://localhost:3000"
|
|
72
72
|
})),
|
|
73
73
|
n8n: z.object({
|
|
74
74
|
enabled: z.boolean().default(false),
|
|
75
75
|
webhookUrl: z.string().optional()
|
|
76
76
|
}).default(() => ({ enabled: false })),
|
|
77
|
+
realtime: z.object({
|
|
78
|
+
enabled: z.boolean().default(false),
|
|
79
|
+
provider: z.enum(["binance"]).default("binance"),
|
|
80
|
+
symbols: z.array(z.string()).default([])
|
|
81
|
+
}).optional(),
|
|
82
|
+
trading: z.object({
|
|
83
|
+
enabled: z.boolean().default(false),
|
|
84
|
+
maxSlippage: z.number().default(0.5),
|
|
85
|
+
gasMultiplier: z.number().default(1.2),
|
|
86
|
+
confirmBeforeExecute: z.boolean().default(true),
|
|
87
|
+
dryRun: z.boolean().default(true),
|
|
88
|
+
walletName: z.string().optional()
|
|
89
|
+
}).optional(),
|
|
77
90
|
discordToken: z.string().optional(),
|
|
78
91
|
discordGuildId: z.string().optional(),
|
|
79
92
|
telegramToken: z.string().optional()
|
|
@@ -82,7 +95,15 @@ var init_schema = __esm({
|
|
|
82
95
|
});
|
|
83
96
|
|
|
84
97
|
// src/config/loader.ts
|
|
85
|
-
|
|
98
|
+
var loader_exports = {};
|
|
99
|
+
__export(loader_exports, {
|
|
100
|
+
getConfig: () => getConfig,
|
|
101
|
+
getConfigDir: () => getConfigDir,
|
|
102
|
+
getSettableKeys: () => getSettableKeys,
|
|
103
|
+
loadConfig: () => loadConfig,
|
|
104
|
+
saveConfigValue: () => saveConfigValue
|
|
105
|
+
});
|
|
106
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
86
107
|
import { join } from "path";
|
|
87
108
|
import { homedir } from "os";
|
|
88
109
|
import yaml from "yaml";
|
|
@@ -209,7 +230,8 @@ function saveConfigValue(key, value) {
|
|
|
209
230
|
raw[key] = value;
|
|
210
231
|
}
|
|
211
232
|
vizzorConfigSchema.parse(raw);
|
|
212
|
-
writeFileSync(configPath, yaml.stringify(raw), "utf-8");
|
|
233
|
+
writeFileSync(configPath, yaml.stringify(raw), { encoding: "utf-8", mode: 384 });
|
|
234
|
+
chmodSync(configPath, 384);
|
|
213
235
|
cachedConfig = null;
|
|
214
236
|
loadConfig();
|
|
215
237
|
}
|
|
@@ -250,6 +272,20 @@ var init_loader = __esm({
|
|
|
250
272
|
}
|
|
251
273
|
});
|
|
252
274
|
|
|
275
|
+
// src/utils/validate.ts
|
|
276
|
+
function assertValidAddress(address) {
|
|
277
|
+
if (!EVM_ADDRESS_RE.test(address)) {
|
|
278
|
+
throw new Error(`Invalid address format: expected 0x followed by 40 hex characters`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
var EVM_ADDRESS_RE;
|
|
282
|
+
var init_validate = __esm({
|
|
283
|
+
"src/utils/validate.ts"() {
|
|
284
|
+
"use strict";
|
|
285
|
+
EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
253
289
|
// src/chains/evm/abi/erc20.ts
|
|
254
290
|
var erc20Abi;
|
|
255
291
|
var init_erc20 = __esm({
|
|
@@ -595,11 +631,12 @@ import {
|
|
|
595
631
|
createPublicClient,
|
|
596
632
|
http
|
|
597
633
|
} from "viem";
|
|
598
|
-
import { mainnet, polygon, arbitrum, optimism, base } from "viem/chains";
|
|
634
|
+
import { mainnet, polygon, arbitrum, optimism, base, bsc, avalanche } from "viem/chains";
|
|
599
635
|
var CHAIN_MAP, EvmAdapter;
|
|
600
636
|
var init_adapter = __esm({
|
|
601
637
|
"src/chains/evm/adapter.ts"() {
|
|
602
638
|
"use strict";
|
|
639
|
+
init_validate();
|
|
603
640
|
init_erc20();
|
|
604
641
|
init_etherscan();
|
|
605
642
|
init_constants();
|
|
@@ -608,7 +645,9 @@ var init_adapter = __esm({
|
|
|
608
645
|
polygon,
|
|
609
646
|
arbitrum,
|
|
610
647
|
optimism,
|
|
611
|
-
base
|
|
648
|
+
base,
|
|
649
|
+
bsc,
|
|
650
|
+
avalanche
|
|
612
651
|
};
|
|
613
652
|
EvmAdapter = class {
|
|
614
653
|
chainId;
|
|
@@ -651,18 +690,23 @@ var init_adapter = __esm({
|
|
|
651
690
|
isConnected() {
|
|
652
691
|
return this.client !== null;
|
|
653
692
|
}
|
|
693
|
+
/** Validate and cast to viem Address. */
|
|
694
|
+
toAddress(address) {
|
|
695
|
+
assertValidAddress(address);
|
|
696
|
+
return this.toAddress(address);
|
|
697
|
+
}
|
|
654
698
|
// ── Balances ────────────────────────────────────────────────────────────
|
|
655
699
|
async getBalance(address) {
|
|
656
700
|
const client2 = this.requireClient();
|
|
657
|
-
return client2.getBalance({ address });
|
|
701
|
+
return client2.getBalance({ address: this.toAddress(address) });
|
|
658
702
|
}
|
|
659
703
|
async getTokenBalance(address, tokenAddress) {
|
|
660
704
|
const client2 = this.requireClient();
|
|
661
705
|
const balance = await client2.readContract({
|
|
662
|
-
address: tokenAddress,
|
|
706
|
+
address: this.toAddress(tokenAddress),
|
|
663
707
|
abi: erc20Abi,
|
|
664
708
|
functionName: "balanceOf",
|
|
665
|
-
args: [address]
|
|
709
|
+
args: [this.toAddress(address)]
|
|
666
710
|
});
|
|
667
711
|
return balance;
|
|
668
712
|
}
|
|
@@ -687,13 +731,13 @@ var init_adapter = __esm({
|
|
|
687
731
|
// ── Contracts ───────────────────────────────────────────────────────────
|
|
688
732
|
async getContractCode(address) {
|
|
689
733
|
const client2 = this.requireClient();
|
|
690
|
-
const code = await client2.getCode({ address });
|
|
734
|
+
const code = await client2.getCode({ address: this.toAddress(address) });
|
|
691
735
|
return code ?? "0x";
|
|
692
736
|
}
|
|
693
737
|
async readContract(address, abi, functionName, args2) {
|
|
694
738
|
const client2 = this.requireClient();
|
|
695
739
|
return client2.readContract({
|
|
696
|
-
address,
|
|
740
|
+
address: this.toAddress(address),
|
|
697
741
|
abi,
|
|
698
742
|
functionName,
|
|
699
743
|
args: args2
|
|
@@ -702,25 +746,25 @@ var init_adapter = __esm({
|
|
|
702
746
|
async getContractEvents(address, abi, eventName, options) {
|
|
703
747
|
const client2 = this.requireClient();
|
|
704
748
|
const logs = await client2.getContractEvents({
|
|
705
|
-
address,
|
|
749
|
+
address: this.toAddress(address),
|
|
706
750
|
abi,
|
|
707
751
|
eventName,
|
|
708
752
|
fromBlock: options?.fromBlock,
|
|
709
753
|
toBlock: options?.toBlock,
|
|
710
754
|
args: options?.args
|
|
711
755
|
});
|
|
712
|
-
return logs.map((
|
|
713
|
-
eventName:
|
|
714
|
-
blockNumber:
|
|
715
|
-
transactionHash:
|
|
716
|
-
args:
|
|
717
|
-
logIndex:
|
|
756
|
+
return logs.map((log15) => ({
|
|
757
|
+
eventName: log15.eventName ?? eventName,
|
|
758
|
+
blockNumber: log15.blockNumber ?? 0n,
|
|
759
|
+
transactionHash: log15.transactionHash ?? "0x",
|
|
760
|
+
args: log15.args ?? {},
|
|
761
|
+
logIndex: log15.logIndex ?? 0
|
|
718
762
|
}));
|
|
719
763
|
}
|
|
720
764
|
// ── Tokens ──────────────────────────────────────────────────────────────
|
|
721
765
|
async getTokenInfo(address) {
|
|
722
766
|
const client2 = this.requireClient();
|
|
723
|
-
const tokenAddr = address;
|
|
767
|
+
const tokenAddr = this.toAddress(address);
|
|
724
768
|
const [name, symbol, decimals, totalSupply] = await Promise.all([
|
|
725
769
|
client2.readContract({
|
|
726
770
|
address: tokenAddr,
|
|
@@ -779,6 +823,46 @@ var init_adapter = __esm({
|
|
|
779
823
|
}
|
|
780
824
|
});
|
|
781
825
|
|
|
826
|
+
// src/utils/logger.ts
|
|
827
|
+
import pino from "pino";
|
|
828
|
+
function createLogger(name) {
|
|
829
|
+
const isDev = process.env["NODE_ENV"] !== "production";
|
|
830
|
+
if (isDev) {
|
|
831
|
+
return pino({
|
|
832
|
+
name,
|
|
833
|
+
level,
|
|
834
|
+
transport: {
|
|
835
|
+
target: "pino-pretty",
|
|
836
|
+
options: {
|
|
837
|
+
colorize: true
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return pino({ name, level });
|
|
843
|
+
}
|
|
844
|
+
var level;
|
|
845
|
+
var init_logger = __esm({
|
|
846
|
+
"src/utils/logger.ts"() {
|
|
847
|
+
"use strict";
|
|
848
|
+
level = process.env["VIZZOR_LOG_LEVEL"] ?? "info";
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// src/chains/evm/writable-adapter.ts
|
|
853
|
+
import { createWalletClient, http as http2 } from "viem";
|
|
854
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
855
|
+
import { mainnet as mainnet2, polygon as polygon2, arbitrum as arbitrum2, optimism as optimism2, base as base2, bsc as bsc2, avalanche as avalanche2 } from "viem/chains";
|
|
856
|
+
var log;
|
|
857
|
+
var init_writable_adapter = __esm({
|
|
858
|
+
"src/chains/evm/writable-adapter.ts"() {
|
|
859
|
+
"use strict";
|
|
860
|
+
init_adapter();
|
|
861
|
+
init_logger();
|
|
862
|
+
log = createLogger("writable-evm");
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
|
|
782
866
|
// src/chains/zk/adapter.ts
|
|
783
867
|
function getZkChainIds() {
|
|
784
868
|
return Object.keys(ZK_CHAIN_CONFIG);
|
|
@@ -836,6 +920,457 @@ var init_adapter2 = __esm({
|
|
|
836
920
|
}
|
|
837
921
|
});
|
|
838
922
|
|
|
923
|
+
// src/chains/solana/adapter.ts
|
|
924
|
+
var log2, SolanaAdapter;
|
|
925
|
+
var init_adapter3 = __esm({
|
|
926
|
+
"src/chains/solana/adapter.ts"() {
|
|
927
|
+
"use strict";
|
|
928
|
+
init_logger();
|
|
929
|
+
log2 = createLogger("solana-adapter");
|
|
930
|
+
SolanaAdapter = class {
|
|
931
|
+
chainId = "solana";
|
|
932
|
+
name = "Solana";
|
|
933
|
+
nativeCurrency = { symbol: "SOL", decimals: 9 };
|
|
934
|
+
rpcUrl = "https://api.mainnet-beta.solana.com";
|
|
935
|
+
connected = false;
|
|
936
|
+
async connect(rpcUrl) {
|
|
937
|
+
if (rpcUrl) this.rpcUrl = rpcUrl;
|
|
938
|
+
this.connected = true;
|
|
939
|
+
log2.info(`Connected to Solana: ${this.rpcUrl}`);
|
|
940
|
+
}
|
|
941
|
+
async disconnect() {
|
|
942
|
+
this.connected = false;
|
|
943
|
+
}
|
|
944
|
+
isConnected() {
|
|
945
|
+
return this.connected;
|
|
946
|
+
}
|
|
947
|
+
async rpc(method, params = []) {
|
|
948
|
+
const res = await fetch(this.rpcUrl, {
|
|
949
|
+
method: "POST",
|
|
950
|
+
headers: { "Content-Type": "application/json" },
|
|
951
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
952
|
+
signal: AbortSignal.timeout(15e3)
|
|
953
|
+
});
|
|
954
|
+
const json = await res.json();
|
|
955
|
+
if (json.error) throw new Error(`Solana RPC: ${json.error.message}`);
|
|
956
|
+
return json.result;
|
|
957
|
+
}
|
|
958
|
+
async getBalance(address) {
|
|
959
|
+
const result = await this.rpc("getBalance", [address]);
|
|
960
|
+
return BigInt(result.value);
|
|
961
|
+
}
|
|
962
|
+
async getTokenBalance(address, tokenAddress) {
|
|
963
|
+
const result = await this.rpc("getTokenAccountsByOwner", [
|
|
964
|
+
address,
|
|
965
|
+
{ mint: tokenAddress },
|
|
966
|
+
{ encoding: "jsonParsed" }
|
|
967
|
+
]);
|
|
968
|
+
if (result.value.length === 0) return 0n;
|
|
969
|
+
return BigInt(result.value[0].account.data.parsed.info.tokenAmount.amount);
|
|
970
|
+
}
|
|
971
|
+
async getTransactionHistory(address, options) {
|
|
972
|
+
const limit = options?.limit ?? 20;
|
|
973
|
+
const sigs = await this.rpc("getSignaturesForAddress", [address, { limit }]);
|
|
974
|
+
return sigs.map((s) => ({
|
|
975
|
+
hash: s.signature,
|
|
976
|
+
blockNumber: BigInt(s.slot),
|
|
977
|
+
from: address,
|
|
978
|
+
to: null,
|
|
979
|
+
value: 0n,
|
|
980
|
+
gasUsed: 0n,
|
|
981
|
+
gasPrice: 0n,
|
|
982
|
+
timestamp: s.blockTime ?? 0,
|
|
983
|
+
status: s.err ? "reverted" : "success",
|
|
984
|
+
input: ""
|
|
985
|
+
}));
|
|
986
|
+
}
|
|
987
|
+
async getTokenTransfers(_address, _options) {
|
|
988
|
+
return [];
|
|
989
|
+
}
|
|
990
|
+
async getContractCode(address) {
|
|
991
|
+
const result = await this.rpc("getAccountInfo", [address, { encoding: "base64" }]);
|
|
992
|
+
return result?.value?.data?.[0] ?? "";
|
|
993
|
+
}
|
|
994
|
+
async readContract(_address, _abi, _functionName, _args) {
|
|
995
|
+
throw new Error("readContract not supported on Solana \u2014 use program-specific methods");
|
|
996
|
+
}
|
|
997
|
+
async getContractEvents(_address, _abi, _eventName, _options) {
|
|
998
|
+
return [];
|
|
999
|
+
}
|
|
1000
|
+
async getTokenInfo(address) {
|
|
1001
|
+
const result = await this.rpc("getAccountInfo", [address, { encoding: "jsonParsed" }]);
|
|
1002
|
+
const info = result?.value?.data?.parsed?.info;
|
|
1003
|
+
return {
|
|
1004
|
+
address,
|
|
1005
|
+
name: "SPL Token",
|
|
1006
|
+
symbol: "SPL",
|
|
1007
|
+
decimals: info?.decimals ?? 9,
|
|
1008
|
+
totalSupply: BigInt(info?.supply ?? "0")
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
async getTopHolders(_tokenAddress, _limit) {
|
|
1012
|
+
return [];
|
|
1013
|
+
}
|
|
1014
|
+
async getBlockNumber() {
|
|
1015
|
+
const slot = await this.rpc("getSlot");
|
|
1016
|
+
return BigInt(slot);
|
|
1017
|
+
}
|
|
1018
|
+
async getBlock(blockNumber) {
|
|
1019
|
+
const result = await this.rpc("getBlock", [
|
|
1020
|
+
Number(blockNumber),
|
|
1021
|
+
{ transactionDetails: "none" }
|
|
1022
|
+
]);
|
|
1023
|
+
return {
|
|
1024
|
+
number: blockNumber,
|
|
1025
|
+
hash: result?.blockhash ?? "",
|
|
1026
|
+
parentHash: "",
|
|
1027
|
+
timestamp: result?.blockTime ?? 0,
|
|
1028
|
+
gasUsed: 0n,
|
|
1029
|
+
gasLimit: 0n,
|
|
1030
|
+
baseFeePerGas: null,
|
|
1031
|
+
transactionCount: result?.transactions?.length ?? 0
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// src/chains/sui/adapter.ts
|
|
1039
|
+
var log3, SuiAdapter;
|
|
1040
|
+
var init_adapter4 = __esm({
|
|
1041
|
+
"src/chains/sui/adapter.ts"() {
|
|
1042
|
+
"use strict";
|
|
1043
|
+
init_logger();
|
|
1044
|
+
log3 = createLogger("sui-adapter");
|
|
1045
|
+
SuiAdapter = class {
|
|
1046
|
+
chainId = "sui";
|
|
1047
|
+
name = "Sui";
|
|
1048
|
+
nativeCurrency = { symbol: "SUI", decimals: 9 };
|
|
1049
|
+
rpcUrl = "https://fullnode.mainnet.sui.io:443";
|
|
1050
|
+
connected = false;
|
|
1051
|
+
async connect(rpcUrl) {
|
|
1052
|
+
if (rpcUrl) this.rpcUrl = rpcUrl;
|
|
1053
|
+
this.connected = true;
|
|
1054
|
+
log3.info(`Connected to Sui: ${this.rpcUrl}`);
|
|
1055
|
+
}
|
|
1056
|
+
async disconnect() {
|
|
1057
|
+
this.connected = false;
|
|
1058
|
+
}
|
|
1059
|
+
isConnected() {
|
|
1060
|
+
return this.connected;
|
|
1061
|
+
}
|
|
1062
|
+
async rpc(method, params = []) {
|
|
1063
|
+
const res = await fetch(this.rpcUrl, {
|
|
1064
|
+
method: "POST",
|
|
1065
|
+
headers: { "Content-Type": "application/json" },
|
|
1066
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
1067
|
+
signal: AbortSignal.timeout(15e3)
|
|
1068
|
+
});
|
|
1069
|
+
const json = await res.json();
|
|
1070
|
+
if (json.error) throw new Error(`Sui RPC: ${json.error.message}`);
|
|
1071
|
+
return json.result;
|
|
1072
|
+
}
|
|
1073
|
+
async getBalance(address) {
|
|
1074
|
+
const result = await this.rpc("suix_getBalance", [address, "0x2::sui::SUI"]);
|
|
1075
|
+
return BigInt(result.totalBalance);
|
|
1076
|
+
}
|
|
1077
|
+
async getTokenBalance(address, tokenAddress) {
|
|
1078
|
+
const result = await this.rpc("suix_getBalance", [address, tokenAddress]);
|
|
1079
|
+
return BigInt(result.totalBalance);
|
|
1080
|
+
}
|
|
1081
|
+
async getTransactionHistory(address, options) {
|
|
1082
|
+
const limit = options?.limit ?? 20;
|
|
1083
|
+
const result = await this.rpc("suix_queryTransactionBlocks", [
|
|
1084
|
+
{ filter: { FromAddress: address } },
|
|
1085
|
+
null,
|
|
1086
|
+
limit,
|
|
1087
|
+
true
|
|
1088
|
+
]);
|
|
1089
|
+
return (result.data ?? []).map((tx) => ({
|
|
1090
|
+
hash: tx.digest,
|
|
1091
|
+
blockNumber: BigInt(tx.checkpoint ?? "0"),
|
|
1092
|
+
from: address,
|
|
1093
|
+
to: null,
|
|
1094
|
+
value: 0n,
|
|
1095
|
+
gasUsed: 0n,
|
|
1096
|
+
gasPrice: 0n,
|
|
1097
|
+
timestamp: Math.floor(Number(tx.timestampMs) / 1e3),
|
|
1098
|
+
status: "success",
|
|
1099
|
+
input: ""
|
|
1100
|
+
}));
|
|
1101
|
+
}
|
|
1102
|
+
async getTokenTransfers(_address, _options) {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
async getContractCode(address) {
|
|
1106
|
+
const result = await this.rpc("sui_getObject", [address, { showContent: true }]);
|
|
1107
|
+
return result.data?.content?.type ?? "";
|
|
1108
|
+
}
|
|
1109
|
+
async readContract(_address, _abi, _functionName, _args) {
|
|
1110
|
+
throw new Error("readContract not supported on Sui \u2014 use Move call methods");
|
|
1111
|
+
}
|
|
1112
|
+
async getContractEvents(_address, _abi, _eventName, _options) {
|
|
1113
|
+
return [];
|
|
1114
|
+
}
|
|
1115
|
+
async getTokenInfo(address) {
|
|
1116
|
+
const result = await this.rpc("suix_getCoinMetadata", [address]);
|
|
1117
|
+
return {
|
|
1118
|
+
address,
|
|
1119
|
+
name: result?.name ?? "Unknown",
|
|
1120
|
+
symbol: result?.symbol ?? "???",
|
|
1121
|
+
decimals: result?.decimals ?? 9,
|
|
1122
|
+
totalSupply: 0n
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
async getTopHolders(_tokenAddress, _limit) {
|
|
1126
|
+
return [];
|
|
1127
|
+
}
|
|
1128
|
+
async getBlockNumber() {
|
|
1129
|
+
const result = await this.rpc("sui_getLatestCheckpointSequenceNumber");
|
|
1130
|
+
return BigInt(result);
|
|
1131
|
+
}
|
|
1132
|
+
async getBlock(blockNumber) {
|
|
1133
|
+
const result = await this.rpc("sui_getCheckpoint", [String(blockNumber)]);
|
|
1134
|
+
return {
|
|
1135
|
+
number: blockNumber,
|
|
1136
|
+
hash: result?.digest ?? "",
|
|
1137
|
+
parentHash: result?.previousDigest ?? "",
|
|
1138
|
+
timestamp: result ? Math.floor(Number(result.timestampMs) / 1e3) : 0,
|
|
1139
|
+
gasUsed: 0n,
|
|
1140
|
+
gasLimit: 0n,
|
|
1141
|
+
baseFeePerGas: null,
|
|
1142
|
+
transactionCount: result?.transactions?.length ?? 0
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// src/chains/aptos/adapter.ts
|
|
1150
|
+
var log4, AptosAdapter;
|
|
1151
|
+
var init_adapter5 = __esm({
|
|
1152
|
+
"src/chains/aptos/adapter.ts"() {
|
|
1153
|
+
"use strict";
|
|
1154
|
+
init_logger();
|
|
1155
|
+
log4 = createLogger("aptos-adapter");
|
|
1156
|
+
AptosAdapter = class {
|
|
1157
|
+
chainId = "aptos";
|
|
1158
|
+
name = "Aptos";
|
|
1159
|
+
nativeCurrency = { symbol: "APT", decimals: 8 };
|
|
1160
|
+
apiUrl = "https://fullnode.mainnet.aptoslabs.com/v1";
|
|
1161
|
+
connected = false;
|
|
1162
|
+
async connect(rpcUrl) {
|
|
1163
|
+
if (rpcUrl) this.apiUrl = rpcUrl;
|
|
1164
|
+
this.connected = true;
|
|
1165
|
+
log4.info(`Connected to Aptos: ${this.apiUrl}`);
|
|
1166
|
+
}
|
|
1167
|
+
async disconnect() {
|
|
1168
|
+
this.connected = false;
|
|
1169
|
+
}
|
|
1170
|
+
isConnected() {
|
|
1171
|
+
return this.connected;
|
|
1172
|
+
}
|
|
1173
|
+
async api(path) {
|
|
1174
|
+
const res = await fetch(`${this.apiUrl}${path}`, {
|
|
1175
|
+
signal: AbortSignal.timeout(15e3)
|
|
1176
|
+
});
|
|
1177
|
+
if (!res.ok) throw new Error(`Aptos API: ${res.status} ${res.statusText}`);
|
|
1178
|
+
return res.json();
|
|
1179
|
+
}
|
|
1180
|
+
async getBalance(address) {
|
|
1181
|
+
try {
|
|
1182
|
+
const result = await this.api(
|
|
1183
|
+
`/accounts/${address}/resource/0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>`
|
|
1184
|
+
);
|
|
1185
|
+
return BigInt(result.data.coin.value);
|
|
1186
|
+
} catch {
|
|
1187
|
+
return 0n;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async getTokenBalance(address, tokenAddress) {
|
|
1191
|
+
try {
|
|
1192
|
+
const result = await this.api(
|
|
1193
|
+
`/accounts/${address}/resource/0x1::coin::CoinStore<${tokenAddress}>`
|
|
1194
|
+
);
|
|
1195
|
+
return BigInt(result.data.coin.value);
|
|
1196
|
+
} catch {
|
|
1197
|
+
return 0n;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
async getTransactionHistory(address, options) {
|
|
1201
|
+
const limit = options?.limit ?? 20;
|
|
1202
|
+
const txns = await this.api(`/accounts/${address}/transactions?limit=${limit}`);
|
|
1203
|
+
return txns.map((tx) => ({
|
|
1204
|
+
hash: tx.hash,
|
|
1205
|
+
blockNumber: BigInt(tx.version),
|
|
1206
|
+
from: tx.sender,
|
|
1207
|
+
to: null,
|
|
1208
|
+
value: 0n,
|
|
1209
|
+
gasUsed: BigInt(tx.gas_used),
|
|
1210
|
+
gasPrice: BigInt(tx.gas_unit_price),
|
|
1211
|
+
timestamp: Math.floor(Number(tx.timestamp) / 1e6),
|
|
1212
|
+
status: tx.success ? "success" : "reverted",
|
|
1213
|
+
input: ""
|
|
1214
|
+
}));
|
|
1215
|
+
}
|
|
1216
|
+
async getTokenTransfers(_address, _options) {
|
|
1217
|
+
return [];
|
|
1218
|
+
}
|
|
1219
|
+
async getContractCode(address) {
|
|
1220
|
+
try {
|
|
1221
|
+
const result = await this.api(`/accounts/${address}/modules`);
|
|
1222
|
+
return result.map((m) => m.bytecode).join("");
|
|
1223
|
+
} catch {
|
|
1224
|
+
return "";
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async readContract(_address, _abi, _functionName, _args) {
|
|
1228
|
+
throw new Error("readContract not supported on Aptos \u2014 use Move view functions");
|
|
1229
|
+
}
|
|
1230
|
+
async getContractEvents(_address, _abi, _eventName, _options) {
|
|
1231
|
+
return [];
|
|
1232
|
+
}
|
|
1233
|
+
async getTokenInfo(address) {
|
|
1234
|
+
return {
|
|
1235
|
+
address,
|
|
1236
|
+
name: "Move Coin",
|
|
1237
|
+
symbol: "MOVE",
|
|
1238
|
+
decimals: 8,
|
|
1239
|
+
totalSupply: 0n
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
async getTopHolders(_tokenAddress, _limit) {
|
|
1243
|
+
return [];
|
|
1244
|
+
}
|
|
1245
|
+
async getBlockNumber() {
|
|
1246
|
+
const result = await this.api("/");
|
|
1247
|
+
return BigInt(result.ledger_version);
|
|
1248
|
+
}
|
|
1249
|
+
async getBlock(blockNumber) {
|
|
1250
|
+
const result = await this.api(`/blocks/by_version/${blockNumber}`);
|
|
1251
|
+
return {
|
|
1252
|
+
number: BigInt(result.block_height),
|
|
1253
|
+
hash: result.block_hash,
|
|
1254
|
+
parentHash: "",
|
|
1255
|
+
timestamp: Math.floor(Number(result.block_timestamp) / 1e6),
|
|
1256
|
+
gasUsed: 0n,
|
|
1257
|
+
gasLimit: 0n,
|
|
1258
|
+
baseFeePerGas: null,
|
|
1259
|
+
transactionCount: result.transactions?.length ?? 0
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// src/chains/ton/adapter.ts
|
|
1267
|
+
var log5, TonAdapter;
|
|
1268
|
+
var init_adapter6 = __esm({
|
|
1269
|
+
"src/chains/ton/adapter.ts"() {
|
|
1270
|
+
"use strict";
|
|
1271
|
+
init_logger();
|
|
1272
|
+
log5 = createLogger("ton-adapter");
|
|
1273
|
+
TonAdapter = class {
|
|
1274
|
+
chainId = "ton";
|
|
1275
|
+
name = "TON";
|
|
1276
|
+
nativeCurrency = { symbol: "TON", decimals: 9 };
|
|
1277
|
+
apiUrl = "https://toncenter.com/api/v2";
|
|
1278
|
+
connected = false;
|
|
1279
|
+
async connect(rpcUrl) {
|
|
1280
|
+
if (rpcUrl) this.apiUrl = rpcUrl;
|
|
1281
|
+
this.connected = true;
|
|
1282
|
+
log5.info(`Connected to TON: ${this.apiUrl}`);
|
|
1283
|
+
}
|
|
1284
|
+
async disconnect() {
|
|
1285
|
+
this.connected = false;
|
|
1286
|
+
}
|
|
1287
|
+
isConnected() {
|
|
1288
|
+
return this.connected;
|
|
1289
|
+
}
|
|
1290
|
+
async api(method, params = {}) {
|
|
1291
|
+
const qs = new URLSearchParams(params).toString();
|
|
1292
|
+
const url = `${this.apiUrl}/${method}${qs ? `?${qs}` : ""}`;
|
|
1293
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
1294
|
+
if (!res.ok) throw new Error(`TON API: ${res.status}`);
|
|
1295
|
+
const json = await res.json();
|
|
1296
|
+
if (!json.ok) throw new Error(`TON API: ${json.error ?? "unknown error"}`);
|
|
1297
|
+
return json.result;
|
|
1298
|
+
}
|
|
1299
|
+
async getBalance(address) {
|
|
1300
|
+
const result = await this.api("getAddressBalance", { address });
|
|
1301
|
+
return BigInt(result);
|
|
1302
|
+
}
|
|
1303
|
+
async getTokenBalance(_address, _tokenAddress) {
|
|
1304
|
+
return 0n;
|
|
1305
|
+
}
|
|
1306
|
+
async getTransactionHistory(address, options) {
|
|
1307
|
+
const limit = options?.limit ?? 20;
|
|
1308
|
+
const txns = await this.api("getTransactions", {
|
|
1309
|
+
address,
|
|
1310
|
+
limit: String(limit)
|
|
1311
|
+
});
|
|
1312
|
+
return txns.map((tx) => ({
|
|
1313
|
+
hash: tx.transaction_id.hash,
|
|
1314
|
+
blockNumber: 0n,
|
|
1315
|
+
from: tx.in_msg?.source ?? address,
|
|
1316
|
+
to: tx.in_msg?.destination ?? null,
|
|
1317
|
+
value: BigInt(tx.in_msg?.value ?? "0"),
|
|
1318
|
+
gasUsed: BigInt(tx.fee),
|
|
1319
|
+
gasPrice: 0n,
|
|
1320
|
+
timestamp: tx.utime,
|
|
1321
|
+
status: "success",
|
|
1322
|
+
input: ""
|
|
1323
|
+
}));
|
|
1324
|
+
}
|
|
1325
|
+
async getTokenTransfers(_address, _options) {
|
|
1326
|
+
return [];
|
|
1327
|
+
}
|
|
1328
|
+
async getContractCode(address) {
|
|
1329
|
+
try {
|
|
1330
|
+
const result = await this.api("getAddressInformation", { address });
|
|
1331
|
+
return result.code ?? "";
|
|
1332
|
+
} catch {
|
|
1333
|
+
return "";
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async readContract(_address, _abi, _functionName, _args) {
|
|
1337
|
+
throw new Error("readContract not supported on TON \u2014 use get methods");
|
|
1338
|
+
}
|
|
1339
|
+
async getContractEvents(_address, _abi, _eventName, _options) {
|
|
1340
|
+
return [];
|
|
1341
|
+
}
|
|
1342
|
+
async getTokenInfo(_address) {
|
|
1343
|
+
return {
|
|
1344
|
+
address: _address,
|
|
1345
|
+
name: "Jetton",
|
|
1346
|
+
symbol: "JET",
|
|
1347
|
+
decimals: 9,
|
|
1348
|
+
totalSupply: 0n
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
async getTopHolders(_tokenAddress, _limit) {
|
|
1352
|
+
return [];
|
|
1353
|
+
}
|
|
1354
|
+
async getBlockNumber() {
|
|
1355
|
+
const result = await this.api("getMasterchainInfo");
|
|
1356
|
+
return BigInt(result.last.seqno);
|
|
1357
|
+
}
|
|
1358
|
+
async getBlock(blockNumber) {
|
|
1359
|
+
return {
|
|
1360
|
+
number: blockNumber,
|
|
1361
|
+
hash: "",
|
|
1362
|
+
parentHash: "",
|
|
1363
|
+
timestamp: 0,
|
|
1364
|
+
gasUsed: 0n,
|
|
1365
|
+
gasLimit: 0n,
|
|
1366
|
+
baseFeePerGas: null,
|
|
1367
|
+
transactionCount: 0
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
|
|
839
1374
|
// src/chains/registry.ts
|
|
840
1375
|
function getAdapter(chainId) {
|
|
841
1376
|
const factory = registry.get(chainId);
|
|
@@ -854,7 +1389,12 @@ var init_registry = __esm({
|
|
|
854
1389
|
"src/chains/registry.ts"() {
|
|
855
1390
|
"use strict";
|
|
856
1391
|
init_adapter();
|
|
1392
|
+
init_writable_adapter();
|
|
857
1393
|
init_adapter2();
|
|
1394
|
+
init_adapter3();
|
|
1395
|
+
init_adapter4();
|
|
1396
|
+
init_adapter5();
|
|
1397
|
+
init_adapter6();
|
|
858
1398
|
registry = /* @__PURE__ */ new Map();
|
|
859
1399
|
evmFactory = (chainId) => new EvmAdapter(chainId);
|
|
860
1400
|
BUILTIN_EVM_CHAINS = ["ethereum", "polygon", "arbitrum", "optimism", "base"];
|
|
@@ -865,32 +1405,12 @@ var init_registry = __esm({
|
|
|
865
1405
|
for (const chainId of getZkChainIds()) {
|
|
866
1406
|
registry.set(chainId, zkFactory);
|
|
867
1407
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const isDev = process.env["NODE_ENV"] !== "production";
|
|
875
|
-
if (isDev) {
|
|
876
|
-
return pino({
|
|
877
|
-
name,
|
|
878
|
-
level,
|
|
879
|
-
transport: {
|
|
880
|
-
target: "pino-pretty",
|
|
881
|
-
options: {
|
|
882
|
-
colorize: true
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
return pino({ name, level });
|
|
888
|
-
}
|
|
889
|
-
var level;
|
|
890
|
-
var init_logger = __esm({
|
|
891
|
-
"src/utils/logger.ts"() {
|
|
892
|
-
"use strict";
|
|
893
|
-
level = process.env["VIZZOR_LOG_LEVEL"] ?? "info";
|
|
1408
|
+
registry.set("bsc", evmFactory);
|
|
1409
|
+
registry.set("avalanche", evmFactory);
|
|
1410
|
+
registry.set("solana", () => new SolanaAdapter());
|
|
1411
|
+
registry.set("sui", () => new SuiAdapter());
|
|
1412
|
+
registry.set("aptos", () => new AptosAdapter());
|
|
1413
|
+
registry.set("ton", () => new TonAdapter());
|
|
894
1414
|
}
|
|
895
1415
|
});
|
|
896
1416
|
|
|
@@ -902,12 +1422,12 @@ function initMLClient(url) {
|
|
|
902
1422
|
function getMLClient() {
|
|
903
1423
|
return mlClient;
|
|
904
1424
|
}
|
|
905
|
-
var
|
|
1425
|
+
var log6, MLClient, mlClient;
|
|
906
1426
|
var init_client = __esm({
|
|
907
1427
|
"src/ml/client.ts"() {
|
|
908
1428
|
"use strict";
|
|
909
1429
|
init_logger();
|
|
910
|
-
|
|
1430
|
+
log6 = createLogger("ml-client");
|
|
911
1431
|
MLClient = class {
|
|
912
1432
|
baseUrl;
|
|
913
1433
|
healthy = false;
|
|
@@ -925,7 +1445,7 @@ var init_client = __esm({
|
|
|
925
1445
|
if (!res.ok) return null;
|
|
926
1446
|
return await res.json();
|
|
927
1447
|
} catch (err) {
|
|
928
|
-
|
|
1448
|
+
log6.debug(`ML predict failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
929
1449
|
return null;
|
|
930
1450
|
}
|
|
931
1451
|
}
|
|
@@ -941,7 +1461,7 @@ var init_client = __esm({
|
|
|
941
1461
|
const data = await res.json();
|
|
942
1462
|
return data.predictions;
|
|
943
1463
|
} catch (err) {
|
|
944
|
-
|
|
1464
|
+
log6.debug(`ML batch predict failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
945
1465
|
return [];
|
|
946
1466
|
}
|
|
947
1467
|
}
|
|
@@ -957,7 +1477,7 @@ var init_client = __esm({
|
|
|
957
1477
|
const data = await res.json();
|
|
958
1478
|
return data.anomalies;
|
|
959
1479
|
} catch (err) {
|
|
960
|
-
|
|
1480
|
+
log6.debug(`ML anomaly detection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
961
1481
|
return [];
|
|
962
1482
|
}
|
|
963
1483
|
}
|
|
@@ -984,7 +1504,7 @@ var init_client = __esm({
|
|
|
984
1504
|
if (!res.ok) return null;
|
|
985
1505
|
return await res.json();
|
|
986
1506
|
} catch (err) {
|
|
987
|
-
|
|
1507
|
+
log6.debug(`ML rug predict failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
988
1508
|
return null;
|
|
989
1509
|
}
|
|
990
1510
|
}
|
|
@@ -999,7 +1519,7 @@ var init_client = __esm({
|
|
|
999
1519
|
if (!res.ok) return null;
|
|
1000
1520
|
return await res.json();
|
|
1001
1521
|
} catch (err) {
|
|
1002
|
-
|
|
1522
|
+
log6.debug(`ML wallet classify failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1003
1523
|
return null;
|
|
1004
1524
|
}
|
|
1005
1525
|
}
|
|
@@ -1014,7 +1534,7 @@ var init_client = __esm({
|
|
|
1014
1534
|
if (!res.ok) return null;
|
|
1015
1535
|
return await res.json();
|
|
1016
1536
|
} catch (err) {
|
|
1017
|
-
|
|
1537
|
+
log6.debug(`ML sentiment failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1018
1538
|
return null;
|
|
1019
1539
|
}
|
|
1020
1540
|
}
|
|
@@ -1030,7 +1550,7 @@ var init_client = __esm({
|
|
|
1030
1550
|
const data = await res.json();
|
|
1031
1551
|
return data.results;
|
|
1032
1552
|
} catch (err) {
|
|
1033
|
-
|
|
1553
|
+
log6.debug(`ML sentiment batch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1034
1554
|
return [];
|
|
1035
1555
|
}
|
|
1036
1556
|
}
|
|
@@ -1048,7 +1568,7 @@ var init_client = __esm({
|
|
|
1048
1568
|
if (!res.ok) return null;
|
|
1049
1569
|
return await res.json();
|
|
1050
1570
|
} catch (err) {
|
|
1051
|
-
|
|
1571
|
+
log6.debug(`ML trend score failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1052
1572
|
return null;
|
|
1053
1573
|
}
|
|
1054
1574
|
}
|
|
@@ -1063,7 +1583,7 @@ var init_client = __esm({
|
|
|
1063
1583
|
if (!res.ok) return null;
|
|
1064
1584
|
return await res.json();
|
|
1065
1585
|
} catch (err) {
|
|
1066
|
-
|
|
1586
|
+
log6.debug(`ML TA interpret failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1067
1587
|
return null;
|
|
1068
1588
|
}
|
|
1069
1589
|
}
|
|
@@ -1078,7 +1598,7 @@ var init_client = __esm({
|
|
|
1078
1598
|
if (!res.ok) return null;
|
|
1079
1599
|
return await res.json();
|
|
1080
1600
|
} catch (err) {
|
|
1081
|
-
|
|
1601
|
+
log6.debug(`ML strategy eval failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1082
1602
|
return null;
|
|
1083
1603
|
}
|
|
1084
1604
|
}
|
|
@@ -1093,7 +1613,7 @@ var init_client = __esm({
|
|
|
1093
1613
|
if (!res.ok) return null;
|
|
1094
1614
|
return await res.json();
|
|
1095
1615
|
} catch (err) {
|
|
1096
|
-
|
|
1616
|
+
log6.debug(`ML regime detect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1097
1617
|
return null;
|
|
1098
1618
|
}
|
|
1099
1619
|
}
|
|
@@ -1108,7 +1628,7 @@ var init_client = __esm({
|
|
|
1108
1628
|
if (!res.ok) return null;
|
|
1109
1629
|
return await res.json();
|
|
1110
1630
|
} catch (err) {
|
|
1111
|
-
|
|
1631
|
+
log6.debug(`ML project risk failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1112
1632
|
return null;
|
|
1113
1633
|
}
|
|
1114
1634
|
}
|
|
@@ -1123,7 +1643,7 @@ var init_client = __esm({
|
|
|
1123
1643
|
if (!res.ok) return null;
|
|
1124
1644
|
return await res.json();
|
|
1125
1645
|
} catch (err) {
|
|
1126
|
-
|
|
1646
|
+
log6.debug(`ML portfolio opt failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1127
1647
|
return null;
|
|
1128
1648
|
}
|
|
1129
1649
|
}
|
|
@@ -1132,43 +1652,74 @@ var init_client = __esm({
|
|
|
1132
1652
|
const res = await fetch(`${this.baseUrl}/predict/intent`, {
|
|
1133
1653
|
method: "POST",
|
|
1134
1654
|
headers: { "Content-Type": "application/json" },
|
|
1135
|
-
body: JSON.stringify({ text }),
|
|
1655
|
+
body: JSON.stringify({ text }),
|
|
1656
|
+
signal: AbortSignal.timeout(5e3)
|
|
1657
|
+
});
|
|
1658
|
+
if (!res.ok) return null;
|
|
1659
|
+
return await res.json();
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
log6.debug(`ML intent classify failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
async scoreBytecodeRisk(features) {
|
|
1666
|
+
try {
|
|
1667
|
+
const res = await fetch(`${this.baseUrl}/predict/bytecode-risk`, {
|
|
1668
|
+
method: "POST",
|
|
1669
|
+
headers: { "Content-Type": "application/json" },
|
|
1670
|
+
body: JSON.stringify(features),
|
|
1671
|
+
signal: AbortSignal.timeout(5e3)
|
|
1672
|
+
});
|
|
1673
|
+
if (!res.ok) return null;
|
|
1674
|
+
return await res.json();
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
log6.debug(`ML bytecode risk failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
async predictPortfolioForward(features) {
|
|
1681
|
+
try {
|
|
1682
|
+
const res = await fetch(`${this.baseUrl}/predict/portfolio-forward`, {
|
|
1683
|
+
method: "POST",
|
|
1684
|
+
headers: { "Content-Type": "application/json" },
|
|
1685
|
+
body: JSON.stringify(features),
|
|
1136
1686
|
signal: AbortSignal.timeout(5e3)
|
|
1137
1687
|
});
|
|
1138
1688
|
if (!res.ok) return null;
|
|
1139
1689
|
return await res.json();
|
|
1140
1690
|
} catch (err) {
|
|
1141
|
-
|
|
1691
|
+
log6.debug(`ML portfolio forward failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1142
1692
|
return null;
|
|
1143
1693
|
}
|
|
1144
1694
|
}
|
|
1145
|
-
async
|
|
1695
|
+
async trainModel(modelName, params) {
|
|
1146
1696
|
try {
|
|
1147
|
-
const res = await fetch(`${this.baseUrl}/
|
|
1697
|
+
const res = await fetch(`${this.baseUrl}/train`, {
|
|
1148
1698
|
method: "POST",
|
|
1149
1699
|
headers: { "Content-Type": "application/json" },
|
|
1150
|
-
body: JSON.stringify(
|
|
1151
|
-
signal: AbortSignal.timeout(
|
|
1700
|
+
body: JSON.stringify({ model: modelName, ...params }),
|
|
1701
|
+
signal: AbortSignal.timeout(3e5)
|
|
1702
|
+
// 5 min for training
|
|
1152
1703
|
});
|
|
1153
1704
|
if (!res.ok) return null;
|
|
1154
1705
|
return await res.json();
|
|
1155
1706
|
} catch (err) {
|
|
1156
|
-
|
|
1707
|
+
log6.debug(`ML train failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1157
1708
|
return null;
|
|
1158
1709
|
}
|
|
1159
1710
|
}
|
|
1160
|
-
async
|
|
1711
|
+
async evaluateModel(modelName) {
|
|
1161
1712
|
try {
|
|
1162
|
-
const res = await fetch(`${this.baseUrl}/
|
|
1713
|
+
const res = await fetch(`${this.baseUrl}/evaluate`, {
|
|
1163
1714
|
method: "POST",
|
|
1164
1715
|
headers: { "Content-Type": "application/json" },
|
|
1165
|
-
body: JSON.stringify(
|
|
1166
|
-
signal: AbortSignal.timeout(
|
|
1716
|
+
body: JSON.stringify({ model: modelName }),
|
|
1717
|
+
signal: AbortSignal.timeout(6e4)
|
|
1167
1718
|
});
|
|
1168
1719
|
if (!res.ok) return null;
|
|
1169
1720
|
return await res.json();
|
|
1170
1721
|
} catch (err) {
|
|
1171
|
-
|
|
1722
|
+
log6.debug(`ML evaluate failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1172
1723
|
return null;
|
|
1173
1724
|
}
|
|
1174
1725
|
}
|
|
@@ -1342,6 +1893,7 @@ __export(scan_exports, {
|
|
|
1342
1893
|
import chalk from "chalk";
|
|
1343
1894
|
import ora from "ora";
|
|
1344
1895
|
async function handleScan(project, options) {
|
|
1896
|
+
assertValidAddress(project);
|
|
1345
1897
|
const spinner = ora(`Scanning ${project} on ${options.chain}...`).start();
|
|
1346
1898
|
try {
|
|
1347
1899
|
const adapter = getAdapter(options.chain);
|
|
@@ -1396,6 +1948,7 @@ var init_scan = __esm({
|
|
|
1396
1948
|
init_loader();
|
|
1397
1949
|
init_project_analyzer();
|
|
1398
1950
|
init_risk_scorer();
|
|
1951
|
+
init_validate();
|
|
1399
1952
|
}
|
|
1400
1953
|
});
|
|
1401
1954
|
|
|
@@ -1560,9 +2113,18 @@ async function fetchCryptoNews(symbol, apiToken) {
|
|
|
1560
2113
|
const url = `${BASE_URL2}/posts/?${params.toString()}`;
|
|
1561
2114
|
const res = await fetch(url);
|
|
1562
2115
|
if (!res.ok) {
|
|
1563
|
-
|
|
2116
|
+
return [];
|
|
2117
|
+
}
|
|
2118
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
2119
|
+
if (!contentType.includes("application/json")) {
|
|
2120
|
+
return [];
|
|
2121
|
+
}
|
|
2122
|
+
let data;
|
|
2123
|
+
try {
|
|
2124
|
+
data = await res.json();
|
|
2125
|
+
} catch {
|
|
2126
|
+
return [];
|
|
1564
2127
|
}
|
|
1565
|
-
const data = await res.json();
|
|
1566
2128
|
const posts = data.results ?? [];
|
|
1567
2129
|
return posts.map((post) => {
|
|
1568
2130
|
const { positive, negative } = post.votes;
|
|
@@ -1718,7 +2280,7 @@ async function fetchJson2(url) {
|
|
|
1718
2280
|
}
|
|
1719
2281
|
async function fetchTickerPrice(symbol) {
|
|
1720
2282
|
const pair = resolvePair(symbol);
|
|
1721
|
-
const data = await fetchJson2(`${BASE_URL3}/ticker/24hr?symbol=${pair}`);
|
|
2283
|
+
const data = await fetchJson2(`${BASE_URL3}/ticker/24hr?symbol=${encodeURIComponent(pair)}`);
|
|
1722
2284
|
return {
|
|
1723
2285
|
symbol: symbol.toUpperCase(),
|
|
1724
2286
|
price: parseFloat(data.lastPrice),
|
|
@@ -1728,7 +2290,7 @@ async function fetchTickerPrice(symbol) {
|
|
|
1728
2290
|
async function fetchKlines(symbol, interval, limit = 100) {
|
|
1729
2291
|
const pair = resolvePair(symbol);
|
|
1730
2292
|
const data = await fetchJson2(
|
|
1731
|
-
`${BASE_URL3}/klines?symbol=${pair}&interval=${interval}&limit=${limit}`
|
|
2293
|
+
`${BASE_URL3}/klines?symbol=${encodeURIComponent(pair)}&interval=${interval}&limit=${limit}`
|
|
1732
2294
|
);
|
|
1733
2295
|
return data.map((k) => ({
|
|
1734
2296
|
openTime: Number(k[0]),
|
|
@@ -1744,7 +2306,7 @@ async function fetchKlines(symbol, interval, limit = 100) {
|
|
|
1744
2306
|
}
|
|
1745
2307
|
async function fetchFundingRate(symbol) {
|
|
1746
2308
|
const pair = resolvePair(symbol);
|
|
1747
|
-
const data = await fetchJson2(`${FUTURES_URL}/premiumIndex?symbol=${pair}`);
|
|
2309
|
+
const data = await fetchJson2(`${FUTURES_URL}/premiumIndex?symbol=${encodeURIComponent(pair)}`);
|
|
1748
2310
|
const item = data[0];
|
|
1749
2311
|
if (!item) throw new Error(`No funding data for ${pair}`);
|
|
1750
2312
|
return {
|
|
@@ -1758,9 +2320,11 @@ async function fetchOpenInterest(symbol) {
|
|
|
1758
2320
|
const pair = resolvePair(symbol);
|
|
1759
2321
|
const [oiData, tickerData] = await Promise.all([
|
|
1760
2322
|
fetchJson2(
|
|
1761
|
-
`${FUTURES_URL}/openInterest?symbol=${pair}`
|
|
2323
|
+
`${FUTURES_URL}/openInterest?symbol=${encodeURIComponent(pair)}`
|
|
1762
2324
|
),
|
|
1763
|
-
fetchJson2(
|
|
2325
|
+
fetchJson2(
|
|
2326
|
+
`${FUTURES_URL}/ticker/price?symbol=${encodeURIComponent(pair)}`
|
|
2327
|
+
)
|
|
1764
2328
|
]);
|
|
1765
2329
|
const oi = parseFloat(oiData.openInterest);
|
|
1766
2330
|
const price = parseFloat(tickerData.lastPrice);
|
|
@@ -1790,7 +2354,7 @@ async function fetchTopGainersLosers(limit = 10) {
|
|
|
1790
2354
|
async function isValidSymbol(symbol) {
|
|
1791
2355
|
try {
|
|
1792
2356
|
const pair = resolvePair(symbol);
|
|
1793
|
-
await fetchJson2(`${BASE_URL3}/ticker/price?symbol=${pair}`);
|
|
2357
|
+
await fetchJson2(`${BASE_URL3}/ticker/price?symbol=${encodeURIComponent(pair)}`);
|
|
1794
2358
|
return true;
|
|
1795
2359
|
} catch {
|
|
1796
2360
|
return false;
|
|
@@ -2811,6 +3375,7 @@ __export(track_exports, {
|
|
|
2811
3375
|
import chalk3 from "chalk";
|
|
2812
3376
|
import ora3 from "ora";
|
|
2813
3377
|
async function handleTrack(wallet, options) {
|
|
3378
|
+
assertValidAddress(wallet);
|
|
2814
3379
|
const spinner = ora3(`Analyzing wallet ${wallet.slice(0, 10)}... on ${options.chain}`).start();
|
|
2815
3380
|
try {
|
|
2816
3381
|
const adapter = getAdapter(options.chain);
|
|
@@ -2871,6 +3436,7 @@ var init_track = __esm({
|
|
|
2871
3436
|
init_registry();
|
|
2872
3437
|
init_loader();
|
|
2873
3438
|
init_wallet_analyzer();
|
|
3439
|
+
init_validate();
|
|
2874
3440
|
}
|
|
2875
3441
|
});
|
|
2876
3442
|
|
|
@@ -3163,6 +3729,7 @@ __export(audit_exports, {
|
|
|
3163
3729
|
import chalk5 from "chalk";
|
|
3164
3730
|
import ora5 from "ora";
|
|
3165
3731
|
async function handleAudit(contract, options) {
|
|
3732
|
+
assertValidAddress(contract);
|
|
3166
3733
|
const spinner = ora5(`Auditing contract ${contract.slice(0, 10)}... on ${options.chain}`).start();
|
|
3167
3734
|
try {
|
|
3168
3735
|
const adapter = getAdapter(options.chain);
|
|
@@ -3240,6 +3807,7 @@ var init_audit = __esm({
|
|
|
3240
3807
|
init_registry();
|
|
3241
3808
|
init_loader();
|
|
3242
3809
|
init_contract_auditor();
|
|
3810
|
+
init_validate();
|
|
3243
3811
|
}
|
|
3244
3812
|
});
|
|
3245
3813
|
|
|
@@ -4292,7 +4860,9 @@ async function fetchJson6(url) {
|
|
|
4292
4860
|
}
|
|
4293
4861
|
async function checkTokenSecurity(contractAddress, chain) {
|
|
4294
4862
|
const chainId = resolveChainId(chain);
|
|
4295
|
-
const data = await fetchJson6(
|
|
4863
|
+
const data = await fetchJson6(
|
|
4864
|
+
`${BASE_URL6}/token_security/${encodeURIComponent(chainId)}?contract_addresses=${encodeURIComponent(contractAddress.toLowerCase())}`
|
|
4865
|
+
);
|
|
4296
4866
|
if (data.code !== 1) return null;
|
|
4297
4867
|
const addr = contractAddress.toLowerCase();
|
|
4298
4868
|
const raw = data.result[addr];
|
|
@@ -4347,7 +4917,9 @@ async function checkTokenSecurity(contractAddress, chain) {
|
|
|
4347
4917
|
}
|
|
4348
4918
|
async function checkAddressSecurity(address, chain) {
|
|
4349
4919
|
const chainId = resolveChainId(chain);
|
|
4350
|
-
const data = await fetchJson6(
|
|
4920
|
+
const data = await fetchJson6(
|
|
4921
|
+
`${BASE_URL6}/address_security/${encodeURIComponent(chainId)}?address=${encodeURIComponent(address.toLowerCase())}`
|
|
4922
|
+
);
|
|
4351
4923
|
if (data.code !== 1) return null;
|
|
4352
4924
|
const raw = data.result;
|
|
4353
4925
|
const toBool = (v) => v === "1" || v === 1 || v === true;
|
|
@@ -4374,7 +4946,81 @@ var init_goplus = __esm({
|
|
|
4374
4946
|
optimism: "10",
|
|
4375
4947
|
base: "8453",
|
|
4376
4948
|
avalanche: "43114",
|
|
4377
|
-
solana: "solana"
|
|
4949
|
+
solana: "solana",
|
|
4950
|
+
sui: "sui",
|
|
4951
|
+
aptos: "aptos",
|
|
4952
|
+
ton: "ton"
|
|
4953
|
+
};
|
|
4954
|
+
}
|
|
4955
|
+
});
|
|
4956
|
+
|
|
4957
|
+
// src/ai/sanitize.ts
|
|
4958
|
+
function sanitizeExternalData(text, maxLen = LIMITS.generic) {
|
|
4959
|
+
let cleaned = text;
|
|
4960
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
4961
|
+
cleaned = cleaned.replace(pattern, "[FILTERED]");
|
|
4962
|
+
}
|
|
4963
|
+
cleaned = cleaned.replace(/^#{1,6}\s+/gm, "");
|
|
4964
|
+
cleaned = cleaned.replace(/```[\s\S]*?```/g, "[CODE]");
|
|
4965
|
+
if (cleaned.length > maxLen) {
|
|
4966
|
+
cleaned = cleaned.slice(0, maxLen) + "...";
|
|
4967
|
+
}
|
|
4968
|
+
return cleaned;
|
|
4969
|
+
}
|
|
4970
|
+
function sanitizeTokenName(name) {
|
|
4971
|
+
return sanitizeExternalData(name, LIMITS.tokenName);
|
|
4972
|
+
}
|
|
4973
|
+
function sanitizeHeadline(headline) {
|
|
4974
|
+
return sanitizeExternalData(headline, LIMITS.headline);
|
|
4975
|
+
}
|
|
4976
|
+
function wrapUntrustedData(label, data) {
|
|
4977
|
+
return `[BEGIN EXTERNAL DATA: ${label}]
|
|
4978
|
+
${data}
|
|
4979
|
+
[END EXTERNAL DATA: ${label}]`;
|
|
4980
|
+
}
|
|
4981
|
+
function sanitizeToolResult(result) {
|
|
4982
|
+
if (typeof result === "string") {
|
|
4983
|
+
return sanitizeExternalData(result);
|
|
4984
|
+
}
|
|
4985
|
+
if (Array.isArray(result)) {
|
|
4986
|
+
return result.map(sanitizeToolResult);
|
|
4987
|
+
}
|
|
4988
|
+
if (result !== null && typeof result === "object") {
|
|
4989
|
+
const sanitized = {};
|
|
4990
|
+
for (const [key, value] of Object.entries(result)) {
|
|
4991
|
+
sanitized[key] = sanitizeToolResult(value);
|
|
4992
|
+
}
|
|
4993
|
+
return sanitized;
|
|
4994
|
+
}
|
|
4995
|
+
return result;
|
|
4996
|
+
}
|
|
4997
|
+
var INJECTION_PATTERNS, LIMITS;
|
|
4998
|
+
var init_sanitize = __esm({
|
|
4999
|
+
"src/ai/sanitize.ts"() {
|
|
5000
|
+
"use strict";
|
|
5001
|
+
INJECTION_PATTERNS = [
|
|
5002
|
+
/ignore\s+(all\s+)?previous\s+instructions/gi,
|
|
5003
|
+
/ignore\s+(all\s+)?above/gi,
|
|
5004
|
+
/you\s+are\s+now/gi,
|
|
5005
|
+
/system\s*:/gi,
|
|
5006
|
+
/assistant\s*:/gi,
|
|
5007
|
+
/\[system\]/gi,
|
|
5008
|
+
/\[instruction\]/gi,
|
|
5009
|
+
/<\/?system>/gi,
|
|
5010
|
+
/repeat\s+(everything|the\s+prompt|your\s+instructions)/gi,
|
|
5011
|
+
/reveal\s+(your|the)\s+(prompt|instructions|config)/gi,
|
|
5012
|
+
/forget\s+(everything|your\s+instructions)/gi,
|
|
5013
|
+
/override\s+(previous|your|all)/gi,
|
|
5014
|
+
/new\s+instructions?\s*:/gi,
|
|
5015
|
+
/\bdo\s+not\s+follow\b/gi,
|
|
5016
|
+
/\bact\s+as\b/gi,
|
|
5017
|
+
/\brole\s*:\s*(system|assistant)\b/gi
|
|
5018
|
+
];
|
|
5019
|
+
LIMITS = {
|
|
5020
|
+
tokenName: 60,
|
|
5021
|
+
headline: 250,
|
|
5022
|
+
description: 500,
|
|
5023
|
+
generic: 1e3
|
|
4378
5024
|
};
|
|
4379
5025
|
}
|
|
4380
5026
|
});
|
|
@@ -4601,10 +5247,11 @@ async function buildContextBlock(userMessage) {
|
|
|
4601
5247
|
}
|
|
4602
5248
|
output.push(...buildInstructions(queryType, hasSpecificToken, isComplexQuery));
|
|
4603
5249
|
output.push("");
|
|
4604
|
-
|
|
5250
|
+
const raw = output.join("\n");
|
|
5251
|
+
return wrapUntrustedData("MARKET_CONTEXT", raw);
|
|
4605
5252
|
}
|
|
4606
5253
|
function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
|
|
4607
|
-
const
|
|
5254
|
+
const base3 = [
|
|
4608
5255
|
"CRITICAL INSTRUCTIONS (MUST FOLLOW):",
|
|
4609
5256
|
'- NEVER output "--- REAL-TIME DATA ---" or "--- END REAL-TIME DATA ---" markers.',
|
|
4610
5257
|
"- You MUST use ONLY the real-time data above. Do NOT invent or fabricate any data.",
|
|
@@ -4613,7 +5260,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
|
|
|
4613
5260
|
];
|
|
4614
5261
|
switch (queryType) {
|
|
4615
5262
|
case "news":
|
|
4616
|
-
|
|
5263
|
+
base3.push(
|
|
4617
5264
|
"",
|
|
4618
5265
|
"QUERY TYPE: NEWS \u2014 The user wants crypto news and market updates.",
|
|
4619
5266
|
"- Summarize the news headlines from the data above.",
|
|
@@ -4624,7 +5271,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
|
|
|
4624
5271
|
);
|
|
4625
5272
|
break;
|
|
4626
5273
|
case "trends":
|
|
4627
|
-
|
|
5274
|
+
base3.push(
|
|
4628
5275
|
"",
|
|
4629
5276
|
"QUERY TYPE: TRENDS/MARKET OVERVIEW \u2014 The user wants to know what is trending.",
|
|
4630
5277
|
"- Lead with the overall market sentiment (Fear & Greed + BTC direction).",
|
|
@@ -4635,7 +5282,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
|
|
|
4635
5282
|
);
|
|
4636
5283
|
break;
|
|
4637
5284
|
case "token_analysis":
|
|
4638
|
-
|
|
5285
|
+
base3.push(
|
|
4639
5286
|
"",
|
|
4640
5287
|
"QUERY TYPE: TOKEN ANALYSIS \u2014 The user wants deep analysis of a specific token.",
|
|
4641
5288
|
"- Follow the analysis report structure above (Verdict \u2192 Market \u2192 Security \u2192 Signals \u2192 Risks).",
|
|
@@ -4646,7 +5293,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
|
|
|
4646
5293
|
);
|
|
4647
5294
|
break;
|
|
4648
5295
|
case "prediction":
|
|
4649
|
-
|
|
5296
|
+
base3.push(
|
|
4650
5297
|
"",
|
|
4651
5298
|
"QUERY TYPE: PRICE PREDICTION \u2014 The user wants future price projections.",
|
|
4652
5299
|
"- ALWAYS include the PRICE PREDICTION SCENARIOS with the exact dollar values from the data.",
|
|
@@ -4659,7 +5306,7 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
|
|
|
4659
5306
|
);
|
|
4660
5307
|
break;
|
|
4661
5308
|
default:
|
|
4662
|
-
|
|
5309
|
+
base3.push(
|
|
4663
5310
|
"",
|
|
4664
5311
|
"QUERY TYPE: GENERAL \u2014 Answer naturally using the data above as context.",
|
|
4665
5312
|
"- Use the real-time prices and sentiment as background context.",
|
|
@@ -4670,12 +5317,12 @@ function buildInstructions(queryType, hasSpecificToken, isComplexQuery) {
|
|
|
4670
5317
|
break;
|
|
4671
5318
|
}
|
|
4672
5319
|
if (isComplexQuery && hasSpecificToken) {
|
|
4673
|
-
|
|
5320
|
+
base3.push(
|
|
4674
5321
|
"",
|
|
4675
5322
|
"COMPLEX ANALYSIS: Structure your reasoning as Data \u2192 Signals \u2192 Alignment \u2192 Confidence \u2192 Risks \u2192 Conclusion."
|
|
4676
5323
|
);
|
|
4677
5324
|
}
|
|
4678
|
-
return
|
|
5325
|
+
return base3;
|
|
4679
5326
|
}
|
|
4680
5327
|
async function fetchTokenData(tokens, address) {
|
|
4681
5328
|
const lines = ["## Token / Price Data"];
|
|
@@ -4685,7 +5332,7 @@ async function fetchTokenData(tokens, address) {
|
|
|
4685
5332
|
const pair = pairs[0];
|
|
4686
5333
|
if (pair) {
|
|
4687
5334
|
lines.push(
|
|
4688
|
-
`${pair.baseToken.name} (${pair.baseToken.symbol}) on ${pair.chainId}:`,
|
|
5335
|
+
`${sanitizeTokenName(pair.baseToken.name)} (${sanitizeTokenName(pair.baseToken.symbol)}) on ${pair.chainId}:`,
|
|
4689
5336
|
` Price: $${pair.priceUsd ?? "?"}`,
|
|
4690
5337
|
` 24h Volume: $${(pair.volume?.h24 ?? 0).toLocaleString()}`,
|
|
4691
5338
|
` Liquidity: $${(pair.liquidity?.usd ?? 0).toLocaleString()}`,
|
|
@@ -4754,7 +5401,7 @@ async function fetchTokenData(tokens, address) {
|
|
|
4754
5401
|
const pair = pairs[0];
|
|
4755
5402
|
if (pair) {
|
|
4756
5403
|
lines.push(
|
|
4757
|
-
`${pair.baseToken.name} (${pair.baseToken.symbol}) on ${pair.chainId}:`,
|
|
5404
|
+
`${sanitizeTokenName(pair.baseToken.name)} (${sanitizeTokenName(pair.baseToken.symbol)}) on ${pair.chainId}:`,
|
|
4758
5405
|
` Price: $${pair.priceUsd ?? "?"}`,
|
|
4759
5406
|
` 24h Volume: $${(pair.volume?.h24 ?? 0).toLocaleString()}`,
|
|
4760
5407
|
` 24h Change: ${(pair.priceChange?.h24 ?? 0) > 0 ? "+" : ""}${(pair.priceChange?.h24 ?? 0).toFixed(2)}%`,
|
|
@@ -4773,7 +5420,7 @@ async function fetchTrendingData() {
|
|
|
4773
5420
|
const lines = ["## Trending Tokens (live)"];
|
|
4774
5421
|
for (const t of trending.slice(0, 10)) {
|
|
4775
5422
|
lines.push(
|
|
4776
|
-
`- ${t.name} (${t.symbol}) on ${t.chain}: $${t.priceUsd} | 24h: ${t.priceChange24h > 0 ? "+" : ""}${t.priceChange24h.toFixed(1)}% | Vol: $${t.volume24h.toLocaleString()} [${t.source}]`
|
|
5423
|
+
`- ${sanitizeTokenName(t.name)} (${sanitizeTokenName(t.symbol)}) on ${t.chain}: $${t.priceUsd} | 24h: ${t.priceChange24h > 0 ? "+" : ""}${t.priceChange24h.toFixed(1)}% | Vol: $${t.volume24h.toLocaleString()} [${t.source}]`
|
|
4777
5424
|
);
|
|
4778
5425
|
}
|
|
4779
5426
|
return lines.join("\n");
|
|
@@ -4797,7 +5444,9 @@ async function fetchNewsData(symbol) {
|
|
|
4797
5444
|
const n = headlines[i];
|
|
4798
5445
|
const ml = mlResults[i];
|
|
4799
5446
|
const label = ml ? `${ml.sentiment.toUpperCase()} (${(ml.confidence * 100).toFixed(0)}%)` : n.sentiment.toUpperCase();
|
|
4800
|
-
lines.push(
|
|
5447
|
+
lines.push(
|
|
5448
|
+
`- [${label}] ${sanitizeHeadline(n.title)} (${sanitizeTokenName(n.source.title)}, ${n.publishedAt})`
|
|
5449
|
+
);
|
|
4801
5450
|
}
|
|
4802
5451
|
const avgScore = mlResults.reduce((s, r) => s + r.score, 0) / mlResults.length;
|
|
4803
5452
|
const avgSentiment = avgScore > 0.2 ? "BULLISH" : avgScore < -0.2 ? "BEARISH" : "NEUTRAL";
|
|
@@ -4810,7 +5459,7 @@ ML Aggregate Sentiment: ${avgSentiment} (score: ${avgScore.toFixed(3)})`);
|
|
|
4810
5459
|
}
|
|
4811
5460
|
for (const n of headlines) {
|
|
4812
5461
|
lines.push(
|
|
4813
|
-
`- [${n.sentiment.toUpperCase()}] ${n.title} (${n.source.title}, ${n.publishedAt})`
|
|
5462
|
+
`- [${n.sentiment.toUpperCase()}] ${sanitizeHeadline(n.title)} (${sanitizeTokenName(n.source.title)}, ${n.publishedAt})`
|
|
4814
5463
|
);
|
|
4815
5464
|
}
|
|
4816
5465
|
return lines.join("\n");
|
|
@@ -4850,7 +5499,7 @@ async function fetchRaisesData() {
|
|
|
4850
5499
|
const amount = r.amount ? r.amount >= 1e9 ? `$${(r.amount / 1e9).toFixed(1)}B` : r.amount >= 1e6 ? `$${(r.amount / 1e6).toFixed(1)}M` : r.amount >= 1e3 ? `$${(r.amount / 1e3).toFixed(0)}K` : `$${r.amount.toLocaleString()}` : "undisclosed";
|
|
4851
5500
|
const date = new Date(r.date * 1e3).toISOString().split("T")[0];
|
|
4852
5501
|
lines.push(
|
|
4853
|
-
`- ${r.name} \u2014 ${r.round} (${amount}) on ${r.chains.join(", ") || "multi-chain"} [${date}]${r.leadInvestors.length > 0 ? ` Led by: ${r.leadInvestors.join(", ")}` : ""}`
|
|
5502
|
+
`- ${sanitizeTokenName(r.name)} \u2014 ${sanitizeExternalData(r.round, 50)} (${amount}) on ${r.chains.join(", ") || "multi-chain"} [${date}]${r.leadInvestors.length > 0 ? ` Led by: ${r.leadInvestors.map((inv) => sanitizeTokenName(inv)).join(", ")}` : ""}`
|
|
4854
5503
|
);
|
|
4855
5504
|
}
|
|
4856
5505
|
return lines.join("\n");
|
|
@@ -4865,7 +5514,9 @@ async function fetchPumpData() {
|
|
|
4865
5514
|
const lines = ["## Latest Pump.fun Launches (Solana)"];
|
|
4866
5515
|
for (const c of coins) {
|
|
4867
5516
|
const mcap = c.usd_market_cap ? `$${c.usd_market_cap.toFixed(0)}` : "?";
|
|
4868
|
-
lines.push(
|
|
5517
|
+
lines.push(
|
|
5518
|
+
`- ${sanitizeTokenName(c.name)} (${sanitizeTokenName(c.symbol)}) \u2014 MC: ${mcap} | Replies: ${c.reply_count}`
|
|
5519
|
+
);
|
|
4869
5520
|
}
|
|
4870
5521
|
return lines.join("\n");
|
|
4871
5522
|
} catch {
|
|
@@ -5785,6 +6436,7 @@ var init_context_injector = __esm({
|
|
|
5785
6436
|
init_loader();
|
|
5786
6437
|
init_constants();
|
|
5787
6438
|
init_client();
|
|
6439
|
+
init_sanitize();
|
|
5788
6440
|
PRICE_KEYWORDS = ["price", "worth", "cost", "value", "how much"];
|
|
5789
6441
|
TRENDING_KEYWORDS = ["trending", "hot", "popular", "top", "best", "hype"];
|
|
5790
6442
|
NEWS_KEYWORDS = ["news", "latest", "update", "happening", "announcement"];
|
|
@@ -6524,7 +7176,12 @@ function getCached(key) {
|
|
|
6524
7176
|
getDb().prepare("DELETE FROM cache WHERE key = ?").run(key);
|
|
6525
7177
|
return null;
|
|
6526
7178
|
}
|
|
6527
|
-
|
|
7179
|
+
try {
|
|
7180
|
+
return JSON.parse(row.value);
|
|
7181
|
+
} catch {
|
|
7182
|
+
getDb().prepare("DELETE FROM cache WHERE key = ?").run(key);
|
|
7183
|
+
return null;
|
|
7184
|
+
}
|
|
6528
7185
|
}
|
|
6529
7186
|
function setCache(key, value, ttlSeconds) {
|
|
6530
7187
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -6556,12 +7213,14 @@ var init_engine = __esm({
|
|
|
6556
7213
|
logger = createLogger("agent-engine");
|
|
6557
7214
|
AgentEngine = class {
|
|
6558
7215
|
timer = null;
|
|
7216
|
+
running = false;
|
|
6559
7217
|
state;
|
|
6560
7218
|
strategy;
|
|
6561
7219
|
constructor(config2) {
|
|
6562
7220
|
this.strategy = getStrategy(config2.strategy);
|
|
7221
|
+
const interval = Math.max(30, config2.interval);
|
|
6563
7222
|
this.state = {
|
|
6564
|
-
config: config2,
|
|
7223
|
+
config: { ...config2, interval },
|
|
6565
7224
|
status: "idle",
|
|
6566
7225
|
lastCycle: null,
|
|
6567
7226
|
cycleCount: 0,
|
|
@@ -6575,25 +7234,35 @@ var init_engine = __esm({
|
|
|
6575
7234
|
if (this.state.status === "running") return;
|
|
6576
7235
|
this.state.status = "running";
|
|
6577
7236
|
this.state.error = null;
|
|
7237
|
+
this.running = true;
|
|
6578
7238
|
logger.info(`Agent ${this.state.config.name} started (${this.strategy.name})`);
|
|
6579
|
-
void this.
|
|
6580
|
-
this.timer = setInterval(() => {
|
|
6581
|
-
void this.runCycle();
|
|
6582
|
-
}, this.state.config.interval * 1e3);
|
|
7239
|
+
void this.scheduleNextCycle(true);
|
|
6583
7240
|
}
|
|
6584
7241
|
stop() {
|
|
7242
|
+
this.running = false;
|
|
6585
7243
|
if (this.timer) {
|
|
6586
|
-
|
|
7244
|
+
clearTimeout(this.timer);
|
|
6587
7245
|
this.timer = null;
|
|
6588
7246
|
}
|
|
6589
7247
|
this.state.status = "stopped";
|
|
6590
7248
|
logger.info(`Agent ${this.state.config.name} stopped`);
|
|
6591
7249
|
}
|
|
7250
|
+
async scheduleNextCycle(immediate = false) {
|
|
7251
|
+
if (!this.running) return;
|
|
7252
|
+
if (!immediate) {
|
|
7253
|
+
await new Promise((resolve4) => {
|
|
7254
|
+
this.timer = setTimeout(resolve4, this.state.config.interval * 1e3);
|
|
7255
|
+
});
|
|
7256
|
+
}
|
|
7257
|
+
if (!this.running) return;
|
|
7258
|
+
await this.runCycle();
|
|
7259
|
+
void this.scheduleNextCycle(false);
|
|
7260
|
+
}
|
|
6592
7261
|
async runCycle() {
|
|
6593
7262
|
for (const symbol of this.state.config.pairs) {
|
|
6594
7263
|
try {
|
|
6595
7264
|
const signals = await this.gatherSignals(symbol);
|
|
6596
|
-
const decision = this.strategy.evaluate(signals);
|
|
7265
|
+
const decision = this.strategy.evaluateAsync ? await this.strategy.evaluateAsync(signals, symbol) : this.strategy.evaluate(signals);
|
|
6597
7266
|
const result = {
|
|
6598
7267
|
agentId: this.state.config.id,
|
|
6599
7268
|
symbol,
|
|
@@ -6890,6 +7559,60 @@ function evaluateWithRules(signals) {
|
|
|
6890
7559
|
reasoning: [...reasoning, "Mixed signals \u2014 holding"]
|
|
6891
7560
|
};
|
|
6892
7561
|
}
|
|
7562
|
+
async function evaluateWithML(symbol, signals) {
|
|
7563
|
+
const mlClient2 = getMLClient();
|
|
7564
|
+
if (!mlClient2) return evaluateWithRules(signals);
|
|
7565
|
+
try {
|
|
7566
|
+
const regime = await detectMarketRegime(symbol, {
|
|
7567
|
+
returns_1d: signals.priceChange24h ?? 0,
|
|
7568
|
+
returns_7d: 0,
|
|
7569
|
+
volatility_14d: signals.atr !== null && signals.price !== null && signals.price > 0 ? signals.atr / signals.price * 100 : 3,
|
|
7570
|
+
volume_ratio: 1,
|
|
7571
|
+
rsi: signals.rsi ?? 50,
|
|
7572
|
+
bb_width: 0,
|
|
7573
|
+
fear_greed: signals.fearGreed ?? 50,
|
|
7574
|
+
funding_rate: signals.fundingRate ?? 0,
|
|
7575
|
+
price_vs_sma200: 0
|
|
7576
|
+
});
|
|
7577
|
+
const strategyResult = await mlClient2.evaluateStrategy({
|
|
7578
|
+
rsi: signals.rsi ?? 50,
|
|
7579
|
+
macd_histogram: signals.macdHistogram ?? 0,
|
|
7580
|
+
ema12: signals.ema12 ?? 0,
|
|
7581
|
+
ema26: signals.ema26 ?? 0,
|
|
7582
|
+
bollinger_pct_b: signals.bollingerPercentB ?? 0.5,
|
|
7583
|
+
atr: signals.atr ?? 0,
|
|
7584
|
+
obv: signals.obv ?? 0,
|
|
7585
|
+
funding_rate: signals.fundingRate ?? 0,
|
|
7586
|
+
fear_greed: signals.fearGreed ?? 50,
|
|
7587
|
+
price_change_24h: signals.priceChange24h ?? 0,
|
|
7588
|
+
price: signals.price ?? 0,
|
|
7589
|
+
regime: regime.regime
|
|
7590
|
+
});
|
|
7591
|
+
if (strategyResult && strategyResult.confidence > 60) {
|
|
7592
|
+
return {
|
|
7593
|
+
action: strategyResult.action,
|
|
7594
|
+
confidence: Math.round(strategyResult.confidence),
|
|
7595
|
+
reasoning: [
|
|
7596
|
+
`Regime: ${regime.regime} (${regime.confidence.toFixed(0)}% confidence)`,
|
|
7597
|
+
...strategyResult.reasoning
|
|
7598
|
+
]
|
|
7599
|
+
};
|
|
7600
|
+
}
|
|
7601
|
+
const ruleDecision = evaluateWithRules(signals);
|
|
7602
|
+
return {
|
|
7603
|
+
action: ruleDecision.action,
|
|
7604
|
+
confidence: Math.round(((strategyResult?.confidence ?? 0) + ruleDecision.confidence) / 2),
|
|
7605
|
+
reasoning: [
|
|
7606
|
+
`Regime: ${regime.regime}`,
|
|
7607
|
+
...strategyResult?.reasoning ?? [],
|
|
7608
|
+
"(Low ML confidence \u2014 blending with rules)",
|
|
7609
|
+
...ruleDecision.reasoning
|
|
7610
|
+
]
|
|
7611
|
+
};
|
|
7612
|
+
} catch {
|
|
7613
|
+
return evaluateWithRules(signals);
|
|
7614
|
+
}
|
|
7615
|
+
}
|
|
6893
7616
|
var mlAdaptiveStrategy;
|
|
6894
7617
|
var init_ml_adaptive = __esm({
|
|
6895
7618
|
"src/core/agent/strategies/ml-adaptive.ts"() {
|
|
@@ -6901,6 +7624,9 @@ var init_ml_adaptive = __esm({
|
|
|
6901
7624
|
description: "ML-powered strategy using contextual bandit approach. Uses LSTM/RF predictions when available, falls back to rule-based when < 100 training samples.",
|
|
6902
7625
|
evaluate(signals) {
|
|
6903
7626
|
return evaluateWithRules(signals);
|
|
7627
|
+
},
|
|
7628
|
+
async evaluateAsync(signals, symbol) {
|
|
7629
|
+
return evaluateWithML(symbol, signals);
|
|
6904
7630
|
}
|
|
6905
7631
|
};
|
|
6906
7632
|
}
|
|
@@ -6948,6 +7674,10 @@ function ensureAgentTables() {
|
|
|
6948
7674
|
function createAgent(name, strategy, pairs, interval = 60) {
|
|
6949
7675
|
ensureAgentTables();
|
|
6950
7676
|
getStrategy(strategy);
|
|
7677
|
+
const existing = getAgentByName(name);
|
|
7678
|
+
if (existing) {
|
|
7679
|
+
throw new Error(`Agent "${name}" already exists. Delete it first or choose a different name.`);
|
|
7680
|
+
}
|
|
6951
7681
|
const id = randomUUID();
|
|
6952
7682
|
const now = Date.now();
|
|
6953
7683
|
getDb().prepare(
|
|
@@ -7004,24 +7734,32 @@ function deleteAgent(id) {
|
|
|
7004
7734
|
engine.stop();
|
|
7005
7735
|
engines.delete(id);
|
|
7006
7736
|
}
|
|
7007
|
-
const result = getDb().prepare("DELETE FROM agents WHERE id = ?").run(id);
|
|
7008
7737
|
getDb().prepare("DELETE FROM agent_decisions WHERE agent_id = ?").run(id);
|
|
7738
|
+
const result = getDb().prepare("DELETE FROM agents WHERE id = ?").run(id);
|
|
7009
7739
|
return result.changes > 0;
|
|
7010
7740
|
}
|
|
7011
7741
|
function startAgent(id) {
|
|
7012
7742
|
const config2 = getAgentById(id);
|
|
7013
7743
|
if (!config2) throw new Error(`Agent not found: ${id}`);
|
|
7014
|
-
|
|
7015
|
-
if (
|
|
7016
|
-
|
|
7017
|
-
|
|
7744
|
+
const existing = engines.get(id);
|
|
7745
|
+
if (existing) {
|
|
7746
|
+
const state = existing.getState();
|
|
7747
|
+
if (state.status === "running") {
|
|
7748
|
+
return state;
|
|
7749
|
+
}
|
|
7018
7750
|
}
|
|
7751
|
+
const engine = new AgentEngine(config2);
|
|
7752
|
+
engines.set(id, engine);
|
|
7019
7753
|
engine.start();
|
|
7020
7754
|
return engine.getState();
|
|
7021
7755
|
}
|
|
7022
7756
|
function stopAgent(id) {
|
|
7023
7757
|
const engine = engines.get(id);
|
|
7024
|
-
if (!engine)
|
|
7758
|
+
if (!engine) {
|
|
7759
|
+
const config2 = getAgentById(id);
|
|
7760
|
+
if (!config2) throw new Error(`Agent not found: ${id}`);
|
|
7761
|
+
return { config: config2, status: "idle", lastCycle: null, cycleCount: 0, error: null };
|
|
7762
|
+
}
|
|
7025
7763
|
engine.stop();
|
|
7026
7764
|
return engine.getState();
|
|
7027
7765
|
}
|
|
@@ -7052,23 +7790,43 @@ function logDecision(result) {
|
|
|
7052
7790
|
JSON.stringify(result.signals),
|
|
7053
7791
|
result.timestamp
|
|
7054
7792
|
);
|
|
7793
|
+
pruneCounter++;
|
|
7794
|
+
if (pruneCounter >= 100) {
|
|
7795
|
+
pruneCounter = 0;
|
|
7796
|
+
pruneDecisions(result.agentId, 1e3);
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7799
|
+
function pruneDecisions(agentId, keep = 1e3) {
|
|
7800
|
+
ensureAgentTables();
|
|
7801
|
+
const result = getDb().prepare(
|
|
7802
|
+
`DELETE FROM agent_decisions WHERE agent_id = ? AND id NOT IN (
|
|
7803
|
+
SELECT id FROM agent_decisions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?
|
|
7804
|
+
)`
|
|
7805
|
+
).run(agentId, agentId, keep);
|
|
7806
|
+
return result.changes;
|
|
7055
7807
|
}
|
|
7056
7808
|
function getRecentDecisions(agentId, limit = 20) {
|
|
7057
7809
|
ensureAgentTables();
|
|
7058
7810
|
const rows = getDb().prepare(`SELECT * FROM agent_decisions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`).all(agentId, limit);
|
|
7059
|
-
return rows.map((r) =>
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7811
|
+
return rows.map((r) => {
|
|
7812
|
+
try {
|
|
7813
|
+
return {
|
|
7814
|
+
agentId: r.agent_id,
|
|
7815
|
+
symbol: r.symbol,
|
|
7816
|
+
timestamp: r.created_at,
|
|
7817
|
+
signals: JSON.parse(r.signals),
|
|
7818
|
+
decision: {
|
|
7819
|
+
action: r.action,
|
|
7820
|
+
confidence: r.confidence,
|
|
7821
|
+
reasoning: JSON.parse(r.reasoning)
|
|
7822
|
+
}
|
|
7823
|
+
};
|
|
7824
|
+
} catch {
|
|
7825
|
+
return null;
|
|
7068
7826
|
}
|
|
7069
|
-
}));
|
|
7827
|
+
}).filter((r) => r !== null);
|
|
7070
7828
|
}
|
|
7071
|
-
var STRATEGIES, engines;
|
|
7829
|
+
var STRATEGIES, engines, pruneCounter;
|
|
7072
7830
|
var init_manager = __esm({
|
|
7073
7831
|
"src/core/agent/manager.ts"() {
|
|
7074
7832
|
"use strict";
|
|
@@ -7083,12 +7841,13 @@ var init_manager = __esm({
|
|
|
7083
7841
|
"ml-adaptive": mlAdaptiveStrategy
|
|
7084
7842
|
};
|
|
7085
7843
|
engines = /* @__PURE__ */ new Map();
|
|
7844
|
+
pruneCounter = 0;
|
|
7086
7845
|
}
|
|
7087
7846
|
});
|
|
7088
7847
|
|
|
7089
7848
|
// src/core/agent/wallet.ts
|
|
7090
|
-
import { createPublicClient as createPublicClient2, http as
|
|
7091
|
-
import { mainnet as
|
|
7849
|
+
import { createPublicClient as createPublicClient2, http as http3, formatEther } from "viem";
|
|
7850
|
+
import { mainnet as mainnet3 } from "viem/chains";
|
|
7092
7851
|
async function getWalletBalance(address) {
|
|
7093
7852
|
const balance = await client.getBalance({ address });
|
|
7094
7853
|
return formatEther(balance);
|
|
@@ -7101,8 +7860,8 @@ var init_wallet = __esm({
|
|
|
7101
7860
|
"src/core/agent/wallet.ts"() {
|
|
7102
7861
|
"use strict";
|
|
7103
7862
|
client = createPublicClient2({
|
|
7104
|
-
chain:
|
|
7105
|
-
transport:
|
|
7863
|
+
chain: mainnet3,
|
|
7864
|
+
transport: http3()
|
|
7106
7865
|
});
|
|
7107
7866
|
}
|
|
7108
7867
|
});
|
|
@@ -7547,8 +8306,176 @@ var init_store_factory = __esm({
|
|
|
7547
8306
|
}
|
|
7548
8307
|
});
|
|
7549
8308
|
|
|
8309
|
+
// src/core/backtest/engine.ts
|
|
8310
|
+
var engine_exports = {};
|
|
8311
|
+
__export(engine_exports, {
|
|
8312
|
+
BacktestEngine: () => BacktestEngine
|
|
8313
|
+
});
|
|
8314
|
+
var log7, BacktestEngine;
|
|
8315
|
+
var init_engine2 = __esm({
|
|
8316
|
+
"src/core/backtest/engine.ts"() {
|
|
8317
|
+
"use strict";
|
|
8318
|
+
init_binance();
|
|
8319
|
+
init_manager();
|
|
8320
|
+
init_logger();
|
|
8321
|
+
log7 = createLogger("backtest");
|
|
8322
|
+
BacktestEngine = class {
|
|
8323
|
+
config;
|
|
8324
|
+
constructor(config2) {
|
|
8325
|
+
this.config = config2;
|
|
8326
|
+
}
|
|
8327
|
+
async run() {
|
|
8328
|
+
log7.info(
|
|
8329
|
+
`Backtest: ${this.config.strategy} on ${this.config.pair} (${this.config.from} \u2192 ${this.config.to})`
|
|
8330
|
+
);
|
|
8331
|
+
const strategy = getStrategy(this.config.strategy);
|
|
8332
|
+
const klines = await fetchKlines(this.config.pair, this.config.timeframe, 1e3);
|
|
8333
|
+
const fromTs = new Date(this.config.from).getTime();
|
|
8334
|
+
const toTs = new Date(this.config.to).getTime();
|
|
8335
|
+
const filtered = klines.filter((k) => k.openTime >= fromTs && k.openTime <= toTs);
|
|
8336
|
+
if (filtered.length === 0) {
|
|
8337
|
+
return this.emptyResult();
|
|
8338
|
+
}
|
|
8339
|
+
const trades = [];
|
|
8340
|
+
const equityCurve = [];
|
|
8341
|
+
const drawdownCurve = [];
|
|
8342
|
+
let cash = this.config.initialCapital;
|
|
8343
|
+
let position = null;
|
|
8344
|
+
let peak = cash;
|
|
8345
|
+
for (const candle of filtered) {
|
|
8346
|
+
const price = candle.close;
|
|
8347
|
+
const equity = position ? cash + position.size * price : cash;
|
|
8348
|
+
if (equity > peak) peak = equity;
|
|
8349
|
+
const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0;
|
|
8350
|
+
equityCurve.push({ time: candle.openTime, equity });
|
|
8351
|
+
drawdownCurve.push({ time: candle.openTime, drawdown });
|
|
8352
|
+
const signals = {
|
|
8353
|
+
rsi: null,
|
|
8354
|
+
macdHistogram: null,
|
|
8355
|
+
ema12: null,
|
|
8356
|
+
ema26: null,
|
|
8357
|
+
bollingerPercentB: null,
|
|
8358
|
+
atr: null,
|
|
8359
|
+
obv: null,
|
|
8360
|
+
fundingRate: null,
|
|
8361
|
+
fearGreed: null,
|
|
8362
|
+
priceChange24h: null,
|
|
8363
|
+
price
|
|
8364
|
+
};
|
|
8365
|
+
const decision = strategy.evaluate(signals);
|
|
8366
|
+
if (decision.action === "buy" && !position && cash > 0) {
|
|
8367
|
+
const size = cash * 0.95 / price;
|
|
8368
|
+
const commission = cash * 0.95 * (this.config.commissionPct / 100);
|
|
8369
|
+
cash -= size * price + commission;
|
|
8370
|
+
position = { side: "long", entryPrice: price, entryTime: candle.openTime, size };
|
|
8371
|
+
} else if (decision.action === "sell" && position) {
|
|
8372
|
+
const exitValue = position.size * price;
|
|
8373
|
+
const commission = exitValue * (this.config.commissionPct / 100);
|
|
8374
|
+
const pnl = exitValue - position.size * position.entryPrice - commission;
|
|
8375
|
+
const pnlPct = pnl / (position.size * position.entryPrice) * 100;
|
|
8376
|
+
trades.push({
|
|
8377
|
+
entryTime: position.entryTime,
|
|
8378
|
+
exitTime: candle.openTime,
|
|
8379
|
+
side: position.side,
|
|
8380
|
+
entryPrice: position.entryPrice,
|
|
8381
|
+
exitPrice: price,
|
|
8382
|
+
size: position.size,
|
|
8383
|
+
pnl,
|
|
8384
|
+
pnlPct,
|
|
8385
|
+
reason: decision.reasoning.join("; ")
|
|
8386
|
+
});
|
|
8387
|
+
cash += exitValue - commission;
|
|
8388
|
+
position = null;
|
|
8389
|
+
}
|
|
8390
|
+
}
|
|
8391
|
+
if (position && filtered.length > 0) {
|
|
8392
|
+
const lastPrice = filtered[filtered.length - 1].close;
|
|
8393
|
+
const exitValue = position.size * lastPrice;
|
|
8394
|
+
const pnl = exitValue - position.size * position.entryPrice;
|
|
8395
|
+
trades.push({
|
|
8396
|
+
entryTime: position.entryTime,
|
|
8397
|
+
exitTime: filtered[filtered.length - 1].openTime,
|
|
8398
|
+
side: position.side,
|
|
8399
|
+
entryPrice: position.entryPrice,
|
|
8400
|
+
exitPrice: lastPrice,
|
|
8401
|
+
size: position.size,
|
|
8402
|
+
pnl,
|
|
8403
|
+
pnlPct: pnl / (position.size * position.entryPrice) * 100,
|
|
8404
|
+
reason: "End of backtest period"
|
|
8405
|
+
});
|
|
8406
|
+
cash += exitValue;
|
|
8407
|
+
}
|
|
8408
|
+
const metrics = this.calculateMetrics(trades, cash);
|
|
8409
|
+
return {
|
|
8410
|
+
config: this.config,
|
|
8411
|
+
trades,
|
|
8412
|
+
metrics,
|
|
8413
|
+
equityCurve,
|
|
8414
|
+
drawdownCurve
|
|
8415
|
+
};
|
|
8416
|
+
}
|
|
8417
|
+
calculateMetrics(trades, finalCash) {
|
|
8418
|
+
const wins = trades.filter((t) => t.pnl > 0);
|
|
8419
|
+
const losses = trades.filter((t) => t.pnl <= 0);
|
|
8420
|
+
const totalReturn = finalCash - this.config.initialCapital;
|
|
8421
|
+
const totalReturnPct = totalReturn / this.config.initialCapital * 100;
|
|
8422
|
+
const winRate = trades.length > 0 ? wins.length / trades.length : 0;
|
|
8423
|
+
const grossProfit = wins.reduce((sum, t) => sum + t.pnl, 0);
|
|
8424
|
+
const grossLoss = Math.abs(losses.reduce((sum, t) => sum + t.pnl, 0));
|
|
8425
|
+
const profitFactor = grossLoss > 0 ? grossProfit / grossLoss : grossProfit > 0 ? Infinity : 0;
|
|
8426
|
+
const returns = trades.map((t) => t.pnlPct / 100);
|
|
8427
|
+
const avgReturn = returns.length > 0 ? returns.reduce((a, b) => a + b, 0) / returns.length : 0;
|
|
8428
|
+
const variance = returns.length > 1 ? returns.reduce((s, r) => s + (r - avgReturn) ** 2, 0) / (returns.length - 1) : 0;
|
|
8429
|
+
const sharpeRatio = Math.sqrt(variance) > 0 ? avgReturn / Math.sqrt(variance) * Math.sqrt(365) : 0;
|
|
8430
|
+
let maxDrawdown = 0;
|
|
8431
|
+
let peak = this.config.initialCapital;
|
|
8432
|
+
let running = this.config.initialCapital;
|
|
8433
|
+
for (const t of trades) {
|
|
8434
|
+
running += t.pnl;
|
|
8435
|
+
if (running > peak) peak = running;
|
|
8436
|
+
const dd = peak > 0 ? (peak - running) / peak * 100 : 0;
|
|
8437
|
+
if (dd > maxDrawdown) maxDrawdown = dd;
|
|
8438
|
+
}
|
|
8439
|
+
const avgHoldingPeriodMs = trades.length > 0 ? trades.reduce((sum, t) => sum + (t.exitTime - t.entryTime), 0) / trades.length : 0;
|
|
8440
|
+
return {
|
|
8441
|
+
totalReturn,
|
|
8442
|
+
totalReturnPct,
|
|
8443
|
+
winRate,
|
|
8444
|
+
totalTrades: trades.length,
|
|
8445
|
+
profitFactor,
|
|
8446
|
+
sharpeRatio,
|
|
8447
|
+
maxDrawdown,
|
|
8448
|
+
avgHoldingPeriodMs
|
|
8449
|
+
};
|
|
8450
|
+
}
|
|
8451
|
+
emptyResult() {
|
|
8452
|
+
return {
|
|
8453
|
+
config: this.config,
|
|
8454
|
+
trades: [],
|
|
8455
|
+
metrics: {
|
|
8456
|
+
totalReturn: 0,
|
|
8457
|
+
totalReturnPct: 0,
|
|
8458
|
+
winRate: 0,
|
|
8459
|
+
totalTrades: 0,
|
|
8460
|
+
profitFactor: 0,
|
|
8461
|
+
sharpeRatio: 0,
|
|
8462
|
+
maxDrawdown: 0,
|
|
8463
|
+
avgHoldingPeriodMs: 0
|
|
8464
|
+
},
|
|
8465
|
+
equityCurve: [],
|
|
8466
|
+
drawdownCurve: []
|
|
8467
|
+
};
|
|
8468
|
+
}
|
|
8469
|
+
};
|
|
8470
|
+
}
|
|
8471
|
+
});
|
|
8472
|
+
|
|
7550
8473
|
// src/ai/tool-handler.ts
|
|
7551
8474
|
async function handleTool(name, input) {
|
|
8475
|
+
const raw = await handleToolUnsafe(name, input);
|
|
8476
|
+
return sanitizeToolResult(raw);
|
|
8477
|
+
}
|
|
8478
|
+
async function handleToolUnsafe(name, input) {
|
|
7552
8479
|
const params = input;
|
|
7553
8480
|
switch (name) {
|
|
7554
8481
|
case "get_token_info": {
|
|
@@ -8220,6 +9147,92 @@ async function handleTool(name, input) {
|
|
|
8220
9147
|
}))
|
|
8221
9148
|
};
|
|
8222
9149
|
}
|
|
9150
|
+
case "get_ml_model_health": {
|
|
9151
|
+
let mlClient2 = getMLClient();
|
|
9152
|
+
if (!mlClient2) {
|
|
9153
|
+
try {
|
|
9154
|
+
const cfg = getConfig();
|
|
9155
|
+
if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
|
|
9156
|
+
mlClient2 = initMLClient(cfg.ml.sidecarUrl);
|
|
9157
|
+
}
|
|
9158
|
+
} catch {
|
|
9159
|
+
}
|
|
9160
|
+
}
|
|
9161
|
+
if (!mlClient2) {
|
|
9162
|
+
return { error: "ML sidecar not configured" };
|
|
9163
|
+
}
|
|
9164
|
+
const health = await mlClient2.getModelHealth();
|
|
9165
|
+
if (!health) {
|
|
9166
|
+
return { error: "ML sidecar not responding" };
|
|
9167
|
+
}
|
|
9168
|
+
return {
|
|
9169
|
+
models: health.models.map((m) => ({
|
|
9170
|
+
name: m.name,
|
|
9171
|
+
version: m.version,
|
|
9172
|
+
loaded: m.loaded,
|
|
9173
|
+
lastTrained: m.lastTrained,
|
|
9174
|
+
accuracy: m.accuracy
|
|
9175
|
+
})),
|
|
9176
|
+
uptime: health.uptime,
|
|
9177
|
+
predictionsServed: health.predictionsServed
|
|
9178
|
+
};
|
|
9179
|
+
}
|
|
9180
|
+
case "classify_user_intent": {
|
|
9181
|
+
const text = String(params["text"] ?? "");
|
|
9182
|
+
let mlClient2 = getMLClient();
|
|
9183
|
+
if (!mlClient2) {
|
|
9184
|
+
try {
|
|
9185
|
+
const cfg = getConfig();
|
|
9186
|
+
if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
|
|
9187
|
+
mlClient2 = initMLClient(cfg.ml.sidecarUrl);
|
|
9188
|
+
}
|
|
9189
|
+
} catch {
|
|
9190
|
+
}
|
|
9191
|
+
}
|
|
9192
|
+
if (!mlClient2) {
|
|
9193
|
+
return { error: "ML sidecar not configured for intent classification" };
|
|
9194
|
+
}
|
|
9195
|
+
const intent = await mlClient2.classifyIntent(text);
|
|
9196
|
+
if (!intent) {
|
|
9197
|
+
return { error: "Intent classification failed" };
|
|
9198
|
+
}
|
|
9199
|
+
return {
|
|
9200
|
+
intent: intent.intent,
|
|
9201
|
+
confidence: intent.confidence,
|
|
9202
|
+
secondaryIntent: intent.secondary_intent,
|
|
9203
|
+
detectedTokens: intent.detected_tokens,
|
|
9204
|
+
detectedAddresses: intent.detected_addresses,
|
|
9205
|
+
model: intent.model
|
|
9206
|
+
};
|
|
9207
|
+
}
|
|
9208
|
+
case "run_backtest": {
|
|
9209
|
+
const { BacktestEngine: BacktestEngine2 } = await Promise.resolve().then(() => (init_engine2(), engine_exports));
|
|
9210
|
+
const engine = new BacktestEngine2({
|
|
9211
|
+
strategy: String(params["strategy"] ?? "momentum"),
|
|
9212
|
+
pair: String(params["pair"] ?? "BTCUSDT"),
|
|
9213
|
+
from: String(params["from"] ?? ""),
|
|
9214
|
+
to: String(params["to"] ?? ""),
|
|
9215
|
+
initialCapital: 1e4,
|
|
9216
|
+
timeframe: String(params["timeframe"] ?? "4h"),
|
|
9217
|
+
slippageBps: 10,
|
|
9218
|
+
commissionPct: 0.1
|
|
9219
|
+
});
|
|
9220
|
+
const result = await engine.run();
|
|
9221
|
+
return {
|
|
9222
|
+
strategy: result.config.strategy,
|
|
9223
|
+
pair: result.config.pair,
|
|
9224
|
+
period: `${result.config.from} \u2192 ${result.config.to}`,
|
|
9225
|
+
metrics: result.metrics,
|
|
9226
|
+
tradeCount: result.trades.length,
|
|
9227
|
+
lastTrades: result.trades.slice(-5).map((t) => ({
|
|
9228
|
+
entry: t.entryPrice,
|
|
9229
|
+
exit: t.exitPrice,
|
|
9230
|
+
pnl: t.pnl,
|
|
9231
|
+
pnlPct: t.pnlPct,
|
|
9232
|
+
side: t.side
|
|
9233
|
+
}))
|
|
9234
|
+
};
|
|
9235
|
+
}
|
|
8223
9236
|
default:
|
|
8224
9237
|
return { error: `Unknown tool: ${name}` };
|
|
8225
9238
|
}
|
|
@@ -8248,6 +9261,7 @@ var init_tool_handler = __esm({
|
|
|
8248
9261
|
init_client();
|
|
8249
9262
|
init_feature_engineer();
|
|
8250
9263
|
init_store_factory();
|
|
9264
|
+
init_sanitize();
|
|
8251
9265
|
}
|
|
8252
9266
|
});
|
|
8253
9267
|
|
|
@@ -8651,6 +9665,59 @@ var init_tools = __esm({
|
|
|
8651
9665
|
required: ["agentName"]
|
|
8652
9666
|
}
|
|
8653
9667
|
},
|
|
9668
|
+
{
|
|
9669
|
+
name: "get_ml_model_health",
|
|
9670
|
+
description: "Get health status of all ML models in the sidecar including loaded status, version, last training date, accuracy, uptime, and total predictions served.",
|
|
9671
|
+
input_schema: {
|
|
9672
|
+
type: "object",
|
|
9673
|
+
properties: {},
|
|
9674
|
+
required: []
|
|
9675
|
+
}
|
|
9676
|
+
},
|
|
9677
|
+
{
|
|
9678
|
+
name: "classify_user_intent",
|
|
9679
|
+
description: "Classify a user message into an intent category (e.g. price_check, token_analysis, prediction, news, portfolio, agent_management) using ML NLP. Returns intent, confidence, detected tokens, and detected addresses.",
|
|
9680
|
+
input_schema: {
|
|
9681
|
+
type: "object",
|
|
9682
|
+
properties: {
|
|
9683
|
+
text: {
|
|
9684
|
+
type: "string",
|
|
9685
|
+
description: "The user message text to classify."
|
|
9686
|
+
}
|
|
9687
|
+
},
|
|
9688
|
+
required: ["text"]
|
|
9689
|
+
}
|
|
9690
|
+
},
|
|
9691
|
+
{
|
|
9692
|
+
name: "run_backtest",
|
|
9693
|
+
description: "Run a historical backtest for a trading strategy. Simulates strategy execution on historical kline data and returns trades, metrics (return, win rate, Sharpe, drawdown), and equity curve.",
|
|
9694
|
+
input_schema: {
|
|
9695
|
+
type: "object",
|
|
9696
|
+
properties: {
|
|
9697
|
+
strategy: {
|
|
9698
|
+
type: "string",
|
|
9699
|
+
description: 'Strategy name: "momentum", "trend-following", or "ml-adaptive".'
|
|
9700
|
+
},
|
|
9701
|
+
pair: {
|
|
9702
|
+
type: "string",
|
|
9703
|
+
description: 'Trading pair (e.g. "BTCUSDT").'
|
|
9704
|
+
},
|
|
9705
|
+
from: {
|
|
9706
|
+
type: "string",
|
|
9707
|
+
description: "Start date (YYYY-MM-DD)."
|
|
9708
|
+
},
|
|
9709
|
+
to: {
|
|
9710
|
+
type: "string",
|
|
9711
|
+
description: "End date (YYYY-MM-DD)."
|
|
9712
|
+
},
|
|
9713
|
+
timeframe: {
|
|
9714
|
+
type: "string",
|
|
9715
|
+
description: 'Candle timeframe: "1h", "4h", "1d". Defaults to "4h".'
|
|
9716
|
+
}
|
|
9717
|
+
},
|
|
9718
|
+
required: ["strategy", "pair", "from", "to"]
|
|
9719
|
+
}
|
|
9720
|
+
},
|
|
8654
9721
|
{
|
|
8655
9722
|
name: "create_agent",
|
|
8656
9723
|
description: "Create an autonomous trading agent that monitors crypto pairs using a strategy (momentum or trend-following). Returns the created agent config.",
|
|
@@ -9288,13 +10355,16 @@ async function startDiscordBot() {
|
|
|
9288
10355
|
client2.on("messageCreate", async (message) => {
|
|
9289
10356
|
if (message.author.bot) return;
|
|
9290
10357
|
if (!client2.user || !message.mentions.has(client2.user)) return;
|
|
9291
|
-
|
|
10358
|
+
let text = message.content.replace(/<@!?\d+>/g, "").trim();
|
|
9292
10359
|
if (!text) {
|
|
9293
10360
|
await message.reply(
|
|
9294
10361
|
"Mention me with a question! e.g. `@Vizzor what is BTC price?`\nOr use slash commands: `/scan` `/trends` `/track` `/ico` `/audit` `/help`"
|
|
9295
10362
|
);
|
|
9296
10363
|
return;
|
|
9297
10364
|
}
|
|
10365
|
+
if (text.length > 2e3) {
|
|
10366
|
+
text = text.slice(0, 2e3);
|
|
10367
|
+
}
|
|
9298
10368
|
await message.reply("\u{1F52E} Analyzing...");
|
|
9299
10369
|
try {
|
|
9300
10370
|
const response = await analyze(buildChatSystemPrompt(), text, VIZZOR_TOOLS);
|
|
@@ -9964,8 +11034,11 @@ async function startTelegramBot() {
|
|
|
9964
11034
|
bot.use(rateLimitMiddleware);
|
|
9965
11035
|
registerCommands(bot);
|
|
9966
11036
|
bot.on("message:text", async (ctx) => {
|
|
9967
|
-
|
|
11037
|
+
let text = ctx.message.text;
|
|
9968
11038
|
if (text.startsWith("/")) return;
|
|
11039
|
+
if (text.length > 4e3) {
|
|
11040
|
+
text = text.slice(0, 4e3);
|
|
11041
|
+
}
|
|
9969
11042
|
await ctx.reply("\u{1F52E} Analyzing...");
|
|
9970
11043
|
try {
|
|
9971
11044
|
const response = await analyze(buildChatSystemPrompt(), text, VIZZOR_TOOLS);
|
|
@@ -10095,13 +11168,13 @@ var init_bot3 = __esm({
|
|
|
10095
11168
|
});
|
|
10096
11169
|
|
|
10097
11170
|
// src/data/collector.ts
|
|
10098
|
-
var
|
|
11171
|
+
var log8, MAJOR_SYMBOLS, TIMEFRAMES, COLLECTION_INTERVAL_MS, DataCollector;
|
|
10099
11172
|
var init_collector = __esm({
|
|
10100
11173
|
"src/data/collector.ts"() {
|
|
10101
11174
|
"use strict";
|
|
10102
11175
|
init_binance();
|
|
10103
11176
|
init_logger();
|
|
10104
|
-
|
|
11177
|
+
log8 = createLogger("collector");
|
|
10105
11178
|
MAJOR_SYMBOLS = [
|
|
10106
11179
|
"BTC",
|
|
10107
11180
|
"ETH",
|
|
@@ -10152,10 +11225,10 @@ var init_collector = __esm({
|
|
|
10152
11225
|
}
|
|
10153
11226
|
start() {
|
|
10154
11227
|
if (this.status.running) {
|
|
10155
|
-
|
|
11228
|
+
log8.warn("Collector already running");
|
|
10156
11229
|
return;
|
|
10157
11230
|
}
|
|
10158
|
-
|
|
11231
|
+
log8.info(
|
|
10159
11232
|
`Starting data collector: ${this.symbols.length} symbols, ${TIMEFRAMES.length} timeframes, every ${this.intervalMs / 1e3}s`
|
|
10160
11233
|
);
|
|
10161
11234
|
this.status.running = true;
|
|
@@ -10168,10 +11241,10 @@ var init_collector = __esm({
|
|
|
10168
11241
|
this.timer = null;
|
|
10169
11242
|
}
|
|
10170
11243
|
this.status.running = false;
|
|
10171
|
-
|
|
11244
|
+
log8.info("Data collector stopped");
|
|
10172
11245
|
}
|
|
10173
11246
|
async collectAll() {
|
|
10174
|
-
|
|
11247
|
+
log8.info("Collection cycle starting");
|
|
10175
11248
|
const start = Date.now();
|
|
10176
11249
|
for (const timeframe of TIMEFRAMES) {
|
|
10177
11250
|
for (const symbol of this.symbols) {
|
|
@@ -10179,7 +11252,7 @@ var init_collector = __esm({
|
|
|
10179
11252
|
await this.collectSymbol(symbol, timeframe);
|
|
10180
11253
|
} catch (err) {
|
|
10181
11254
|
this.status.errors++;
|
|
10182
|
-
|
|
11255
|
+
log8.error(
|
|
10183
11256
|
`Failed to collect ${symbol}/${timeframe}: ${err instanceof Error ? err.message : String(err)}`
|
|
10184
11257
|
);
|
|
10185
11258
|
}
|
|
@@ -10187,7 +11260,7 @@ var init_collector = __esm({
|
|
|
10187
11260
|
}
|
|
10188
11261
|
this.status.lastRun = Date.now();
|
|
10189
11262
|
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
10190
|
-
|
|
11263
|
+
log8.info(
|
|
10191
11264
|
`Collection cycle complete in ${elapsed}s (total records: ${this.status.totalRecords})`
|
|
10192
11265
|
);
|
|
10193
11266
|
}
|
|
@@ -10296,42 +11369,171 @@ async function registerMarketRoutes(server) {
|
|
|
10296
11369
|
return handleTool("get_trending", {});
|
|
10297
11370
|
}
|
|
10298
11371
|
});
|
|
10299
|
-
server.get("/fear-greed", {
|
|
11372
|
+
server.get("/fear-greed", {
|
|
11373
|
+
schema: {
|
|
11374
|
+
tags: ["Market"],
|
|
11375
|
+
summary: "Get Crypto Fear & Greed Index with history"
|
|
11376
|
+
},
|
|
11377
|
+
handler: async () => {
|
|
11378
|
+
return handleTool("get_fear_greed", {});
|
|
11379
|
+
}
|
|
11380
|
+
});
|
|
11381
|
+
server.get("/news", {
|
|
11382
|
+
schema: {
|
|
11383
|
+
tags: ["Market"],
|
|
11384
|
+
summary: "Get latest crypto news with sentiment",
|
|
11385
|
+
querystring: {
|
|
11386
|
+
type: "object",
|
|
11387
|
+
properties: { symbol: { type: "string" } }
|
|
11388
|
+
}
|
|
11389
|
+
},
|
|
11390
|
+
handler: async (request) => {
|
|
11391
|
+
try {
|
|
11392
|
+
const { symbol } = request.query;
|
|
11393
|
+
return await handleTool("get_crypto_news", { symbol });
|
|
11394
|
+
} catch {
|
|
11395
|
+
return { news: [] };
|
|
11396
|
+
}
|
|
11397
|
+
}
|
|
11398
|
+
});
|
|
11399
|
+
server.get("/prediction", {
|
|
11400
|
+
schema: {
|
|
11401
|
+
tags: ["Market"],
|
|
11402
|
+
summary: "Get AI prediction for a symbol",
|
|
11403
|
+
querystring: {
|
|
11404
|
+
type: "object",
|
|
11405
|
+
properties: { symbol: { type: "string" } },
|
|
11406
|
+
required: ["symbol"]
|
|
11407
|
+
}
|
|
11408
|
+
},
|
|
11409
|
+
handler: async (request) => {
|
|
11410
|
+
const { symbol } = request.query;
|
|
11411
|
+
return handleTool("get_prediction", { symbol });
|
|
11412
|
+
}
|
|
11413
|
+
});
|
|
11414
|
+
server.get("/dex/search", {
|
|
11415
|
+
schema: {
|
|
11416
|
+
tags: ["Market"],
|
|
11417
|
+
summary: "Search tokens on decentralized exchanges",
|
|
11418
|
+
querystring: {
|
|
11419
|
+
type: "object",
|
|
11420
|
+
properties: { q: { type: "string" } },
|
|
11421
|
+
required: ["q"]
|
|
11422
|
+
}
|
|
11423
|
+
},
|
|
11424
|
+
handler: async (request) => {
|
|
11425
|
+
const { q } = request.query;
|
|
11426
|
+
return handleTool("search_token_dex", { query: q });
|
|
11427
|
+
}
|
|
11428
|
+
});
|
|
11429
|
+
server.get("/ml-health", {
|
|
11430
|
+
schema: {
|
|
11431
|
+
tags: ["Market"],
|
|
11432
|
+
summary: "Get ML sidecar health and model status"
|
|
11433
|
+
},
|
|
11434
|
+
handler: async () => {
|
|
11435
|
+
let mlClient2 = getMLClient();
|
|
11436
|
+
if (!mlClient2) {
|
|
11437
|
+
const cfg = loadConfig();
|
|
11438
|
+
if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
|
|
11439
|
+
mlClient2 = initMLClient(cfg.ml.sidecarUrl);
|
|
11440
|
+
}
|
|
11441
|
+
}
|
|
11442
|
+
if (!mlClient2) {
|
|
11443
|
+
return { available: false, models: [], uptime: 0, predictionsServed: 0 };
|
|
11444
|
+
}
|
|
11445
|
+
const health = await mlClient2.getModelHealth();
|
|
11446
|
+
if (!health) {
|
|
11447
|
+
return { available: false, models: [], uptime: 0, predictionsServed: 0 };
|
|
11448
|
+
}
|
|
11449
|
+
return { available: true, ...health };
|
|
11450
|
+
}
|
|
11451
|
+
});
|
|
11452
|
+
server.post("/ml/sentiment", {
|
|
10300
11453
|
schema: {
|
|
10301
11454
|
tags: ["Market"],
|
|
10302
|
-
summary: "
|
|
11455
|
+
summary: "Analyze text sentiment via ML",
|
|
11456
|
+
body: {
|
|
11457
|
+
type: "object",
|
|
11458
|
+
properties: { text: { type: "string" } },
|
|
11459
|
+
required: ["text"]
|
|
11460
|
+
}
|
|
10303
11461
|
},
|
|
10304
|
-
handler: async () => {
|
|
10305
|
-
|
|
11462
|
+
handler: async (request) => {
|
|
11463
|
+
const { text } = request.body;
|
|
11464
|
+
let mlClient2 = getMLClient();
|
|
11465
|
+
if (!mlClient2) {
|
|
11466
|
+
const cfg = loadConfig();
|
|
11467
|
+
if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
|
|
11468
|
+
mlClient2 = initMLClient(cfg.ml.sidecarUrl);
|
|
11469
|
+
}
|
|
11470
|
+
}
|
|
11471
|
+
if (!mlClient2) return { error: "ML sidecar unavailable" };
|
|
11472
|
+
const result = await mlClient2.analyzeSentiment(text);
|
|
11473
|
+
return result ?? { error: "Sentiment analysis failed" };
|
|
10306
11474
|
}
|
|
10307
11475
|
});
|
|
10308
|
-
server.
|
|
11476
|
+
server.post("/ml/regime", {
|
|
10309
11477
|
schema: {
|
|
10310
11478
|
tags: ["Market"],
|
|
10311
|
-
summary: "
|
|
10312
|
-
|
|
11479
|
+
summary: "Detect market regime via ML",
|
|
11480
|
+
body: {
|
|
10313
11481
|
type: "object",
|
|
10314
|
-
properties: {
|
|
11482
|
+
properties: {
|
|
11483
|
+
returns_1d: { type: "number" },
|
|
11484
|
+
returns_7d: { type: "number" },
|
|
11485
|
+
volatility_14d: { type: "number" },
|
|
11486
|
+
volume_ratio: { type: "number" },
|
|
11487
|
+
rsi: { type: "number" },
|
|
11488
|
+
bb_width: { type: "number" },
|
|
11489
|
+
fear_greed: { type: "number" },
|
|
11490
|
+
funding_rate: { type: "number" },
|
|
11491
|
+
price_vs_sma200: { type: "number" }
|
|
11492
|
+
}
|
|
10315
11493
|
}
|
|
10316
11494
|
},
|
|
10317
11495
|
handler: async (request) => {
|
|
10318
|
-
const
|
|
10319
|
-
|
|
11496
|
+
const features = request.body;
|
|
11497
|
+
let mlClient2 = getMLClient();
|
|
11498
|
+
if (!mlClient2) {
|
|
11499
|
+
const cfg = loadConfig();
|
|
11500
|
+
if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
|
|
11501
|
+
mlClient2 = initMLClient(cfg.ml.sidecarUrl);
|
|
11502
|
+
}
|
|
11503
|
+
}
|
|
11504
|
+
if (!mlClient2) return { error: "ML sidecar unavailable" };
|
|
11505
|
+
const result = await mlClient2.detectRegime(features);
|
|
11506
|
+
return result ?? { error: "Regime detection failed" };
|
|
10320
11507
|
}
|
|
10321
11508
|
});
|
|
10322
|
-
server.
|
|
11509
|
+
server.post("/ml/trend", {
|
|
10323
11510
|
schema: {
|
|
10324
11511
|
tags: ["Market"],
|
|
10325
|
-
summary: "
|
|
10326
|
-
|
|
11512
|
+
summary: "Score trend via ML",
|
|
11513
|
+
body: {
|
|
10327
11514
|
type: "object",
|
|
10328
|
-
properties: {
|
|
10329
|
-
|
|
11515
|
+
properties: {
|
|
11516
|
+
price_change_24h: { type: "number" },
|
|
11517
|
+
price_change_7d: { type: "number" },
|
|
11518
|
+
volume_24h: { type: "number" },
|
|
11519
|
+
market_cap: { type: "number" },
|
|
11520
|
+
volume_to_mcap_ratio: { type: "number" },
|
|
11521
|
+
rank: { type: "number" }
|
|
11522
|
+
}
|
|
10330
11523
|
}
|
|
10331
11524
|
},
|
|
10332
11525
|
handler: async (request) => {
|
|
10333
|
-
const
|
|
10334
|
-
|
|
11526
|
+
const features = request.body;
|
|
11527
|
+
let mlClient2 = getMLClient();
|
|
11528
|
+
if (!mlClient2) {
|
|
11529
|
+
const cfg = loadConfig();
|
|
11530
|
+
if (cfg.ml?.enabled && cfg.ml.sidecarUrl) {
|
|
11531
|
+
mlClient2 = initMLClient(cfg.ml.sidecarUrl);
|
|
11532
|
+
}
|
|
11533
|
+
}
|
|
11534
|
+
if (!mlClient2) return { error: "ML sidecar unavailable" };
|
|
11535
|
+
const result = await mlClient2.scoreTrend(features);
|
|
11536
|
+
return result ?? { error: "Trend scoring failed" };
|
|
10335
11537
|
}
|
|
10336
11538
|
});
|
|
10337
11539
|
server.get("/derivatives/:symbol", {
|
|
@@ -10350,6 +11552,8 @@ var init_market3 = __esm({
|
|
|
10350
11552
|
"src/api/routes/v1/market.ts"() {
|
|
10351
11553
|
"use strict";
|
|
10352
11554
|
init_tool_handler();
|
|
11555
|
+
init_client();
|
|
11556
|
+
init_loader();
|
|
10353
11557
|
}
|
|
10354
11558
|
});
|
|
10355
11559
|
|
|
@@ -10482,6 +11686,134 @@ var init_security = __esm({
|
|
|
10482
11686
|
}
|
|
10483
11687
|
});
|
|
10484
11688
|
|
|
11689
|
+
// src/api/routes/v1/backtest.ts
|
|
11690
|
+
async function backtestRoutes(fastify) {
|
|
11691
|
+
fastify.post("/v1/backtest", async (request, reply) => {
|
|
11692
|
+
const body = request.body;
|
|
11693
|
+
const config2 = {
|
|
11694
|
+
strategy: String(body["strategy"] ?? "momentum"),
|
|
11695
|
+
pair: String(body["pair"] ?? "BTCUSDT"),
|
|
11696
|
+
from: String(body["from"] ?? ""),
|
|
11697
|
+
to: String(body["to"] ?? ""),
|
|
11698
|
+
initialCapital: Number(body["initialCapital"] ?? 1e4),
|
|
11699
|
+
timeframe: String(body["timeframe"] ?? "4h"),
|
|
11700
|
+
slippageBps: Number(body["slippageBps"] ?? 10),
|
|
11701
|
+
commissionPct: Number(body["commissionPct"] ?? 0.1)
|
|
11702
|
+
};
|
|
11703
|
+
if (!config2.from || !config2.to) {
|
|
11704
|
+
return reply.status(400).send({ error: "from and to dates are required" });
|
|
11705
|
+
}
|
|
11706
|
+
const engine = new BacktestEngine(config2);
|
|
11707
|
+
const result = await engine.run();
|
|
11708
|
+
return result;
|
|
11709
|
+
});
|
|
11710
|
+
}
|
|
11711
|
+
var init_backtest = __esm({
|
|
11712
|
+
"src/api/routes/v1/backtest.ts"() {
|
|
11713
|
+
"use strict";
|
|
11714
|
+
init_engine2();
|
|
11715
|
+
}
|
|
11716
|
+
});
|
|
11717
|
+
|
|
11718
|
+
// src/api/routes/v1/agents.ts
|
|
11719
|
+
async function agentRoutes(fastify) {
|
|
11720
|
+
fastify.get("/v1/agents", async () => {
|
|
11721
|
+
const agents = listAgents();
|
|
11722
|
+
return {
|
|
11723
|
+
agents: agents.map((a) => {
|
|
11724
|
+
const status = getAgentStatus(a.id);
|
|
11725
|
+
return {
|
|
11726
|
+
name: a.name,
|
|
11727
|
+
strategy: a.strategy,
|
|
11728
|
+
pairs: a.pairs,
|
|
11729
|
+
interval: a.interval,
|
|
11730
|
+
status: status?.status ?? "idle",
|
|
11731
|
+
cycleCount: status?.cycleCount ?? 0
|
|
11732
|
+
};
|
|
11733
|
+
})
|
|
11734
|
+
};
|
|
11735
|
+
});
|
|
11736
|
+
fastify.post("/v1/agents", async (request) => {
|
|
11737
|
+
const body = request.body;
|
|
11738
|
+
const name = String(body["name"] ?? "");
|
|
11739
|
+
const strategy = String(body["strategy"] ?? "momentum");
|
|
11740
|
+
const pairs = body["pairs"] ?? ["BTC", "ETH"];
|
|
11741
|
+
const interval = Number(body["interval"] ?? 60);
|
|
11742
|
+
const agent = createAgent(name, strategy, pairs, interval);
|
|
11743
|
+
return { id: agent.id, name: agent.name, strategy: agent.strategy, pairs: agent.pairs };
|
|
11744
|
+
});
|
|
11745
|
+
fastify.get("/v1/agents/:name", async (request) => {
|
|
11746
|
+
const { name } = request.params;
|
|
11747
|
+
const agent = getAgentByName(name);
|
|
11748
|
+
if (!agent) return { error: "Agent not found" };
|
|
11749
|
+
const status = getAgentStatus(agent.id);
|
|
11750
|
+
return {
|
|
11751
|
+
name: agent.name,
|
|
11752
|
+
strategy: agent.strategy,
|
|
11753
|
+
pairs: agent.pairs,
|
|
11754
|
+
status: status?.status ?? "idle",
|
|
11755
|
+
cycleCount: status?.cycleCount ?? 0
|
|
11756
|
+
};
|
|
11757
|
+
});
|
|
11758
|
+
fastify.post("/v1/agents/:name/start", async (request) => {
|
|
11759
|
+
const { name } = request.params;
|
|
11760
|
+
const agent = getAgentByName(name);
|
|
11761
|
+
if (!agent) return { error: "Agent not found" };
|
|
11762
|
+
startAgent(agent.id);
|
|
11763
|
+
return { message: `Agent "${name}" started` };
|
|
11764
|
+
});
|
|
11765
|
+
fastify.post("/v1/agents/:name/stop", async (request) => {
|
|
11766
|
+
const { name } = request.params;
|
|
11767
|
+
const agent = getAgentByName(name);
|
|
11768
|
+
if (!agent) return { error: "Agent not found" };
|
|
11769
|
+
stopAgent(agent.id);
|
|
11770
|
+
return { message: `Agent "${name}" stopped` };
|
|
11771
|
+
});
|
|
11772
|
+
fastify.delete("/v1/agents/:name", async (request) => {
|
|
11773
|
+
const { name } = request.params;
|
|
11774
|
+
const agent = getAgentByName(name);
|
|
11775
|
+
if (!agent) return { error: "Agent not found" };
|
|
11776
|
+
deleteAgent(agent.id);
|
|
11777
|
+
return { message: `Agent "${name}" deleted` };
|
|
11778
|
+
});
|
|
11779
|
+
}
|
|
11780
|
+
var init_agents = __esm({
|
|
11781
|
+
"src/api/routes/v1/agents.ts"() {
|
|
11782
|
+
"use strict";
|
|
11783
|
+
init_agent();
|
|
11784
|
+
}
|
|
11785
|
+
});
|
|
11786
|
+
|
|
11787
|
+
// src/api/routes/v1/portfolio.ts
|
|
11788
|
+
async function portfolioRoutes(fastify) {
|
|
11789
|
+
fastify.get("/v1/portfolio/:agentId", async (request) => {
|
|
11790
|
+
const { agentId } = request.params;
|
|
11791
|
+
return {
|
|
11792
|
+
agentId,
|
|
11793
|
+
totalValue: 1e4,
|
|
11794
|
+
cash: 1e4,
|
|
11795
|
+
positions: [],
|
|
11796
|
+
totalReturn: 0,
|
|
11797
|
+
totalReturnPct: 0,
|
|
11798
|
+
winRate: 0,
|
|
11799
|
+
sharpeRatio: 0,
|
|
11800
|
+
maxDrawdown: 0
|
|
11801
|
+
};
|
|
11802
|
+
});
|
|
11803
|
+
fastify.get("/v1/portfolio/:agentId/trades", async (request) => {
|
|
11804
|
+
const { agentId } = request.params;
|
|
11805
|
+
return {
|
|
11806
|
+
agentId,
|
|
11807
|
+
trades: []
|
|
11808
|
+
};
|
|
11809
|
+
});
|
|
11810
|
+
}
|
|
11811
|
+
var init_portfolio = __esm({
|
|
11812
|
+
"src/api/routes/v1/portfolio.ts"() {
|
|
11813
|
+
"use strict";
|
|
11814
|
+
}
|
|
11815
|
+
});
|
|
11816
|
+
|
|
10485
11817
|
// src/api/auth/keys.ts
|
|
10486
11818
|
import { randomBytes, scryptSync } from "crypto";
|
|
10487
11819
|
function hashApiKey(key) {
|
|
@@ -10543,19 +11875,26 @@ var init_keys2 = __esm({
|
|
|
10543
11875
|
});
|
|
10544
11876
|
|
|
10545
11877
|
// src/api/auth/middleware.ts
|
|
11878
|
+
import { timingSafeEqual } from "crypto";
|
|
10546
11879
|
async function authMiddleware(request, reply) {
|
|
10547
|
-
if (PUBLIC_PATHS.some((p) => request.url === p)
|
|
11880
|
+
if (PUBLIC_PATHS.some((p) => request.url === p)) {
|
|
11881
|
+
return;
|
|
11882
|
+
}
|
|
11883
|
+
if (request.url === "/docs" || request.url.startsWith("/docs/")) {
|
|
11884
|
+
if (process.env["NODE_ENV"] === "production") {
|
|
11885
|
+
return reply.status(404).send({ error: "Not found" });
|
|
11886
|
+
}
|
|
10548
11887
|
return;
|
|
10549
11888
|
}
|
|
10550
11889
|
const apiKey = request.headers["x-api-key"];
|
|
10551
|
-
if (!apiKey) {
|
|
11890
|
+
if (!apiKey || apiKey.length > 256) {
|
|
10552
11891
|
return reply.status(401).send({
|
|
10553
11892
|
error: "Unauthorized",
|
|
10554
|
-
message: "Missing X-API-Key header"
|
|
11893
|
+
message: "Missing or invalid X-API-Key header"
|
|
10555
11894
|
});
|
|
10556
11895
|
}
|
|
10557
11896
|
const keyHash = hashApiKey(apiKey);
|
|
10558
|
-
const valid =
|
|
11897
|
+
const valid = validateKey2(keyHash);
|
|
10559
11898
|
if (!valid) {
|
|
10560
11899
|
return reply.status(403).send({
|
|
10561
11900
|
error: "Forbidden",
|
|
@@ -10563,20 +11902,43 @@ async function authMiddleware(request, reply) {
|
|
|
10563
11902
|
});
|
|
10564
11903
|
}
|
|
10565
11904
|
}
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
11905
|
+
function validateKey2(keyHash) {
|
|
11906
|
+
try {
|
|
11907
|
+
const db2 = getDb();
|
|
11908
|
+
db2.exec(`
|
|
11909
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
11910
|
+
id TEXT PRIMARY KEY,
|
|
11911
|
+
label TEXT NOT NULL,
|
|
11912
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
11913
|
+
key_prefix TEXT NOT NULL,
|
|
11914
|
+
rate_limit INTEGER NOT NULL DEFAULT 100,
|
|
11915
|
+
created_at INTEGER NOT NULL,
|
|
11916
|
+
revoked_at INTEGER
|
|
11917
|
+
)
|
|
11918
|
+
`);
|
|
11919
|
+
const row = db2.prepare("SELECT key_hash FROM api_keys WHERE revoked_at IS NULL").all();
|
|
11920
|
+
if (row.length === 0) {
|
|
11921
|
+
return false;
|
|
11922
|
+
}
|
|
11923
|
+
const inputBuf = Buffer.from(keyHash, "hex");
|
|
11924
|
+
for (const r of row) {
|
|
11925
|
+
const storedBuf = Buffer.from(r.key_hash, "hex");
|
|
11926
|
+
if (inputBuf.length === storedBuf.length && timingSafeEqual(inputBuf, storedBuf)) {
|
|
11927
|
+
return true;
|
|
11928
|
+
}
|
|
11929
|
+
}
|
|
11930
|
+
return false;
|
|
11931
|
+
} catch {
|
|
11932
|
+
return false;
|
|
11933
|
+
}
|
|
10572
11934
|
}
|
|
10573
11935
|
var PUBLIC_PATHS;
|
|
10574
11936
|
var init_middleware = __esm({
|
|
10575
11937
|
"src/api/auth/middleware.ts"() {
|
|
10576
11938
|
"use strict";
|
|
10577
11939
|
init_keys2();
|
|
10578
|
-
|
|
10579
|
-
PUBLIC_PATHS = ["/health"
|
|
11940
|
+
init_cache();
|
|
11941
|
+
PUBLIC_PATHS = ["/health"];
|
|
10580
11942
|
}
|
|
10581
11943
|
});
|
|
10582
11944
|
|
|
@@ -10613,7 +11975,11 @@ import swagger from "@fastify/swagger";
|
|
|
10613
11975
|
import swaggerUi from "@fastify/swagger-ui";
|
|
10614
11976
|
async function startApiServer(options) {
|
|
10615
11977
|
const server = Fastify({ logger: false });
|
|
10616
|
-
|
|
11978
|
+
const isProd = process.env["NODE_ENV"] === "production";
|
|
11979
|
+
const origin = options.corsOrigin ?? "http://localhost:3000";
|
|
11980
|
+
await server.register(cors, {
|
|
11981
|
+
origin: isProd ? origin : true
|
|
11982
|
+
});
|
|
10617
11983
|
await server.register(rateLimit, {
|
|
10618
11984
|
max: 100,
|
|
10619
11985
|
timeWindow: "1 minute"
|
|
@@ -10623,7 +11989,7 @@ async function startApiServer(options) {
|
|
|
10623
11989
|
info: {
|
|
10624
11990
|
title: "Vizzor API",
|
|
10625
11991
|
description: "AI-powered crypto intelligence REST API",
|
|
10626
|
-
version: "0.
|
|
11992
|
+
version: "0.11.0"
|
|
10627
11993
|
},
|
|
10628
11994
|
servers: [{ url: `http://${options.host}:${options.port}` }],
|
|
10629
11995
|
components: {
|
|
@@ -10637,27 +12003,41 @@ async function startApiServer(options) {
|
|
|
10637
12003
|
}
|
|
10638
12004
|
}
|
|
10639
12005
|
});
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
12006
|
+
if (!isProd) {
|
|
12007
|
+
await server.register(swaggerUi, {
|
|
12008
|
+
routePrefix: "/docs"
|
|
12009
|
+
});
|
|
12010
|
+
}
|
|
12011
|
+
if (options.enableAuth !== false) {
|
|
10644
12012
|
server.addHook("onRequest", authMiddleware);
|
|
12013
|
+
} else if (isProd) {
|
|
12014
|
+
log9.warn("API authentication is DISABLED in production \u2014 this is insecure");
|
|
10645
12015
|
}
|
|
10646
12016
|
server.setErrorHandler(errorHandler);
|
|
10647
|
-
server.get("/health", async () =>
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
12017
|
+
server.get("/health", async () => {
|
|
12018
|
+
if (isProd) {
|
|
12019
|
+
return { status: "ok" };
|
|
12020
|
+
}
|
|
12021
|
+
return {
|
|
12022
|
+
status: "ok",
|
|
12023
|
+
version: "0.11.0",
|
|
12024
|
+
uptime: process.uptime(),
|
|
12025
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
12026
|
+
};
|
|
12027
|
+
});
|
|
10653
12028
|
await server.register(registerMarketRoutes, { prefix: "/v1/market" });
|
|
10654
12029
|
await server.register(registerAnalysisRoutes, { prefix: "/v1/analysis" });
|
|
10655
12030
|
await server.register(registerSecurityRoutes, { prefix: "/v1/security" });
|
|
12031
|
+
await server.register(backtestRoutes);
|
|
12032
|
+
await server.register(agentRoutes);
|
|
12033
|
+
await server.register(portfolioRoutes);
|
|
10656
12034
|
await server.listen({ port: options.port, host: options.host });
|
|
10657
|
-
|
|
10658
|
-
|
|
12035
|
+
log9.info(`Vizzor API listening on ${options.host}:${options.port}`);
|
|
12036
|
+
if (!isProd) {
|
|
12037
|
+
log9.info(`OpenAPI docs at http://${options.host}:${options.port}/docs`);
|
|
12038
|
+
}
|
|
10659
12039
|
}
|
|
10660
|
-
var
|
|
12040
|
+
var log9;
|
|
10661
12041
|
var init_server = __esm({
|
|
10662
12042
|
"src/api/server.ts"() {
|
|
10663
12043
|
"use strict";
|
|
@@ -10665,9 +12045,12 @@ var init_server = __esm({
|
|
|
10665
12045
|
init_market3();
|
|
10666
12046
|
init_analysis();
|
|
10667
12047
|
init_security();
|
|
12048
|
+
init_backtest();
|
|
12049
|
+
init_agents();
|
|
12050
|
+
init_portfolio();
|
|
10668
12051
|
init_middleware();
|
|
10669
12052
|
init_error_handler();
|
|
10670
|
-
|
|
12053
|
+
log9 = createLogger("api");
|
|
10671
12054
|
}
|
|
10672
12055
|
});
|
|
10673
12056
|
|
|
@@ -10749,6 +12132,480 @@ var init_api = __esm({
|
|
|
10749
12132
|
}
|
|
10750
12133
|
});
|
|
10751
12134
|
|
|
12135
|
+
// src/cli/commands/backtest.ts
|
|
12136
|
+
var backtest_exports = {};
|
|
12137
|
+
__export(backtest_exports, {
|
|
12138
|
+
registerBacktestCommand: () => registerBacktestCommand
|
|
12139
|
+
});
|
|
12140
|
+
function registerBacktestCommand(program2) {
|
|
12141
|
+
program2.command("backtest").description("Run a historical backtest for a trading strategy").requiredOption(
|
|
12142
|
+
"-s, --strategy <strategy>",
|
|
12143
|
+
"Strategy name (momentum, trend-following, ml-adaptive)"
|
|
12144
|
+
).requiredOption("--pair <pair>", "Trading pair (e.g. BTCUSDT)").requiredOption("--from <date>", "Start date (YYYY-MM-DD)").requiredOption("--to <date>", "End date (YYYY-MM-DD)").option("-c, --capital <amount>", "Initial capital in USD", "10000").option("-t, --timeframe <tf>", "Candle timeframe", "4h").option("--slippage <bps>", "Slippage in basis points", "10").option("--commission <pct>", "Commission percentage", "0.1").action(async (opts) => {
|
|
12145
|
+
try {
|
|
12146
|
+
const engine = new BacktestEngine({
|
|
12147
|
+
strategy: opts.strategy,
|
|
12148
|
+
pair: opts.pair,
|
|
12149
|
+
from: opts.from,
|
|
12150
|
+
to: opts.to,
|
|
12151
|
+
initialCapital: Number(opts.capital),
|
|
12152
|
+
timeframe: opts.timeframe,
|
|
12153
|
+
slippageBps: Number(opts.slippage),
|
|
12154
|
+
commissionPct: Number(opts.commission)
|
|
12155
|
+
});
|
|
12156
|
+
console.log(
|
|
12157
|
+
`Running backtest: ${opts.strategy} on ${opts.pair} (${opts.from} \u2192 ${opts.to})...
|
|
12158
|
+
`
|
|
12159
|
+
);
|
|
12160
|
+
const result = await engine.run();
|
|
12161
|
+
console.log("=== Backtest Results ===");
|
|
12162
|
+
console.log(`Strategy: ${result.config.strategy}`);
|
|
12163
|
+
console.log(`Pair: ${result.config.pair}`);
|
|
12164
|
+
console.log(`Period: ${result.config.from} \u2192 ${result.config.to}`);
|
|
12165
|
+
console.log(`Initial Capital: $${result.config.initialCapital.toLocaleString()}`);
|
|
12166
|
+
console.log("");
|
|
12167
|
+
console.log(
|
|
12168
|
+
`Total Return: $${result.metrics.totalReturn.toFixed(2)} (${result.metrics.totalReturnPct.toFixed(2)}%)`
|
|
12169
|
+
);
|
|
12170
|
+
console.log(`Win Rate: ${(result.metrics.winRate * 100).toFixed(1)}%`);
|
|
12171
|
+
console.log(`Total Trades: ${result.metrics.totalTrades}`);
|
|
12172
|
+
console.log(
|
|
12173
|
+
`Profit Factor: ${result.metrics.profitFactor === Infinity ? "\u221E" : result.metrics.profitFactor.toFixed(2)}`
|
|
12174
|
+
);
|
|
12175
|
+
console.log(`Sharpe Ratio: ${result.metrics.sharpeRatio.toFixed(2)}`);
|
|
12176
|
+
console.log(`Max Drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`);
|
|
12177
|
+
if (result.trades.length > 0) {
|
|
12178
|
+
console.log("\nLast 5 Trades:");
|
|
12179
|
+
for (const t of result.trades.slice(-5)) {
|
|
12180
|
+
const dir = t.pnl >= 0 ? "+" : "";
|
|
12181
|
+
console.log(
|
|
12182
|
+
` ${new Date(t.entryTime).toISOString().split("T")[0]} \u2192 ${new Date(t.exitTime).toISOString().split("T")[0]} | ${t.side} | Entry: $${t.entryPrice.toFixed(2)} \u2192 Exit: $${t.exitPrice.toFixed(2)} | PnL: ${dir}$${t.pnl.toFixed(2)} (${dir}${t.pnlPct.toFixed(2)}%)`
|
|
12183
|
+
);
|
|
12184
|
+
}
|
|
12185
|
+
}
|
|
12186
|
+
} catch (err) {
|
|
12187
|
+
log10.error(`Backtest failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
12188
|
+
process.exitCode = 1;
|
|
12189
|
+
}
|
|
12190
|
+
});
|
|
12191
|
+
}
|
|
12192
|
+
var log10;
|
|
12193
|
+
var init_backtest2 = __esm({
|
|
12194
|
+
"src/cli/commands/backtest.ts"() {
|
|
12195
|
+
"use strict";
|
|
12196
|
+
init_engine2();
|
|
12197
|
+
init_logger();
|
|
12198
|
+
log10 = createLogger("cli-backtest");
|
|
12199
|
+
}
|
|
12200
|
+
});
|
|
12201
|
+
|
|
12202
|
+
// src/core/agent/wallet-manager.ts
|
|
12203
|
+
import { randomBytes as randomBytes2, scryptSync as scryptSync2, createCipheriv, createDecipheriv } from "crypto";
|
|
12204
|
+
import {
|
|
12205
|
+
existsSync as existsSync3,
|
|
12206
|
+
mkdirSync as mkdirSync2,
|
|
12207
|
+
readFileSync as readFileSync3,
|
|
12208
|
+
writeFileSync as writeFileSync3,
|
|
12209
|
+
unlinkSync,
|
|
12210
|
+
readdirSync
|
|
12211
|
+
} from "fs";
|
|
12212
|
+
import { join as join3 } from "path";
|
|
12213
|
+
import { homedir as homedir2 } from "os";
|
|
12214
|
+
function ensureDir() {
|
|
12215
|
+
if (!existsSync3(WALLETS_DIR)) {
|
|
12216
|
+
mkdirSync2(WALLETS_DIR, { recursive: true });
|
|
12217
|
+
}
|
|
12218
|
+
}
|
|
12219
|
+
function deriveKey(password, salt) {
|
|
12220
|
+
return scryptSync2(password, salt, KEY_LENGTH, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
|
|
12221
|
+
}
|
|
12222
|
+
function encrypt(plaintext, password) {
|
|
12223
|
+
const salt = randomBytes2(32);
|
|
12224
|
+
const key = deriveKey(password, salt);
|
|
12225
|
+
const iv = randomBytes2(IV_LENGTH);
|
|
12226
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
12227
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
12228
|
+
const tag = cipher.getAuthTag();
|
|
12229
|
+
return {
|
|
12230
|
+
salt: salt.toString("hex"),
|
|
12231
|
+
iv: iv.toString("hex"),
|
|
12232
|
+
tag: tag.toString("hex"),
|
|
12233
|
+
ciphertext: encrypted.toString("hex")
|
|
12234
|
+
};
|
|
12235
|
+
}
|
|
12236
|
+
function createWallet(name, password) {
|
|
12237
|
+
ensureDir();
|
|
12238
|
+
const filePath = join3(WALLETS_DIR, `${name}.json`);
|
|
12239
|
+
if (existsSync3(filePath)) {
|
|
12240
|
+
throw new Error(`Wallet "${name}" already exists`);
|
|
12241
|
+
}
|
|
12242
|
+
const privateKey = `0x${randomBytes2(32).toString("hex")}`;
|
|
12243
|
+
const { salt, iv, tag, ciphertext } = encrypt(privateKey, password);
|
|
12244
|
+
const wallet = {
|
|
12245
|
+
name,
|
|
12246
|
+
address: "(derived on load)",
|
|
12247
|
+
salt,
|
|
12248
|
+
iv,
|
|
12249
|
+
tag,
|
|
12250
|
+
ciphertext,
|
|
12251
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12252
|
+
};
|
|
12253
|
+
writeFileSync3(filePath, JSON.stringify(wallet, null, 2));
|
|
12254
|
+
log11.info(`Wallet "${name}" created at ${filePath}`);
|
|
12255
|
+
return { name, address: wallet.address };
|
|
12256
|
+
}
|
|
12257
|
+
function importWallet(name, privateKey, password) {
|
|
12258
|
+
ensureDir();
|
|
12259
|
+
const filePath = join3(WALLETS_DIR, `${name}.json`);
|
|
12260
|
+
if (existsSync3(filePath)) {
|
|
12261
|
+
throw new Error(`Wallet "${name}" already exists`);
|
|
12262
|
+
}
|
|
12263
|
+
const pk = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
|
|
12264
|
+
const { salt, iv, tag, ciphertext } = encrypt(pk, password);
|
|
12265
|
+
const wallet = {
|
|
12266
|
+
name,
|
|
12267
|
+
address: "(derived on load)",
|
|
12268
|
+
salt,
|
|
12269
|
+
iv,
|
|
12270
|
+
tag,
|
|
12271
|
+
ciphertext,
|
|
12272
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12273
|
+
};
|
|
12274
|
+
writeFileSync3(filePath, JSON.stringify(wallet, null, 2));
|
|
12275
|
+
log11.info(`Wallet "${name}" imported`);
|
|
12276
|
+
return { name, address: wallet.address };
|
|
12277
|
+
}
|
|
12278
|
+
function listWallets() {
|
|
12279
|
+
ensureDir();
|
|
12280
|
+
const files = readdirSync(WALLETS_DIR).filter((f) => f.endsWith(".json"));
|
|
12281
|
+
return files.map((f) => {
|
|
12282
|
+
const wallet = JSON.parse(readFileSync3(join3(WALLETS_DIR, f), "utf8"));
|
|
12283
|
+
return { name: wallet.name, address: wallet.address, createdAt: wallet.createdAt };
|
|
12284
|
+
});
|
|
12285
|
+
}
|
|
12286
|
+
function deleteWallet(name) {
|
|
12287
|
+
const filePath = join3(WALLETS_DIR, `${name}.json`);
|
|
12288
|
+
if (!existsSync3(filePath)) {
|
|
12289
|
+
throw new Error(`Wallet "${name}" not found`);
|
|
12290
|
+
}
|
|
12291
|
+
unlinkSync(filePath);
|
|
12292
|
+
log11.info(`Wallet "${name}" deleted`);
|
|
12293
|
+
}
|
|
12294
|
+
var log11, WALLETS_DIR, SCRYPT_N, SCRYPT_R, SCRYPT_P, KEY_LENGTH, IV_LENGTH;
|
|
12295
|
+
var init_wallet_manager = __esm({
|
|
12296
|
+
"src/core/agent/wallet-manager.ts"() {
|
|
12297
|
+
"use strict";
|
|
12298
|
+
init_logger();
|
|
12299
|
+
log11 = createLogger("wallet-manager");
|
|
12300
|
+
WALLETS_DIR = join3(homedir2(), ".vizzor", "wallets");
|
|
12301
|
+
SCRYPT_N = 2 ** 14;
|
|
12302
|
+
SCRYPT_R = 8;
|
|
12303
|
+
SCRYPT_P = 1;
|
|
12304
|
+
KEY_LENGTH = 32;
|
|
12305
|
+
IV_LENGTH = 12;
|
|
12306
|
+
}
|
|
12307
|
+
});
|
|
12308
|
+
|
|
12309
|
+
// src/cli/commands/wallet.ts
|
|
12310
|
+
var wallet_exports = {};
|
|
12311
|
+
__export(wallet_exports, {
|
|
12312
|
+
registerWalletCommand: () => registerWalletCommand
|
|
12313
|
+
});
|
|
12314
|
+
function registerWalletCommand(program2) {
|
|
12315
|
+
const wallet = program2.command("wallet").description("Manage encrypted trading wallets");
|
|
12316
|
+
wallet.command("create <name>").description("Create a new wallet with a random private key").requiredOption("-p, --password <password>", "Encryption password").action((name, opts) => {
|
|
12317
|
+
try {
|
|
12318
|
+
const result = createWallet(name, opts.password);
|
|
12319
|
+
console.log(`Wallet "${result.name}" created successfully.`);
|
|
12320
|
+
console.log(`Address: ${result.address}`);
|
|
12321
|
+
console.log("Stored in ~/.vizzor/wallets/");
|
|
12322
|
+
} catch (err) {
|
|
12323
|
+
log12.error(`Failed to create wallet: ${err instanceof Error ? err.message : String(err)}`);
|
|
12324
|
+
process.exitCode = 1;
|
|
12325
|
+
}
|
|
12326
|
+
});
|
|
12327
|
+
wallet.command("import <name>").description("Import a wallet from a private key").requiredOption("-k, --key <privateKey>", "Private key (hex)").requiredOption("-p, --password <password>", "Encryption password").action((name, opts) => {
|
|
12328
|
+
try {
|
|
12329
|
+
const result = importWallet(name, opts.key, opts.password);
|
|
12330
|
+
console.log(`Wallet "${result.name}" imported successfully.`);
|
|
12331
|
+
} catch (err) {
|
|
12332
|
+
log12.error(`Failed to import wallet: ${err instanceof Error ? err.message : String(err)}`);
|
|
12333
|
+
process.exitCode = 1;
|
|
12334
|
+
}
|
|
12335
|
+
});
|
|
12336
|
+
wallet.command("list").description("List all saved wallets").action(() => {
|
|
12337
|
+
const wallets = listWallets();
|
|
12338
|
+
if (wallets.length === 0) {
|
|
12339
|
+
console.log("No wallets found.");
|
|
12340
|
+
return;
|
|
12341
|
+
}
|
|
12342
|
+
console.log("Wallets:");
|
|
12343
|
+
for (const w of wallets) {
|
|
12344
|
+
console.log(` ${w.name} \u2014 ${w.address} (created: ${w.createdAt})`);
|
|
12345
|
+
}
|
|
12346
|
+
});
|
|
12347
|
+
wallet.command("delete <name>").description("Delete a wallet").action((name) => {
|
|
12348
|
+
try {
|
|
12349
|
+
deleteWallet(name);
|
|
12350
|
+
console.log(`Wallet "${name}" deleted.`);
|
|
12351
|
+
} catch (err) {
|
|
12352
|
+
log12.error(`Failed to delete wallet: ${err instanceof Error ? err.message : String(err)}`);
|
|
12353
|
+
process.exitCode = 1;
|
|
12354
|
+
}
|
|
12355
|
+
});
|
|
12356
|
+
}
|
|
12357
|
+
var log12;
|
|
12358
|
+
var init_wallet2 = __esm({
|
|
12359
|
+
"src/cli/commands/wallet.ts"() {
|
|
12360
|
+
"use strict";
|
|
12361
|
+
init_wallet_manager();
|
|
12362
|
+
init_logger();
|
|
12363
|
+
log12 = createLogger("cli-wallet");
|
|
12364
|
+
}
|
|
12365
|
+
});
|
|
12366
|
+
|
|
12367
|
+
// src/data/sources/binance-ws.ts
|
|
12368
|
+
import { EventEmitter } from "events";
|
|
12369
|
+
var log13, BASE_URL7, BinanceWebSocket;
|
|
12370
|
+
var init_binance_ws = __esm({
|
|
12371
|
+
"src/data/sources/binance-ws.ts"() {
|
|
12372
|
+
"use strict";
|
|
12373
|
+
init_logger();
|
|
12374
|
+
log13 = createLogger("binance-ws");
|
|
12375
|
+
BASE_URL7 = "wss://stream.binance.com:9443/ws";
|
|
12376
|
+
BinanceWebSocket = class extends EventEmitter {
|
|
12377
|
+
ws = null;
|
|
12378
|
+
streams = [];
|
|
12379
|
+
reconnectDelay = 1e3;
|
|
12380
|
+
maxReconnectDelay = 3e4;
|
|
12381
|
+
heartbeatTimer = null;
|
|
12382
|
+
reconnecting = false;
|
|
12383
|
+
closed = false;
|
|
12384
|
+
constructor(streams) {
|
|
12385
|
+
super();
|
|
12386
|
+
this.streams = streams;
|
|
12387
|
+
}
|
|
12388
|
+
connect() {
|
|
12389
|
+
if (this.closed) return;
|
|
12390
|
+
const url = this.streams.length === 1 ? `${BASE_URL7}/${this.streams[0]}` : `${BASE_URL7}/${this.streams.join("/")}`;
|
|
12391
|
+
log13.debug(`Connecting to ${url}`);
|
|
12392
|
+
this.ws = new WebSocket(url);
|
|
12393
|
+
this.ws.onopen = () => {
|
|
12394
|
+
log13.info(`WebSocket connected (${this.streams.length} streams)`);
|
|
12395
|
+
this.reconnectDelay = 1e3;
|
|
12396
|
+
this.reconnecting = false;
|
|
12397
|
+
this.startHeartbeat();
|
|
12398
|
+
this.emit("connected");
|
|
12399
|
+
};
|
|
12400
|
+
this.ws.onmessage = (event) => {
|
|
12401
|
+
try {
|
|
12402
|
+
const data = JSON.parse(String(event.data));
|
|
12403
|
+
this.handleMessage(data);
|
|
12404
|
+
} catch {
|
|
12405
|
+
}
|
|
12406
|
+
};
|
|
12407
|
+
this.ws.onclose = () => {
|
|
12408
|
+
this.stopHeartbeat();
|
|
12409
|
+
if (!this.closed) {
|
|
12410
|
+
this.scheduleReconnect();
|
|
12411
|
+
}
|
|
12412
|
+
};
|
|
12413
|
+
this.ws.onerror = () => {
|
|
12414
|
+
log13.debug("WebSocket error");
|
|
12415
|
+
};
|
|
12416
|
+
}
|
|
12417
|
+
close() {
|
|
12418
|
+
this.closed = true;
|
|
12419
|
+
this.stopHeartbeat();
|
|
12420
|
+
if (this.ws) {
|
|
12421
|
+
this.ws.close();
|
|
12422
|
+
this.ws = null;
|
|
12423
|
+
}
|
|
12424
|
+
}
|
|
12425
|
+
handleMessage(data) {
|
|
12426
|
+
const eventType = data["e"];
|
|
12427
|
+
if (eventType === "trade") {
|
|
12428
|
+
const trade = {
|
|
12429
|
+
symbol: String(data["s"]),
|
|
12430
|
+
price: Number(data["p"]),
|
|
12431
|
+
quantity: Number(data["q"]),
|
|
12432
|
+
time: Number(data["T"]),
|
|
12433
|
+
isBuyerMaker: Boolean(data["m"])
|
|
12434
|
+
};
|
|
12435
|
+
this.emit("trade", trade);
|
|
12436
|
+
} else if (eventType === "kline") {
|
|
12437
|
+
const k = data["k"];
|
|
12438
|
+
const kline = {
|
|
12439
|
+
symbol: String(data["s"]),
|
|
12440
|
+
interval: String(k["i"]),
|
|
12441
|
+
open: Number(k["o"]),
|
|
12442
|
+
high: Number(k["h"]),
|
|
12443
|
+
low: Number(k["l"]),
|
|
12444
|
+
close: Number(k["c"]),
|
|
12445
|
+
volume: Number(k["v"]),
|
|
12446
|
+
openTime: Number(k["t"]),
|
|
12447
|
+
closeTime: Number(k["T"]),
|
|
12448
|
+
isClosed: Boolean(k["x"])
|
|
12449
|
+
};
|
|
12450
|
+
this.emit("kline", kline);
|
|
12451
|
+
} else if (eventType === "24hrTicker") {
|
|
12452
|
+
const ticker = {
|
|
12453
|
+
symbol: String(data["s"]),
|
|
12454
|
+
price: Number(data["c"]),
|
|
12455
|
+
priceChange: Number(data["p"]),
|
|
12456
|
+
priceChangePct: Number(data["P"]),
|
|
12457
|
+
volume: Number(data["v"]),
|
|
12458
|
+
quoteVolume: Number(data["q"])
|
|
12459
|
+
};
|
|
12460
|
+
this.emit("ticker", ticker);
|
|
12461
|
+
}
|
|
12462
|
+
}
|
|
12463
|
+
scheduleReconnect() {
|
|
12464
|
+
if (this.reconnecting || this.closed) return;
|
|
12465
|
+
this.reconnecting = true;
|
|
12466
|
+
log13.info(`Reconnecting in ${this.reconnectDelay}ms...`);
|
|
12467
|
+
setTimeout(() => {
|
|
12468
|
+
this.connect();
|
|
12469
|
+
}, this.reconnectDelay);
|
|
12470
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
12471
|
+
}
|
|
12472
|
+
startHeartbeat() {
|
|
12473
|
+
this.heartbeatTimer = setInterval(() => {
|
|
12474
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
12475
|
+
this.ws.send(JSON.stringify({ method: "PING" }));
|
|
12476
|
+
}
|
|
12477
|
+
}, 3e4);
|
|
12478
|
+
}
|
|
12479
|
+
stopHeartbeat() {
|
|
12480
|
+
if (this.heartbeatTimer) {
|
|
12481
|
+
clearInterval(this.heartbeatTimer);
|
|
12482
|
+
this.heartbeatTimer = null;
|
|
12483
|
+
}
|
|
12484
|
+
}
|
|
12485
|
+
};
|
|
12486
|
+
}
|
|
12487
|
+
});
|
|
12488
|
+
|
|
12489
|
+
// src/data/sources/ws-manager.ts
|
|
12490
|
+
var ws_manager_exports = {};
|
|
12491
|
+
__export(ws_manager_exports, {
|
|
12492
|
+
WSManager: () => WSManager,
|
|
12493
|
+
getWSManager: () => getWSManager,
|
|
12494
|
+
initWSManager: () => initWSManager
|
|
12495
|
+
});
|
|
12496
|
+
function getWSManager() {
|
|
12497
|
+
return instance2;
|
|
12498
|
+
}
|
|
12499
|
+
function initWSManager() {
|
|
12500
|
+
if (!instance2) {
|
|
12501
|
+
instance2 = new WSManager();
|
|
12502
|
+
}
|
|
12503
|
+
return instance2;
|
|
12504
|
+
}
|
|
12505
|
+
var log14, MAX_STREAMS_PER_CONN, MAX_CONNECTIONS, WSManager, instance2;
|
|
12506
|
+
var init_ws_manager = __esm({
|
|
12507
|
+
"src/data/sources/ws-manager.ts"() {
|
|
12508
|
+
"use strict";
|
|
12509
|
+
init_binance_ws();
|
|
12510
|
+
init_logger();
|
|
12511
|
+
log14 = createLogger("ws-manager");
|
|
12512
|
+
MAX_STREAMS_PER_CONN = 1024;
|
|
12513
|
+
MAX_CONNECTIONS = 5;
|
|
12514
|
+
WSManager = class {
|
|
12515
|
+
connections = [];
|
|
12516
|
+
priceCache = /* @__PURE__ */ new Map();
|
|
12517
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
12518
|
+
// symbol → stream types
|
|
12519
|
+
tradeCallbacks = /* @__PURE__ */ new Map();
|
|
12520
|
+
klineCallbacks = /* @__PURE__ */ new Map();
|
|
12521
|
+
started = false;
|
|
12522
|
+
subscribe(symbol, streams = ["trade", "ticker"]) {
|
|
12523
|
+
const existing = this.subscriptions.get(symbol) ?? /* @__PURE__ */ new Set();
|
|
12524
|
+
for (const s of streams) {
|
|
12525
|
+
existing.add(s);
|
|
12526
|
+
}
|
|
12527
|
+
this.subscriptions.set(symbol, existing);
|
|
12528
|
+
}
|
|
12529
|
+
unsubscribe(symbol) {
|
|
12530
|
+
this.subscriptions.delete(symbol);
|
|
12531
|
+
this.priceCache.delete(symbol.toUpperCase());
|
|
12532
|
+
}
|
|
12533
|
+
start() {
|
|
12534
|
+
if (this.started) return;
|
|
12535
|
+
this.started = true;
|
|
12536
|
+
const allStreams = [];
|
|
12537
|
+
for (const [symbol, types] of this.subscriptions) {
|
|
12538
|
+
const lower = symbol.toLowerCase();
|
|
12539
|
+
for (const type of types) {
|
|
12540
|
+
if (type === "trade") allStreams.push(`${lower}@trade`);
|
|
12541
|
+
else if (type === "kline_1m") allStreams.push(`${lower}@kline_1m`);
|
|
12542
|
+
else if (type === "ticker") allStreams.push(`${lower}@ticker`);
|
|
12543
|
+
}
|
|
12544
|
+
}
|
|
12545
|
+
if (allStreams.length === 0) {
|
|
12546
|
+
log14.info("No subscriptions \u2014 WebSocket manager idle");
|
|
12547
|
+
return;
|
|
12548
|
+
}
|
|
12549
|
+
for (let i = 0; i < allStreams.length; i += MAX_STREAMS_PER_CONN) {
|
|
12550
|
+
if (this.connections.length >= MAX_CONNECTIONS) break;
|
|
12551
|
+
const chunk = allStreams.slice(i, i + MAX_STREAMS_PER_CONN);
|
|
12552
|
+
const ws = new BinanceWebSocket(chunk);
|
|
12553
|
+
ws.on("trade", (trade) => {
|
|
12554
|
+
this.priceCache.set(trade.symbol, { price: trade.price, time: trade.time });
|
|
12555
|
+
const callbacks = this.tradeCallbacks.get(trade.symbol);
|
|
12556
|
+
if (callbacks) {
|
|
12557
|
+
for (const cb of callbacks) cb(trade);
|
|
12558
|
+
}
|
|
12559
|
+
});
|
|
12560
|
+
ws.on("ticker", (ticker) => {
|
|
12561
|
+
this.priceCache.set(ticker.symbol, { price: ticker.price, time: Date.now() });
|
|
12562
|
+
});
|
|
12563
|
+
ws.on("kline", (kline) => {
|
|
12564
|
+
const key = `${kline.symbol}:${kline.interval}`;
|
|
12565
|
+
const callbacks = this.klineCallbacks.get(key);
|
|
12566
|
+
if (callbacks) {
|
|
12567
|
+
for (const cb of callbacks) cb(kline);
|
|
12568
|
+
}
|
|
12569
|
+
});
|
|
12570
|
+
ws.connect();
|
|
12571
|
+
this.connections.push(ws);
|
|
12572
|
+
}
|
|
12573
|
+
log14.info(
|
|
12574
|
+
`WebSocket manager started: ${this.connections.length} connections, ${allStreams.length} streams`
|
|
12575
|
+
);
|
|
12576
|
+
}
|
|
12577
|
+
stop() {
|
|
12578
|
+
for (const ws of this.connections) {
|
|
12579
|
+
ws.close();
|
|
12580
|
+
}
|
|
12581
|
+
this.connections = [];
|
|
12582
|
+
this.started = false;
|
|
12583
|
+
log14.info("WebSocket manager stopped");
|
|
12584
|
+
}
|
|
12585
|
+
getLatestPrice(symbol) {
|
|
12586
|
+
const upper = symbol.toUpperCase().replace("/", "");
|
|
12587
|
+
const cached = this.priceCache.get(upper);
|
|
12588
|
+
if (!cached) return null;
|
|
12589
|
+
if (Date.now() - cached.time > 6e4) return null;
|
|
12590
|
+
return cached.price;
|
|
12591
|
+
}
|
|
12592
|
+
onTrade(symbol, callback) {
|
|
12593
|
+
const upper = symbol.toUpperCase();
|
|
12594
|
+
const set = this.tradeCallbacks.get(upper) ?? /* @__PURE__ */ new Set();
|
|
12595
|
+
set.add(callback);
|
|
12596
|
+
this.tradeCallbacks.set(upper, set);
|
|
12597
|
+
}
|
|
12598
|
+
onKline(symbol, interval, callback) {
|
|
12599
|
+
const key = `${symbol.toUpperCase()}:${interval}`;
|
|
12600
|
+
const set = this.klineCallbacks.get(key) ?? /* @__PURE__ */ new Set();
|
|
12601
|
+
set.add(callback);
|
|
12602
|
+
this.klineCallbacks.set(key, set);
|
|
12603
|
+
}
|
|
12604
|
+
};
|
|
12605
|
+
instance2 = null;
|
|
12606
|
+
}
|
|
12607
|
+
});
|
|
12608
|
+
|
|
10752
12609
|
// src/tui/components/status-bar.tsx
|
|
10753
12610
|
import React, { useState, useEffect } from "react";
|
|
10754
12611
|
import { Box, Text, Spacer } from "ink";
|
|
@@ -10873,7 +12730,7 @@ function WelcomeBanner() {
|
|
|
10873
12730
|
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
10874
12731
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: " " }),
|
|
10875
12732
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "blue", children: "vizzor" }),
|
|
10876
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: " v0.
|
|
12733
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: " v0.11.0" }),
|
|
10877
12734
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " \u2014 AI-powered crypto chronovisor" })
|
|
10878
12735
|
] }),
|
|
10879
12736
|
/* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " ML models: LSTM + Random Forest + Isolation Forest + GBM Rug + Wallet LSTM + DistilBERT NLP" }) }),
|
|
@@ -11561,6 +13418,8 @@ async function executeCommand(name, args2) {
|
|
|
11561
13418
|
return handleConfig(args2);
|
|
11562
13419
|
case "agent":
|
|
11563
13420
|
return handleAgent(args2);
|
|
13421
|
+
case "backtest":
|
|
13422
|
+
return handleBacktest(args2);
|
|
11564
13423
|
case "clear":
|
|
11565
13424
|
return { blocks: [], text: "" };
|
|
11566
13425
|
case "exit":
|
|
@@ -11910,6 +13769,7 @@ function handleHelp() {
|
|
|
11910
13769
|
" /trends Live trending tokens + top gainers/losers",
|
|
11911
13770
|
" /audit <contract> [--chain <chain>] Audit a smart contract (bytecode scanning)",
|
|
11912
13771
|
" /agent <sub> Manage autonomous trading agents",
|
|
13772
|
+
" /backtest -s <strat> --pair <P> --from <D> --to <D> Run a historical backtest",
|
|
11913
13773
|
" /provider [list|<name>] Show/switch AI provider",
|
|
11914
13774
|
" /config [set <key> <value>] Show or update configuration",
|
|
11915
13775
|
" /clear Clear message history",
|
|
@@ -11922,6 +13782,62 @@ function handleHelp() {
|
|
|
11922
13782
|
].join("\n");
|
|
11923
13783
|
return { blocks: [], text };
|
|
11924
13784
|
}
|
|
13785
|
+
async function handleBacktest(args2) {
|
|
13786
|
+
const stratIdx = args2.indexOf("-s") !== -1 ? args2.indexOf("-s") : args2.indexOf("--strategy");
|
|
13787
|
+
const pairIdx = args2.indexOf("--pair");
|
|
13788
|
+
const fromIdx = args2.indexOf("--from");
|
|
13789
|
+
const toIdx = args2.indexOf("--to");
|
|
13790
|
+
const tfIdx = args2.indexOf("--timeframe");
|
|
13791
|
+
const strategy = stratIdx !== -1 && stratIdx + 1 < args2.length ? args2[stratIdx + 1] : "";
|
|
13792
|
+
const pair = pairIdx !== -1 && pairIdx + 1 < args2.length ? args2[pairIdx + 1] : "";
|
|
13793
|
+
const from = fromIdx !== -1 && fromIdx + 1 < args2.length ? args2[fromIdx + 1] : "";
|
|
13794
|
+
const to = toIdx !== -1 && toIdx + 1 < args2.length ? args2[toIdx + 1] : "";
|
|
13795
|
+
const timeframe = tfIdx !== -1 && tfIdx + 1 < args2.length ? args2[tfIdx + 1] : "4h";
|
|
13796
|
+
if (!strategy || !pair || !from || !to) {
|
|
13797
|
+
return {
|
|
13798
|
+
blocks: [],
|
|
13799
|
+
text: "Usage: /backtest -s <strategy> --pair <PAIR> --from <YYYY-MM-DD> --to <YYYY-MM-DD> [--timeframe <tf>]"
|
|
13800
|
+
};
|
|
13801
|
+
}
|
|
13802
|
+
try {
|
|
13803
|
+
const { BacktestEngine: BacktestEngine2 } = await Promise.resolve().then(() => (init_engine2(), engine_exports));
|
|
13804
|
+
const engine = new BacktestEngine2({
|
|
13805
|
+
strategy,
|
|
13806
|
+
pair,
|
|
13807
|
+
from,
|
|
13808
|
+
to,
|
|
13809
|
+
initialCapital: 1e4,
|
|
13810
|
+
timeframe,
|
|
13811
|
+
slippageBps: 10,
|
|
13812
|
+
commissionPct: 0.1
|
|
13813
|
+
});
|
|
13814
|
+
const result = await engine.run();
|
|
13815
|
+
const lines = [
|
|
13816
|
+
`Backtest: ${result.config.strategy} on ${result.config.pair} (${result.config.from} \u2192 ${result.config.to})`,
|
|
13817
|
+
`Initial Capital: $${result.config.initialCapital.toLocaleString()}`,
|
|
13818
|
+
"",
|
|
13819
|
+
`Total Return: $${result.metrics.totalReturn.toFixed(2)} (${result.metrics.totalReturnPct.toFixed(2)}%)`,
|
|
13820
|
+
`Win Rate: ${(result.metrics.winRate * 100).toFixed(1)}%`,
|
|
13821
|
+
`Total Trades: ${result.metrics.totalTrades}`,
|
|
13822
|
+
`Profit Factor: ${result.metrics.profitFactor === Infinity ? "\u221E" : result.metrics.profitFactor.toFixed(2)}`,
|
|
13823
|
+
`Sharpe Ratio: ${result.metrics.sharpeRatio.toFixed(2)}`,
|
|
13824
|
+
`Max Drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`
|
|
13825
|
+
];
|
|
13826
|
+
if (result.trades.length > 0) {
|
|
13827
|
+
lines.push("", "Last 5 Trades:");
|
|
13828
|
+
for (const t of result.trades.slice(-5)) {
|
|
13829
|
+
const dir = t.pnl >= 0 ? "+" : "";
|
|
13830
|
+
lines.push(
|
|
13831
|
+
` ${new Date(t.entryTime).toISOString().split("T")[0]} \u2192 ${new Date(t.exitTime).toISOString().split("T")[0]} | ${t.side} | $${t.entryPrice.toFixed(2)} \u2192 $${t.exitPrice.toFixed(2)} | ${dir}$${t.pnl.toFixed(2)} (${dir}${t.pnlPct.toFixed(2)}%)`
|
|
13832
|
+
);
|
|
13833
|
+
}
|
|
13834
|
+
}
|
|
13835
|
+
return { blocks: [], text: lines.join("\n") };
|
|
13836
|
+
} catch (err) {
|
|
13837
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13838
|
+
return { blocks: [], text: `Backtest failed: ${message}` };
|
|
13839
|
+
}
|
|
13840
|
+
}
|
|
11925
13841
|
async function handleConfig(args2) {
|
|
11926
13842
|
if (args2[0] === "set") {
|
|
11927
13843
|
const key = args2[1];
|
|
@@ -12503,12 +14419,12 @@ var init_app = __esm({
|
|
|
12503
14419
|
// src/index.ts
|
|
12504
14420
|
init_loader();
|
|
12505
14421
|
import { Command } from "commander";
|
|
12506
|
-
import { readFileSync as
|
|
14422
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
12507
14423
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12508
14424
|
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
12509
14425
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
12510
14426
|
var __dirname2 = dirname2(__filename2);
|
|
12511
|
-
var pkg = JSON.parse(
|
|
14427
|
+
var pkg = JSON.parse(readFileSync4(resolve3(__dirname2, "..", "package.json"), "utf-8"));
|
|
12512
14428
|
var program = new Command().name("vizzor").description("Crypto chronovisor \u2014 AI-powered on-chain intelligence").version(pkg.version);
|
|
12513
14429
|
program.hook("preAction", async () => {
|
|
12514
14430
|
await loadConfig();
|
|
@@ -12583,9 +14499,30 @@ apiKeyCmd.command("revoke <id>").description("Revoke an API key").action(async (
|
|
|
12583
14499
|
const { handleApiKeyRevoke: handleApiKeyRevoke2 } = await Promise.resolve().then(() => (init_api(), api_exports));
|
|
12584
14500
|
handleApiKeyRevoke2(id);
|
|
12585
14501
|
});
|
|
14502
|
+
apiCmd.command("start").description("Start the REST API server").option("--port <port>", "Server port", (v) => parseInt(v, 10), 3e3).option("--host <host>", "Server host", "0.0.0.0").option("--auth", "Enable API key authentication", false).action(async (options) => {
|
|
14503
|
+
const { handleServe: handleServe2 } = await Promise.resolve().then(() => (init_serve(), serve_exports));
|
|
14504
|
+
await handleServe2(options);
|
|
14505
|
+
});
|
|
14506
|
+
var { registerBacktestCommand: registerBacktestCommand2 } = await Promise.resolve().then(() => (init_backtest2(), backtest_exports));
|
|
14507
|
+
registerBacktestCommand2(program);
|
|
14508
|
+
var { registerWalletCommand: registerWalletCommand2 } = await Promise.resolve().then(() => (init_wallet2(), wallet_exports));
|
|
14509
|
+
registerWalletCommand2(program);
|
|
12586
14510
|
var args = process.argv.slice(2);
|
|
12587
14511
|
if (args.length === 0) {
|
|
12588
14512
|
await loadConfig();
|
|
14513
|
+
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
|
|
14514
|
+
try {
|
|
14515
|
+
const config2 = getConfig2();
|
|
14516
|
+
if (config2.realtime?.enabled) {
|
|
14517
|
+
const { initWSManager: initWSManager2 } = await Promise.resolve().then(() => (init_ws_manager(), ws_manager_exports));
|
|
14518
|
+
const wsManager = initWSManager2();
|
|
14519
|
+
for (const symbol of config2.realtime.symbols) {
|
|
14520
|
+
wsManager.subscribe(symbol);
|
|
14521
|
+
}
|
|
14522
|
+
wsManager.start();
|
|
14523
|
+
}
|
|
14524
|
+
} catch {
|
|
14525
|
+
}
|
|
12589
14526
|
const { startTUI: startTUI2 } = await Promise.resolve().then(() => (init_app(), app_exports));
|
|
12590
14527
|
startTUI2();
|
|
12591
14528
|
} else {
|