apexbot 1.0.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.
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ /**
3
+ * ApexBot Gateway - Central control plane
4
+ * Similar to Clawdbot's Gateway architecture
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.Gateway = void 0;
11
+ const ws_1 = require("ws");
12
+ const http_1 = __importDefault(require("http"));
13
+ const eventBus_1 = require("../core/eventBus");
14
+ const sessionManager_1 = require("../sessions/sessionManager");
15
+ const channelManager_1 = require("../channels/channelManager");
16
+ const agentManager_1 = require("../agent/agentManager");
17
+ const dashboard_1 = require("./dashboard");
18
+ class Gateway {
19
+ config;
20
+ server = null;
21
+ wss = null;
22
+ clients = new Map();
23
+ sessions;
24
+ channels;
25
+ agents;
26
+ constructor(config = {}) {
27
+ this.config = {
28
+ port: config.port || 18789,
29
+ host: config.host || '127.0.0.1',
30
+ token: config.token,
31
+ verbose: config.verbose || false,
32
+ };
33
+ this.sessions = new sessionManager_1.SessionManager();
34
+ this.channels = new channelManager_1.ChannelManager(this);
35
+ this.agents = new agentManager_1.AgentManager();
36
+ this.setupEventHandlers();
37
+ }
38
+ setupEventHandlers() {
39
+ // Route incoming messages to agent
40
+ (0, eventBus_1.on)('channel:message', async (msg) => {
41
+ if (this.config.verbose) {
42
+ console.log(`[Gateway] Message from ${msg.channel}:${msg.from}: ${msg.text?.slice(0, 50)}...`);
43
+ }
44
+ // Get or create session
45
+ const session = this.sessions.getOrCreate(msg.channel, msg.from, msg.isGroup);
46
+ // Process with agent
47
+ const response = await this.agents.process(session, msg);
48
+ // Send response back
49
+ if (response) {
50
+ (0, eventBus_1.emit)('channel:send', {
51
+ channel: msg.channel,
52
+ to: msg.from,
53
+ text: response.text,
54
+ replyTo: msg.id,
55
+ });
56
+ }
57
+ });
58
+ // Handle outgoing messages
59
+ (0, eventBus_1.on)('channel:send', async (msg) => {
60
+ await this.channels.send(msg.channel, msg.to, msg.text, msg.replyTo);
61
+ });
62
+ }
63
+ async start() {
64
+ // Create HTTP server for WebSocket and REST API
65
+ this.server = http_1.default.createServer((req, res) => {
66
+ this.handleHttpRequest(req, res);
67
+ });
68
+ // Create WebSocket server
69
+ this.wss = new ws_1.WebSocketServer({ server: this.server });
70
+ this.wss.on('connection', (ws, req) => this.handleWsConnection(ws, req));
71
+ // Start listening
72
+ return new Promise((resolve) => {
73
+ this.server.listen(this.config.port, this.config.host, () => {
74
+ console.log(`🚀 Gateway running at http://${this.config.host}:${this.config.port}`);
75
+ console.log(`📡 WebSocket at ws://${this.config.host}:${this.config.port}`);
76
+ resolve();
77
+ });
78
+ });
79
+ }
80
+ handleHttpRequest(req, res) {
81
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
82
+ // CORS headers
83
+ res.setHeader('Access-Control-Allow-Origin', '*');
84
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
85
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
86
+ if (req.method === 'OPTIONS') {
87
+ res.writeHead(204);
88
+ res.end();
89
+ return;
90
+ }
91
+ // Public endpoints (no auth required)
92
+ const publicPaths = ['/', '/health', '/chat', '/ui'];
93
+ const isPublic = publicPaths.some(p => url.pathname === p || url.pathname.startsWith('/chat'));
94
+ // Auth check for protected endpoints
95
+ if (!isPublic && this.config.token) {
96
+ const auth = req.headers.authorization;
97
+ if (auth !== `Bearer ${this.config.token}`) {
98
+ res.writeHead(401, { 'Content-Type': 'application/json' });
99
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
100
+ return;
101
+ }
102
+ }
103
+ // Routes
104
+ switch (url.pathname) {
105
+ case '/':
106
+ case '/health':
107
+ res.writeHead(200, { 'Content-Type': 'application/json' });
108
+ res.end(JSON.stringify({
109
+ status: 'ok',
110
+ version: '0.2.0',
111
+ uptime: process.uptime(),
112
+ channels: this.channels.getStatus(),
113
+ sessions: this.sessions.count(),
114
+ }));
115
+ break;
116
+ case '/api/status':
117
+ res.writeHead(200, { 'Content-Type': 'application/json' });
118
+ res.end(JSON.stringify({
119
+ gateway: { host: this.config.host, port: this.config.port },
120
+ channels: this.channels.getStatus(),
121
+ sessions: this.sessions.getAll().map(s => ({
122
+ id: s.id,
123
+ channel: s.channel,
124
+ peer: s.peer,
125
+ messageCount: s.messageCount,
126
+ })),
127
+ agents: this.agents.getStatus(),
128
+ }));
129
+ break;
130
+ case '/api/sessions':
131
+ res.writeHead(200, { 'Content-Type': 'application/json' });
132
+ res.end(JSON.stringify(this.sessions.getAll()));
133
+ break;
134
+ default:
135
+ // Serve Dashboard UI
136
+ if (url.pathname.startsWith('/chat') || url.pathname === '/ui' || url.pathname === '/dashboard') {
137
+ res.writeHead(200, { 'Content-Type': 'text/html' });
138
+ res.end((0, dashboard_1.getDashboardHtml)());
139
+ }
140
+ else {
141
+ res.writeHead(404, { 'Content-Type': 'application/json' });
142
+ res.end(JSON.stringify({ error: 'Not found' }));
143
+ }
144
+ }
145
+ }
146
+ handleWsConnection(ws, req) {
147
+ const clientId = Math.random().toString(36).substring(7);
148
+ this.clients.set(clientId, ws);
149
+ if (this.config.verbose) {
150
+ console.log(`[Gateway] WS client connected: ${clientId}`);
151
+ }
152
+ ws.on('message', async (data) => {
153
+ try {
154
+ const msg = JSON.parse(data.toString());
155
+ await this.handleWsMessage(clientId, ws, msg);
156
+ }
157
+ catch (e) {
158
+ ws.send(JSON.stringify({ error: 'Invalid message format' }));
159
+ }
160
+ });
161
+ ws.on('close', () => {
162
+ this.clients.delete(clientId);
163
+ if (this.config.verbose) {
164
+ console.log(`[Gateway] WS client disconnected: ${clientId}`);
165
+ }
166
+ });
167
+ // Send welcome
168
+ ws.send(JSON.stringify({
169
+ type: 'connected',
170
+ clientId,
171
+ version: '0.2.0',
172
+ }));
173
+ }
174
+ async handleWsMessage(clientId, ws, msg) {
175
+ switch (msg.type) {
176
+ case 'ping':
177
+ ws.send(JSON.stringify({ type: 'pong' }));
178
+ break;
179
+ case 'chat':
180
+ // WebChat message
181
+ (0, eventBus_1.emit)('channel:message', {
182
+ channel: 'webchat',
183
+ from: clientId,
184
+ text: msg.text,
185
+ id: msg.id || Date.now().toString(),
186
+ isGroup: false,
187
+ });
188
+ break;
189
+ case 'subscribe':
190
+ // Subscribe to events
191
+ // TODO: implement event subscriptions
192
+ break;
193
+ case 'status':
194
+ ws.send(JSON.stringify({
195
+ type: 'status',
196
+ channels: this.channels.getStatus(),
197
+ sessions: this.sessions.count(),
198
+ }));
199
+ break;
200
+ default:
201
+ ws.send(JSON.stringify({ error: `Unknown message type: ${msg.type}` }));
202
+ }
203
+ }
204
+ getWebChatHtml() {
205
+ return `<!DOCTYPE html>
206
+ <html lang="en">
207
+ <head>
208
+ <meta charset="UTF-8">
209
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
210
+ <title>ApexBot - WebChat</title>
211
+ <style>
212
+ * { box-sizing: border-box; margin: 0; padding: 0; }
213
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; }
214
+ header { background: #16213e; padding: 1rem; display: flex; align-items: center; gap: 1rem; }
215
+ header h1 { font-size: 1.5rem; }
216
+ .status { font-size: 0.8rem; color: #4ade80; }
217
+ #messages { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
218
+ .message { max-width: 80%; padding: 0.75rem 1rem; border-radius: 1rem; }
219
+ .message.user { background: #0f3460; align-self: flex-end; }
220
+ .message.bot { background: #16213e; align-self: flex-start; }
221
+ .message .meta { font-size: 0.7rem; color: #888; margin-top: 0.25rem; }
222
+ #input-area { background: #16213e; padding: 1rem; display: flex; gap: 0.5rem; }
223
+ #input { flex: 1; padding: 0.75rem 1rem; border: none; border-radius: 1.5rem; background: #1a1a2e; color: #eee; font-size: 1rem; }
224
+ #input:focus { outline: 2px solid #4ade80; }
225
+ button { padding: 0.75rem 1.5rem; border: none; border-radius: 1.5rem; background: #4ade80; color: #000; font-weight: bold; cursor: pointer; }
226
+ button:hover { background: #22c55e; }
227
+ </style>
228
+ </head>
229
+ <body>
230
+ <header>
231
+ <h1>🤖 ApexBot</h1>
232
+ <span class="status" id="status">Connecting...</span>
233
+ </header>
234
+ <div id="messages"></div>
235
+ <div id="input-area">
236
+ <input type="text" id="input" placeholder="Type a message..." autocomplete="off">
237
+ <button onclick="send()">Send</button>
238
+ </div>
239
+ <script>
240
+ const messages = document.getElementById('messages');
241
+ const input = document.getElementById('input');
242
+ const status = document.getElementById('status');
243
+ let ws;
244
+
245
+ function connect() {
246
+ ws = new WebSocket('ws://' + location.host);
247
+ ws.onopen = () => { status.textContent = '● Connected'; status.style.color = '#4ade80'; };
248
+ ws.onclose = () => { status.textContent = '● Disconnected'; status.style.color = '#f87171'; setTimeout(connect, 3000); };
249
+ ws.onmessage = (e) => {
250
+ const msg = JSON.parse(e.data);
251
+ if (msg.type === 'chat' || msg.type === 'response') {
252
+ addMessage(msg.text, 'bot');
253
+ }
254
+ };
255
+ }
256
+
257
+ function addMessage(text, type) {
258
+ const div = document.createElement('div');
259
+ div.className = 'message ' + type;
260
+ div.innerHTML = text + '<div class="meta">' + new Date().toLocaleTimeString() + '</div>';
261
+ messages.appendChild(div);
262
+ messages.scrollTop = messages.scrollHeight;
263
+ }
264
+
265
+ function send() {
266
+ const text = input.value.trim();
267
+ if (!text) return;
268
+ addMessage(text, 'user');
269
+ ws.send(JSON.stringify({ type: 'chat', text, id: Date.now().toString() }));
270
+ input.value = '';
271
+ }
272
+
273
+ input.addEventListener('keypress', (e) => { if (e.key === 'Enter') send(); });
274
+ connect();
275
+ </script>
276
+ </body>
277
+ </html>`;
278
+ }
279
+ async stop() {
280
+ await this.channels.disconnectAll();
281
+ for (const [id, ws] of this.clients) {
282
+ ws.close();
283
+ }
284
+ this.clients.clear();
285
+ if (this.wss) {
286
+ this.wss.close();
287
+ }
288
+ if (this.server) {
289
+ this.server.close();
290
+ }
291
+ console.log('Gateway stopped');
292
+ }
293
+ broadcast(message) {
294
+ const data = JSON.stringify(message);
295
+ for (const ws of this.clients.values()) {
296
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
297
+ ws.send(data);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ exports.Gateway = Gateway;
303
+ exports.default = Gateway;
package/dist/index.js ADDED
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ require("dotenv/config");
7
+ const decisionEngine_1 = __importDefault(require("./decisionEngine"));
8
+ const manifold_1 = __importDefault(require("./adapters/manifold"));
9
+ const telegram_1 = __importDefault(require("./adapters/telegram"));
10
+ const arbitrage_1 = __importDefault(require("./strategy/arbitrage"));
11
+ const llmChecker_1 = __importDefault(require("./safety/llmChecker"));
12
+ const eventBus_1 = require("./eventBus");
13
+ // ─────────────────────────────────────────────────────────────────
14
+ // Configuration (use env vars in production)
15
+ // ─────────────────────────────────────────────────────────────────
16
+ const config = {
17
+ // Manifold API
18
+ manifold: {
19
+ apiKey: process.env.MANIFOLD_API_KEY || '',
20
+ },
21
+ // Telegram Bot
22
+ telegram: {
23
+ botToken: process.env.TELEGRAM_BOT_TOKEN || '',
24
+ allowedUserIds: (process.env.TELEGRAM_ALLOWED_USERS || '')
25
+ .split(',')
26
+ .map((s) => parseInt(s.trim(), 10))
27
+ .filter((n) => !isNaN(n)),
28
+ },
29
+ // Decision Engine
30
+ engine: {
31
+ bankroll: parseFloat(process.env.STARTING_BANKROLL || '1000'),
32
+ riskMultiplier: 0.2,
33
+ minStake: 1,
34
+ maxStake: 200,
35
+ rateLimitCapacity: 20,
36
+ rateLimitRatePerSec: 10,
37
+ failsafe: {
38
+ maxConsecutiveLosses: 5,
39
+ maxDrawdownPct: 0.2,
40
+ },
41
+ },
42
+ // Arbitrage
43
+ arbitrage: {
44
+ minProfitThreshold: 2, // M$2 minimum expected profit
45
+ correlatedPairs: [],
46
+ },
47
+ // LLM Safety (optional - requires GOOGLE_API_KEY)
48
+ safety: {
49
+ googleApiKey: process.env.GOOGLE_API_KEY || '',
50
+ enabled: Boolean(process.env.GOOGLE_API_KEY),
51
+ },
52
+ };
53
+ // ─────────────────────────────────────────────────────────────────
54
+ // Main
55
+ // ─────────────────────────────────────────────────────────────────
56
+ async function main() {
57
+ console.log('🚀 ApexBot starting...');
58
+ // Initialize adapters
59
+ const manifold = new manifold_1.default(config.manifold);
60
+ const engine = new decisionEngine_1.default(config.engine);
61
+ let telegram = null;
62
+ let arbDetector = null;
63
+ let safetyChecker = null;
64
+ let tradingEnabled = true;
65
+ // ─────────────────────────────────────────────────────────────────
66
+ // Telegram Bot (optional)
67
+ // ─────────────────────────────────────────────────────────────────
68
+ if (config.telegram.botToken && config.telegram.allowedUserIds.length > 0) {
69
+ telegram = new telegram_1.default(config.telegram);
70
+ // Handle telegram commands
71
+ (0, eventBus_1.on)('telegram:status_request', async (req) => {
72
+ const status = `📊 *ApexBot Status*\n` +
73
+ `Bankroll: M$${engine.getBankroll().toFixed(2)}\n` +
74
+ `Trading: ${tradingEnabled ? '✅ Active' : '🛑 Stopped'}\n` +
75
+ `Risk multiplier: ${config.engine.riskMultiplier}`;
76
+ await telegram.sendTo(req.chatId, status);
77
+ });
78
+ (0, eventBus_1.on)('telegram:markets_request', async (req) => {
79
+ try {
80
+ const markets = await manifold.getMarkets(10);
81
+ const list = markets.slice(0, 10).map((m, i) => `${i + 1}. ${m.question.slice(0, 50)}... (${(m.probability * 100).toFixed(0)}%)`).join('\n');
82
+ await telegram.sendTo(req.chatId, `📈 *Top Markets*\n\n${list}`);
83
+ }
84
+ catch (e) {
85
+ await telegram.sendTo(req.chatId, '❌ Nie udało się pobrać rynków.');
86
+ }
87
+ });
88
+ (0, eventBus_1.on)('telegram:stop_trading', () => {
89
+ tradingEnabled = false;
90
+ });
91
+ (0, eventBus_1.on)('telegram:start_trading', () => {
92
+ tradingEnabled = true;
93
+ });
94
+ (0, eventBus_1.on)('telegram:set_risk', (req) => {
95
+ config.engine.riskMultiplier = req.riskMultiplier;
96
+ });
97
+ telegram.start().catch((e) => {
98
+ console.error('Telegram bot failed to start:', e);
99
+ });
100
+ console.log('🤖 Telegram bot initialized');
101
+ }
102
+ else {
103
+ console.log('⚠️ Telegram not configured (set TELEGRAM_BOT_TOKEN and TELEGRAM_ALLOWED_USERS)');
104
+ }
105
+ // ─────────────────────────────────────────────────────────────────
106
+ // Arbitrage Detector
107
+ // ─────────────────────────────────────────────────────────────────
108
+ arbDetector = new arbitrage_1.default(manifold, config.arbitrage);
109
+ console.log('🔍 Arbitrage detector initialized');
110
+ // ─────────────────────────────────────────────────────────────────
111
+ // LLM Safety Checker (optional)
112
+ // ─────────────────────────────────────────────────────────────────
113
+ if (config.safety.enabled) {
114
+ safetyChecker = new llmChecker_1.default({ googleApiKey: config.safety.googleApiKey });
115
+ console.log('🛡️ LLM safety checker initialized (Gemini)');
116
+ }
117
+ else {
118
+ console.log('⚠️ LLM safety checker not enabled (set GOOGLE_API_KEY)');
119
+ }
120
+ // ─────────────────────────────────────────────────────────────────
121
+ // Event handlers
122
+ // ─────────────────────────────────────────────────────────────────
123
+ // Log all strategy alerts
124
+ (0, eventBus_1.on)('strategy:alert', (msg) => {
125
+ console.log(`[ALERT] ${msg}`);
126
+ });
127
+ // Log arbitrage opportunities
128
+ (0, eventBus_1.on)('arb:opportunity', (arb) => {
129
+ console.log(`[ARB] ${arb.details} | Expected profit: M$${arb.expectedProfit.toFixed(2)}`);
130
+ });
131
+ // Log safety warnings
132
+ (0, eventBus_1.on)('safety:warning', (warn) => {
133
+ console.log(`[SAFETY] Market ${warn.marketId}: ${warn.reason} (score: ${warn.score})`);
134
+ });
135
+ // ─────────────────────────────────────────────────────────────────
136
+ // Main loop: periodic market scan
137
+ // ─────────────────────────────────────────────────────────────────
138
+ const scanInterval = 60_000; // 1 minute
139
+ async function scanMarkets() {
140
+ if (!tradingEnabled) {
141
+ console.log('[SCAN] Trading disabled, skipping...');
142
+ return;
143
+ }
144
+ console.log('[SCAN] Fetching markets...');
145
+ try {
146
+ const markets = await manifold.getMarkets(50);
147
+ for (const market of markets) {
148
+ if (market.isResolved)
149
+ continue;
150
+ // Quick safety check
151
+ if (safetyChecker) {
152
+ const quick = safetyChecker.quickCheck(market.question, market.description);
153
+ if (!quick.pass) {
154
+ console.log(`[SKIP] ${market.id}: ${quick.issues.join(', ')}`);
155
+ continue;
156
+ }
157
+ }
158
+ // Emit market update for decision engine
159
+ (0, eventBus_1.emit)('market:update', {
160
+ marketId: market.id,
161
+ price: market.probability,
162
+ probModel: market.probability, // TODO: replace with your own model
163
+ description: market.question,
164
+ timestamp: Date.now(),
165
+ });
166
+ }
167
+ // Check for arbitrage opportunities
168
+ if (arbDetector) {
169
+ await arbDetector.scanAllMarkets(50);
170
+ }
171
+ }
172
+ catch (e) {
173
+ console.error('[SCAN] Error:', e);
174
+ }
175
+ }
176
+ // Initial scan
177
+ await scanMarkets();
178
+ // Periodic scans
179
+ setInterval(scanMarkets, scanInterval);
180
+ // ─────────────────────────────────────────────────────────────────
181
+ // WebSocket connection (real-time updates)
182
+ // ─────────────────────────────────────────────────────────────────
183
+ if (config.manifold.apiKey) {
184
+ manifold.connectWebSocket();
185
+ console.log('📡 WebSocket connection initiated');
186
+ }
187
+ console.log('✅ ApexBot running. Press Ctrl+C to stop.');
188
+ }
189
+ // ─────────────────────────────────────────────────────────────────
190
+ // Entry point
191
+ // ─────────────────────────────────────────────────────────────────
192
+ main().catch((e) => {
193
+ console.error('Fatal error:', e);
194
+ process.exit(1);
195
+ });
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.expectedValueForBuy = expectedValueForBuy;
4
+ exports.expectedValueForSell = expectedValueForSell;
5
+ /**
6
+ * Compute Expected Value (EV) per $1 invested when buying a binary share at price `price`.
7
+ * - price: market price (0..1) you can buy a $1-paying share for
8
+ * - prob: your estimated true probability of the event (0..1)
9
+ * EV = prob * (1 - price) + (1 - prob) * (-price) = prob - price
10
+ * Returns EV in dollars per $1 invested (positive => edge)
11
+ */
12
+ function expectedValueForBuy(price, prob) {
13
+ return prob - price;
14
+ }
15
+ /**
16
+ * EV when selling (shorting) a share: selling (i.e. laying) receives price now and pays $1 on event
17
+ * For a $1 payout on event, if you sell at price `price`, EV = price - prob
18
+ */
19
+ function expectedValueForSell(price, prob) {
20
+ return price - prob;
21
+ }
22
+ exports.default = {
23
+ expectedValueForBuy,
24
+ expectedValueForSell,
25
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.kellyFraction = kellyFraction;
4
+ /**
5
+ * Kelly fraction for binary share markets where buying a share costs `price` and pays $1 on win.
6
+ * We model the bet as: stake S (dollars) -> on win profit = S*(1/price - 1); on loss = -S
7
+ * b = (1/price - 1) (net odds per dollar staked). q = estimated win probability.
8
+ * Standard Kelly: f* = (b*q - (1-q)) / b
9
+ * Returns a fraction of bankroll recommended by (full) Kelly; caller should apply a risk multiplier <1.
10
+ */
11
+ function kellyFraction(price, prob) {
12
+ if (price <= 0 || price >= 1)
13
+ return 0;
14
+ const b = (1 / price - 1);
15
+ const q = prob;
16
+ const numerator = b * q - (1 - q);
17
+ const f = numerator / b;
18
+ if (!isFinite(f))
19
+ return 0;
20
+ return Math.max(0, f);
21
+ }
22
+ exports.default = {
23
+ kellyFraction,
24
+ };
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LeakyBucket = void 0;
4
+ /**
5
+ * Simple Leaky Bucket rate limiter for API calls.
6
+ * capacity: max tokens
7
+ * leakRate: tokens per second replenished (capacity units per sec)
8
+ */
9
+ class LeakyBucket {
10
+ capacity;
11
+ tokens;
12
+ leakRate; // tokens per second
13
+ last;
14
+ constructor(capacity, leakRate) {
15
+ this.capacity = capacity;
16
+ this.tokens = capacity;
17
+ this.leakRate = leakRate;
18
+ this.last = Date.now() / 1000;
19
+ }
20
+ refill() {
21
+ const now = Date.now() / 1000;
22
+ const delta = now - this.last;
23
+ this.last = now;
24
+ this.tokens = Math.min(this.capacity, this.tokens + delta * this.leakRate);
25
+ }
26
+ tryRemove(cost = 1) {
27
+ this.refill();
28
+ if (this.tokens >= cost) {
29
+ this.tokens -= cost;
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+ timeUntil(cost = 1) {
35
+ this.refill();
36
+ if (this.tokens >= cost)
37
+ return 0;
38
+ return (cost - this.tokens) / this.leakRate;
39
+ }
40
+ }
41
+ exports.LeakyBucket = LeakyBucket;
42
+ exports.default = LeakyBucket;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FailSafe = void 0;
4
+ class FailSafe {
5
+ cfg;
6
+ consecutiveLosses = 0;
7
+ peakBankroll = 0;
8
+ constructor(cfg, startingBankroll) {
9
+ this.cfg = cfg;
10
+ this.peakBankroll = startingBankroll;
11
+ }
12
+ onTradeResult(bankroll, pnl) {
13
+ if (pnl < 0)
14
+ this.consecutiveLosses += 1;
15
+ else
16
+ this.consecutiveLosses = 0;
17
+ if (bankroll > this.peakBankroll)
18
+ this.peakBankroll = bankroll;
19
+ }
20
+ shouldStop(bankroll) {
21
+ if (this.consecutiveLosses >= this.cfg.maxConsecutiveLosses) {
22
+ return { stop: true, reason: 'max_consecutive_losses' };
23
+ }
24
+ const drawdown = (this.peakBankroll - bankroll) / Math.max(1, this.peakBankroll);
25
+ if (drawdown >= this.cfg.maxDrawdownPct) {
26
+ return { stop: true, reason: 'max_drawdown' };
27
+ }
28
+ return { stop: false };
29
+ }
30
+ }
31
+ exports.FailSafe = FailSafe;
32
+ exports.default = FailSafe;