@trading-boy/cli 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +5 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +37 -3
- package/dist/api-client.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/audit.d.ts +0 -15
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +17 -79
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/behavioral.d.ts +34 -1
- package/dist/commands/behavioral.d.ts.map +1 -1
- package/dist/commands/behavioral.js +56 -76
- package/dist/commands/behavioral.js.map +1 -1
- package/dist/commands/benchmark-cmd.js +2 -2
- package/dist/commands/benchmark-cmd.js.map +1 -1
- package/dist/commands/billing.d.ts.map +1 -1
- package/dist/commands/billing.js +15 -10
- package/dist/commands/billing.js.map +1 -1
- package/dist/commands/catalysts.d.ts +10 -1
- package/dist/commands/catalysts.d.ts.map +1 -1
- package/dist/commands/catalysts.js +15 -84
- package/dist/commands/catalysts.js.map +1 -1
- package/dist/commands/coaching-cmd.d.ts.map +1 -1
- package/dist/commands/coaching-cmd.js +21 -10
- package/dist/commands/coaching-cmd.js.map +1 -1
- package/dist/commands/config-cmd.d.ts.map +1 -1
- package/dist/commands/config-cmd.js +35 -19
- package/dist/commands/config-cmd.js.map +1 -1
- package/dist/commands/context.d.ts +0 -7
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +47 -199
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/decisions.d.ts +38 -2
- package/dist/commands/decisions.d.ts.map +1 -1
- package/dist/commands/decisions.js +35 -134
- package/dist/commands/decisions.js.map +1 -1
- package/dist/commands/edge-cmd.d.ts +55 -45
- package/dist/commands/edge-cmd.d.ts.map +1 -1
- package/dist/commands/edge-cmd.js +76 -32
- package/dist/commands/edge-cmd.js.map +1 -1
- package/dist/commands/edge-guard-cmd.d.ts.map +1 -1
- package/dist/commands/edge-guard-cmd.js +7 -3
- package/dist/commands/edge-guard-cmd.js.map +1 -1
- package/dist/commands/events.d.ts.map +1 -1
- package/dist/commands/events.js +78 -162
- package/dist/commands/events.js.map +1 -1
- package/dist/commands/infra.d.ts +2 -4
- package/dist/commands/infra.d.ts.map +1 -1
- package/dist/commands/infra.js +65 -163
- package/dist/commands/infra.js.map +1 -1
- package/dist/commands/journal.d.ts.map +1 -1
- package/dist/commands/journal.js +138 -597
- package/dist/commands/journal.js.map +1 -1
- package/dist/commands/login.js +3 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/narratives.d.ts.map +1 -1
- package/dist/commands/narratives.js +90 -311
- package/dist/commands/narratives.js.map +1 -1
- package/dist/commands/query.d.ts +0 -7
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +35 -157
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/replay-cmd.d.ts.map +1 -1
- package/dist/commands/replay-cmd.js +6 -8
- package/dist/commands/replay-cmd.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +45 -83
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/risk.d.ts +29 -1
- package/dist/commands/risk.d.ts.map +1 -1
- package/dist/commands/risk.js +20 -89
- package/dist/commands/risk.js.map +1 -1
- package/dist/commands/social.d.ts +36 -2
- package/dist/commands/social.d.ts.map +1 -1
- package/dist/commands/social.js +74 -148
- package/dist/commands/social.js.map +1 -1
- package/dist/commands/strategy-cmd.d.ts.map +1 -1
- package/dist/commands/strategy-cmd.js +2 -4
- package/dist/commands/strategy-cmd.js.map +1 -1
- package/dist/commands/subscribe.js +4 -4
- package/dist/commands/subscribe.js.map +1 -1
- package/dist/commands/suggestions-cmd.d.ts +24 -0
- package/dist/commands/suggestions-cmd.d.ts.map +1 -0
- package/dist/commands/suggestions-cmd.js +142 -0
- package/dist/commands/suggestions-cmd.js.map +1 -0
- package/dist/commands/thesis-cmd.js +4 -4
- package/dist/commands/thesis-cmd.js.map +1 -1
- package/dist/commands/trader.d.ts +18 -1
- package/dist/commands/trader.d.ts.map +1 -1
- package/dist/commands/trader.js +105 -225
- package/dist/commands/trader.js.map +1 -1
- package/dist/commands/watch.d.ts +0 -10
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +5 -66
- package/dist/commands/watch.js.map +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +18 -2
- package/dist/commands/whoami.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +45 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +97 -0
- package/dist/utils.js.map +1 -1
- package/package.json +5 -10
package/dist/commands/journal.js
CHANGED
|
@@ -1,43 +1,9 @@
|
|
|
1
1
|
import { Option } from 'commander';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import { getConfig, createLogger, DecisionType, ActorType, PlanStatus, } from '@trading-boy/core';
|
|
5
|
-
import { Neo4jClient, createDecisionEvent, linkDecisionToToken, linkDecisionToTrader, linkEntryToExit, getDecisionChain, publishDecision, } from '@trading-boy/graph-db';
|
|
6
|
-
import { formatConnectionError } from '../utils.js';
|
|
7
|
-
import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
|
|
8
|
-
import { buildPreState, buildDefaultPreState, createPlan, getPlan, executePlan, computeKellySizing, extractSetupStats, getStatsBySetupType, } from '@trading-boy/context-engine';
|
|
9
|
-
import { TimescaleClient } from '@trading-boy/timeseries-db';
|
|
2
|
+
import { padRight } from '../utils.js';
|
|
3
|
+
import { apiRequest, ApiError } from '../api-client.js';
|
|
10
4
|
import { registerReviewCommand } from './review.js';
|
|
11
|
-
// ─── Logger ───
|
|
12
|
-
const logger = createLogger('cli-journal');
|
|
13
5
|
// ─── Default Trader ───
|
|
14
6
|
const DEFAULT_TRADER_ID = 'default';
|
|
15
|
-
// ─── Neo4j Client Factory ───
|
|
16
|
-
function createNeo4jClient() {
|
|
17
|
-
const config = getConfig();
|
|
18
|
-
return new Neo4jClient({
|
|
19
|
-
uri: config.NEO4J_URI,
|
|
20
|
-
username: config.NEO4J_USER,
|
|
21
|
-
password: config.NEO4J_PASSWORD,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
function createRedisClient() {
|
|
25
|
-
const config = getConfig();
|
|
26
|
-
return new Redis(config.REDIS_URL, {
|
|
27
|
-
password: config.REDIS_PASSWORD || undefined,
|
|
28
|
-
lazyConnect: true,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
function createTimescaleClient() {
|
|
32
|
-
const config = getConfig();
|
|
33
|
-
return new TimescaleClient({
|
|
34
|
-
host: config.TIMESCALE_HOST,
|
|
35
|
-
port: config.TIMESCALE_PORT,
|
|
36
|
-
user: config.TIMESCALE_USER,
|
|
37
|
-
password: config.TIMESCALE_PASSWORD,
|
|
38
|
-
database: config.TIMESCALE_DB,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
7
|
// ─── Command Registration ───
|
|
42
8
|
export function registerJournalCommand(program) {
|
|
43
9
|
const journal = program
|
|
@@ -68,6 +34,7 @@ export function registerJournalCommand(program) {
|
|
|
68
34
|
.option('--tx-hash <txHash>', 'Solana transaction signature')
|
|
69
35
|
.option('--triggers <triggers...>', 'What initiated the trade')
|
|
70
36
|
.option('--plan <planId>', 'Link to a pre-trade plan (auto-fills fields)')
|
|
37
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
71
38
|
.action(async (symbol, options) => {
|
|
72
39
|
// ─── Input validation ───
|
|
73
40
|
if (options.direction !== undefined && !['LONG', 'SHORT'].includes(options.direction.toUpperCase())) {
|
|
@@ -85,144 +52,41 @@ export function registerJournalCommand(program) {
|
|
|
85
52
|
process.exitCode = 1;
|
|
86
53
|
return;
|
|
87
54
|
}
|
|
88
|
-
// ─── Remote API mode ───
|
|
89
|
-
if (await isRemoteMode()) {
|
|
90
|
-
try {
|
|
91
|
-
const result = await apiRequest('/api/v1/decisions', {
|
|
92
|
-
method: 'POST',
|
|
93
|
-
body: {
|
|
94
|
-
traderId: options.trader,
|
|
95
|
-
decisionType: 'TRADE_ENTRY',
|
|
96
|
-
tokenSymbol: symbol.toUpperCase(),
|
|
97
|
-
...(options.mint ? { tokenMint: options.mint } : {}),
|
|
98
|
-
direction: options.direction?.toUpperCase(),
|
|
99
|
-
entryPrice: options.price,
|
|
100
|
-
size: options.size,
|
|
101
|
-
sizeUsd: options.sizeUsd ?? (options.size && options.price ? options.size * options.price : undefined),
|
|
102
|
-
thesis: options.thesis,
|
|
103
|
-
setupType: options.setup,
|
|
104
|
-
emotionalTag: options.emotion,
|
|
105
|
-
confidence: options.confidence,
|
|
106
|
-
triggers: options.triggers,
|
|
107
|
-
txHash: options.txHash,
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
console.log(`Entry logged: id=${result.id} hash=${result.hash}`);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
const message = error instanceof ApiError ? error.message : (error instanceof Error ? error.message : String(error));
|
|
114
|
-
console.error(`Error: ${message}`);
|
|
115
|
-
if (error instanceof ApiError && error.body && typeof error.body === 'object') {
|
|
116
|
-
const detail = error.body.error ?? error.body.code;
|
|
117
|
-
if (detail)
|
|
118
|
-
console.error(`Detail: ${detail}`);
|
|
119
|
-
}
|
|
120
|
-
process.exitCode = 1;
|
|
121
|
-
}
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
const client = createNeo4jClient();
|
|
125
|
-
const redis = createRedisClient();
|
|
126
|
-
const tsClient = createTimescaleClient();
|
|
127
55
|
try {
|
|
128
|
-
await
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
logger.info({ planId: options.plan }, 'Auto-filling entry from plan');
|
|
147
|
-
}
|
|
148
|
-
// Build real pre-state (fallback to defaults on failure)
|
|
149
|
-
let preState;
|
|
150
|
-
try {
|
|
151
|
-
preState = await buildPreState({ tsClient, redis }, tokenSymbol);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
preState = buildDefaultPreState();
|
|
155
|
-
}
|
|
156
|
-
const eventTime = new Date();
|
|
157
|
-
const input = {
|
|
158
|
-
traderId: options.trader,
|
|
159
|
-
decisionType: DecisionType.TRADE_ENTRY,
|
|
160
|
-
actor: ActorType.HUMAN,
|
|
161
|
-
tokenSymbol,
|
|
162
|
-
...(options.mint ? { tokenMint: options.mint } : {}),
|
|
163
|
-
eventTime,
|
|
164
|
-
preState,
|
|
165
|
-
thesis: options.thesis ?? linkedPlan?.thesis,
|
|
166
|
-
setupType: (options.setup ?? linkedPlan?.setupType),
|
|
167
|
-
direction: (options.direction ?? linkedPlan?.direction),
|
|
168
|
-
entryPrice: options.price ?? linkedPlan?.entryTarget,
|
|
169
|
-
size: options.size,
|
|
170
|
-
sizeUsd: options.sizeUsd ?? (() => {
|
|
171
|
-
const ep = options.price ?? linkedPlan?.entryTarget;
|
|
172
|
-
return options.size && ep ? options.size * ep : undefined;
|
|
173
|
-
})(),
|
|
174
|
-
emotionalTag: (options.emotion ?? linkedPlan?.emotionalTag),
|
|
175
|
-
confidence: options.confidence ?? linkedPlan?.confidence,
|
|
176
|
-
triggers: options.triggers,
|
|
177
|
-
txHash: options.txHash,
|
|
178
|
-
};
|
|
179
|
-
const { id, hash } = await createDecisionEvent(client, input);
|
|
180
|
-
await linkDecisionToToken(client, id, tokenSymbol);
|
|
181
|
-
await linkDecisionToTrader(client, id, options.trader);
|
|
182
|
-
// Mark plan as executed
|
|
183
|
-
if (linkedPlan) {
|
|
184
|
-
await executePlan(tsClient, linkedPlan.id, id);
|
|
185
|
-
logger.info({ planId: linkedPlan.id, decisionId: id }, 'Plan executed');
|
|
186
|
-
}
|
|
187
|
-
// Publish to stream (fire-and-forget)
|
|
188
|
-
publishDecision(redis, {
|
|
189
|
-
decisionId: id,
|
|
190
|
-
traderId: options.trader,
|
|
191
|
-
decisionType: DecisionType.TRADE_ENTRY,
|
|
192
|
-
actor: ActorType.HUMAN,
|
|
193
|
-
tokenSymbol,
|
|
194
|
-
...(options.mint ? { tokenMint: options.mint } : {}),
|
|
195
|
-
eventTime: eventTime.toISOString(),
|
|
196
|
-
preState,
|
|
197
|
-
thesis: options.thesis,
|
|
198
|
-
setupType: options.setup,
|
|
199
|
-
emotionalTag: options.emotion,
|
|
200
|
-
confidence: options.confidence,
|
|
201
|
-
direction: options.direction,
|
|
202
|
-
entryPrice: options.price,
|
|
203
|
-
size: options.size,
|
|
204
|
-
sizeUsd: options.sizeUsd ?? (options.size && options.price ? options.size * options.price : undefined),
|
|
205
|
-
txHash: options.txHash,
|
|
206
|
-
triggers: options.triggers,
|
|
207
|
-
}).catch((error) => {
|
|
208
|
-
logger.warn({ decisionId: id, error: error instanceof Error ? error.message : String(error) }, 'Failed to publish entry to stream (non-fatal)');
|
|
56
|
+
const result = await apiRequest('/api/v1/decisions', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body: {
|
|
59
|
+
traderId: options.trader,
|
|
60
|
+
decisionType: 'TRADE_ENTRY',
|
|
61
|
+
tokenSymbol: symbol.toUpperCase(),
|
|
62
|
+
...(options.mint ? { tokenMint: options.mint } : {}),
|
|
63
|
+
direction: options.direction?.toUpperCase(),
|
|
64
|
+
entryPrice: options.price,
|
|
65
|
+
size: options.size,
|
|
66
|
+
sizeUsd: options.sizeUsd ?? (options.size && options.price ? options.size * options.price : undefined),
|
|
67
|
+
thesis: options.thesis,
|
|
68
|
+
setupType: options.setup,
|
|
69
|
+
emotionalTag: options.emotion,
|
|
70
|
+
confidence: options.confidence,
|
|
71
|
+
triggers: options.triggers,
|
|
72
|
+
txHash: options.txHash,
|
|
73
|
+
},
|
|
209
74
|
});
|
|
210
|
-
|
|
211
|
-
|
|
75
|
+
if (options.format === 'json') {
|
|
76
|
+
console.log(JSON.stringify(result, null, 2));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log(`Entry logged: id=${result.id} hash=${result.hash}`);
|
|
212
80
|
}
|
|
213
81
|
catch (error) {
|
|
214
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
-
logger.error({ error: message }, 'Failed to log entry');
|
|
82
|
+
const message = error instanceof ApiError ? error.message : (error instanceof Error ? error.message : String(error));
|
|
216
83
|
console.error(`Error: ${message}`);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await redis.quit().catch(() => { });
|
|
224
|
-
await tsClient.disconnect().catch(() => { });
|
|
225
|
-
await client.disconnect();
|
|
84
|
+
if (error instanceof ApiError && error.body && typeof error.body === 'object') {
|
|
85
|
+
const detail = error.body.error ?? error.body.code;
|
|
86
|
+
if (detail)
|
|
87
|
+
console.error(`Detail: ${detail}`);
|
|
88
|
+
}
|
|
89
|
+
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
226
90
|
}
|
|
227
91
|
});
|
|
228
92
|
// ─── journal log exit <symbol> ───
|
|
@@ -237,133 +101,39 @@ export function registerJournalCommand(program) {
|
|
|
237
101
|
.option('--thesis <thesis>', 'Exit reasoning')
|
|
238
102
|
.option('--size <size>', 'Position size in token units', parseFloat)
|
|
239
103
|
.option('--size-usd <sizeUsd>', 'Position size in USD', parseFloat)
|
|
104
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
240
105
|
.action(async (symbol, options) => {
|
|
241
|
-
// ─── Remote API mode ───
|
|
242
|
-
if (await isRemoteMode()) {
|
|
243
|
-
try {
|
|
244
|
-
const result = await apiRequest('/api/v1/decisions', {
|
|
245
|
-
method: 'POST',
|
|
246
|
-
body: {
|
|
247
|
-
traderId: options.trader,
|
|
248
|
-
decisionType: 'TRADE_EXIT',
|
|
249
|
-
tokenSymbol: symbol.toUpperCase(),
|
|
250
|
-
...(options.mint ? { tokenMint: options.mint } : {}),
|
|
251
|
-
exitPrice: options.price,
|
|
252
|
-
thesis: options.thesis,
|
|
253
|
-
size: options.size,
|
|
254
|
-
sizeUsd: options.sizeUsd ?? (options.size && options.price ? options.size * options.price : undefined),
|
|
255
|
-
txHash: options.txHash,
|
|
256
|
-
linkedEntryId: options.linkEntry,
|
|
257
|
-
},
|
|
258
|
-
});
|
|
259
|
-
console.log(`Exit logged: id=${result.id} hash=${result.hash}`);
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
const message = error instanceof ApiError ? error.message : (error instanceof Error ? error.message : String(error));
|
|
263
|
-
console.error(`Error: ${message}`);
|
|
264
|
-
if (error instanceof ApiError && error.body && typeof error.body === 'object') {
|
|
265
|
-
const detail = error.body.error ?? error.body.code;
|
|
266
|
-
if (detail)
|
|
267
|
-
console.error(`Detail: ${detail}`);
|
|
268
|
-
}
|
|
269
|
-
process.exitCode = 1;
|
|
270
|
-
}
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
const client = createNeo4jClient();
|
|
274
|
-
const redis = createRedisClient();
|
|
275
|
-
const tsClient = createTimescaleClient();
|
|
276
106
|
try {
|
|
277
|
-
await
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
let entryRecord;
|
|
292
|
-
if (options.linkEntry) {
|
|
293
|
-
const chain = await getDecisionChain(client, options.trader, { tokenSymbol });
|
|
294
|
-
entryRecord = chain.find((d) => d.id === options.linkEntry);
|
|
295
|
-
}
|
|
296
|
-
const input = {
|
|
297
|
-
traderId: options.trader,
|
|
298
|
-
decisionType: DecisionType.TRADE_EXIT,
|
|
299
|
-
actor: ActorType.HUMAN,
|
|
300
|
-
tokenSymbol,
|
|
301
|
-
...(options.mint ? { tokenMint: options.mint } : {}),
|
|
302
|
-
eventTime,
|
|
303
|
-
preState,
|
|
304
|
-
exitPrice: options.price,
|
|
305
|
-
thesis: options.thesis,
|
|
306
|
-
size: options.size,
|
|
307
|
-
sizeUsd: options.sizeUsd ?? (options.size && options.price ? options.size * options.price : undefined),
|
|
308
|
-
txHash: options.txHash,
|
|
309
|
-
linkedEntryId: options.linkEntry,
|
|
310
|
-
};
|
|
311
|
-
const { id, hash } = await createDecisionEvent(client, input);
|
|
312
|
-
await linkDecisionToToken(client, id, tokenSymbol);
|
|
313
|
-
await linkDecisionToTrader(client, id, options.trader);
|
|
314
|
-
// Link entry to exit via FOLLOWED_BY edge
|
|
315
|
-
if (options.linkEntry) {
|
|
316
|
-
await linkEntryToExit(client, options.linkEntry, id);
|
|
317
|
-
logger.debug({ entryId: options.linkEntry, exitId: id }, 'Linked entry to exit');
|
|
318
|
-
}
|
|
319
|
-
// Publish to stream (fire-and-forget)
|
|
320
|
-
const publishInput = {
|
|
321
|
-
decisionId: id,
|
|
322
|
-
traderId: options.trader,
|
|
323
|
-
decisionType: DecisionType.TRADE_EXIT,
|
|
324
|
-
actor: ActorType.HUMAN,
|
|
325
|
-
tokenSymbol,
|
|
326
|
-
...(options.mint ? { tokenMint: options.mint } : {}),
|
|
327
|
-
eventTime: eventTime.toISOString(),
|
|
328
|
-
preState,
|
|
329
|
-
exitPrice: options.price,
|
|
330
|
-
thesis: options.thesis,
|
|
331
|
-
size: options.size,
|
|
332
|
-
sizeUsd: options.sizeUsd ?? (options.size && options.price ? options.size * options.price : undefined),
|
|
333
|
-
txHash: options.txHash,
|
|
334
|
-
linkedEntryId: options.linkEntry,
|
|
335
|
-
};
|
|
336
|
-
// Embed entry fields for PnL computation
|
|
337
|
-
if (entryRecord) {
|
|
338
|
-
publishInput.entryId = entryRecord.id;
|
|
339
|
-
publishInput.entryTokenMint = entryRecord.tokenMint;
|
|
340
|
-
publishInput.entryDirection = entryRecord.direction ?? undefined;
|
|
341
|
-
publishInput.entryEntryPrice = entryRecord.entryPrice ?? undefined;
|
|
342
|
-
publishInput.entrySizeUsd = entryRecord.sizeUsd ?? undefined;
|
|
343
|
-
publishInput.entryEventTime = entryRecord.eventTime;
|
|
344
|
-
publishInput.entryTraderId = options.trader;
|
|
345
|
-
publishInput.entrySetupType = entryRecord.setupType ?? undefined;
|
|
346
|
-
publishInput.entryEmotionalTag = entryRecord.emotionalTag ?? undefined;
|
|
347
|
-
}
|
|
348
|
-
publishDecision(redis, publishInput).catch((error) => {
|
|
349
|
-
logger.warn({ decisionId: id, error: error instanceof Error ? error.message : String(error) }, 'Failed to publish exit to stream (non-fatal)');
|
|
107
|
+
const result = await apiRequest('/api/v1/decisions', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: {
|
|
110
|
+
traderId: options.trader,
|
|
111
|
+
decisionType: 'TRADE_EXIT',
|
|
112
|
+
tokenSymbol: symbol.toUpperCase(),
|
|
113
|
+
...(options.mint ? { tokenMint: options.mint } : {}),
|
|
114
|
+
exitPrice: options.price,
|
|
115
|
+
thesis: options.thesis,
|
|
116
|
+
size: options.size,
|
|
117
|
+
sizeUsd: options.sizeUsd ?? (options.size && options.price ? options.size * options.price : undefined),
|
|
118
|
+
txHash: options.txHash,
|
|
119
|
+
linkedEntryId: options.linkEntry,
|
|
120
|
+
},
|
|
350
121
|
});
|
|
351
|
-
|
|
352
|
-
|
|
122
|
+
if (options.format === 'json') {
|
|
123
|
+
console.log(JSON.stringify(result, null, 2));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
console.log(`Exit logged: id=${result.id} hash=${result.hash}`);
|
|
353
127
|
}
|
|
354
128
|
catch (error) {
|
|
355
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
356
|
-
logger.error({ error: message }, 'Failed to log exit');
|
|
129
|
+
const message = error instanceof ApiError ? error.message : (error instanceof Error ? error.message : String(error));
|
|
357
130
|
console.error(`Error: ${message}`);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
await redis.quit().catch(() => { });
|
|
365
|
-
await tsClient.disconnect().catch(() => { });
|
|
366
|
-
await client.disconnect();
|
|
131
|
+
if (error instanceof ApiError && error.body && typeof error.body === 'object') {
|
|
132
|
+
const detail = error.body.error ?? error.body.code;
|
|
133
|
+
if (detail)
|
|
134
|
+
console.error(`Detail: ${detail}`);
|
|
135
|
+
}
|
|
136
|
+
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
367
137
|
}
|
|
368
138
|
});
|
|
369
139
|
// ─── journal list ───
|
|
@@ -373,68 +143,30 @@ export function registerJournalCommand(program) {
|
|
|
373
143
|
.option('--token <symbol>', 'Filter by token symbol')
|
|
374
144
|
.option('--limit <n>', 'Maximum number of results', parseInt, 10)
|
|
375
145
|
.option('--trader <traderId>', 'Trader ID', DEFAULT_TRADER_ID)
|
|
146
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
376
147
|
.action(async (options) => {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
console.log('No decisions found.');
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
// Print formatted table
|
|
395
|
-
console.log('');
|
|
396
|
-
console.log(padRight('ID', 38) +
|
|
397
|
-
padRight('Type', 20) +
|
|
398
|
-
padRight('Token', 8) +
|
|
399
|
-
padRight('Direction', 10) +
|
|
400
|
-
padRight('Price', 12) +
|
|
401
|
-
padRight('Confidence', 12) +
|
|
402
|
-
padRight('Hash', 10));
|
|
403
|
-
console.log('-'.repeat(110));
|
|
404
|
-
for (const d of decisions) {
|
|
405
|
-
const price = (d.entryPrice ?? d.exitPrice);
|
|
406
|
-
console.log(padRight(String(d.id ?? ''), 38) +
|
|
407
|
-
padRight(String(d.decisionType ?? ''), 20) +
|
|
408
|
-
padRight(String(d.tokenSymbol ?? ''), 8) +
|
|
409
|
-
padRight(String(d.direction ?? '-'), 10) +
|
|
410
|
-
padRight(price !== null && price !== undefined ? Number(price).toFixed(2) : '-', 12) +
|
|
411
|
-
padRight(d.confidence !== null && d.confidence !== undefined ? Number(d.confidence).toFixed(2) : '-', 12) +
|
|
412
|
-
(d.hash ? String(d.hash).slice(0, 8) + '...' : '-'));
|
|
148
|
+
try {
|
|
149
|
+
const params = new URLSearchParams();
|
|
150
|
+
if (options.trader)
|
|
151
|
+
params.set('traderId', options.trader);
|
|
152
|
+
if (options.token)
|
|
153
|
+
params.set('tokenSymbol', options.token.toUpperCase());
|
|
154
|
+
if (options.limit)
|
|
155
|
+
params.set('limit', String(options.limit));
|
|
156
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
157
|
+
const result = await apiRequest(`/api/v1/decisions${qs}`);
|
|
158
|
+
const decisions = result.decisions ?? [];
|
|
159
|
+
if (decisions.length === 0) {
|
|
160
|
+
if (options.format === 'json') {
|
|
161
|
+
console.log(JSON.stringify({ decisions: [] }, null, 2));
|
|
413
162
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
catch (error) {
|
|
418
|
-
const message = error instanceof ApiError ? error.message : (error instanceof Error ? error.message : String(error));
|
|
419
|
-
console.error(`Error: ${message}`);
|
|
420
|
-
if (error instanceof ApiError && error.body && typeof error.body === 'object') {
|
|
421
|
-
const detail = error.body.error ?? error.body.code;
|
|
422
|
-
if (detail)
|
|
423
|
-
console.error(`Detail: ${detail}`);
|
|
163
|
+
else {
|
|
164
|
+
console.log('No decisions found.');
|
|
424
165
|
}
|
|
425
|
-
|
|
166
|
+
return;
|
|
426
167
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const client = createNeo4jClient();
|
|
430
|
-
try {
|
|
431
|
-
await client.connect();
|
|
432
|
-
const decisions = await getDecisionChain(client, options.trader, {
|
|
433
|
-
tokenSymbol: options.token,
|
|
434
|
-
limit: options.limit,
|
|
435
|
-
});
|
|
436
|
-
if (decisions.length === 0) {
|
|
437
|
-
console.log('No decisions found.');
|
|
168
|
+
if (options.format === 'json') {
|
|
169
|
+
console.log(JSON.stringify(result, null, 2));
|
|
438
170
|
return;
|
|
439
171
|
}
|
|
440
172
|
// Print formatted table
|
|
@@ -448,293 +180,102 @@ export function registerJournalCommand(program) {
|
|
|
448
180
|
padRight('Hash', 10));
|
|
449
181
|
console.log('-'.repeat(110));
|
|
450
182
|
for (const d of decisions) {
|
|
451
|
-
const price = d.entryPrice ?? d.exitPrice;
|
|
452
|
-
console.log(padRight(d.id, 38) +
|
|
453
|
-
padRight(d.decisionType, 20) +
|
|
454
|
-
padRight(d.tokenSymbol, 8) +
|
|
455
|
-
padRight(d.direction ?? '-', 10) +
|
|
456
|
-
padRight(price !== null ? price.toFixed(2) : '-', 12) +
|
|
457
|
-
padRight(d.confidence.toFixed(2), 12) +
|
|
458
|
-
(d.hash ? d.hash.slice(0, 8) + '...' : '-'));
|
|
183
|
+
const price = (d.entryPrice ?? d.exitPrice);
|
|
184
|
+
console.log(padRight(String(d.id ?? ''), 38) +
|
|
185
|
+
padRight(String(d.decisionType ?? ''), 20) +
|
|
186
|
+
padRight(String(d.tokenSymbol ?? ''), 8) +
|
|
187
|
+
padRight(String(d.direction ?? '-'), 10) +
|
|
188
|
+
padRight(price !== null && price !== undefined ? Number(price).toFixed(2) : '-', 12) +
|
|
189
|
+
padRight(d.confidence !== null && d.confidence !== undefined ? Number(d.confidence).toFixed(2) : '-', 12) +
|
|
190
|
+
(d.hash ? String(d.hash).slice(0, 8) + '...' : '-'));
|
|
459
191
|
}
|
|
460
192
|
console.log('');
|
|
461
193
|
console.log(`Total: ${decisions.length} decision(s)`);
|
|
462
194
|
}
|
|
463
195
|
catch (error) {
|
|
464
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
465
|
-
logger.error({ error: message }, 'Failed to list decisions');
|
|
196
|
+
const message = error instanceof ApiError ? error.message : (error instanceof Error ? error.message : String(error));
|
|
466
197
|
console.error(`Error: ${message}`);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
await client.disconnect();
|
|
198
|
+
if (error instanceof ApiError && error.body && typeof error.body === 'object') {
|
|
199
|
+
const detail = error.body.error ?? error.body.code;
|
|
200
|
+
if (detail)
|
|
201
|
+
console.error(`Detail: ${detail}`);
|
|
202
|
+
}
|
|
203
|
+
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
474
204
|
}
|
|
475
205
|
});
|
|
476
|
-
// ─── journal plan
|
|
206
|
+
// ─── journal plan ───
|
|
477
207
|
journal
|
|
478
|
-
.command('plan
|
|
479
|
-
.description('
|
|
480
|
-
.requiredOption('--thesis <thesis>', 'The reasoning behind the trade')
|
|
481
|
-
.requiredOption('--setup <setup>', 'Setup type (BREAKOUT, MOMENTUM, etc.)')
|
|
482
|
-
.requiredOption('--direction <direction>', 'Trade direction (LONG, SHORT)')
|
|
483
|
-
.requiredOption('--entry-target <price>', 'Planned entry price', parseFloat)
|
|
484
|
-
.requiredOption('--invalidation <price>', 'Invalidation / stop-loss price', parseFloat)
|
|
485
|
-
.requiredOption('--take-profit <price>', 'Take-profit target price', parseFloat)
|
|
486
|
-
.option('--risk-percent <pct>', 'Percentage of portfolio to risk', parseFloat, 2)
|
|
487
|
-
.option('--confidence <confidence>', 'Confidence level (0-1)', parseFloat, 0.5)
|
|
488
|
-
.option('--emotion <emotion>', 'Emotional tag (CONVICTION, FOMO, REVENGE, IMPULSE, FEAR, GREED, NEUTRAL)', 'NEUTRAL')
|
|
208
|
+
.command('plan')
|
|
209
|
+
.description('List pre-trade plans')
|
|
489
210
|
.option('--trader <traderId>', 'Trader ID', DEFAULT_TRADER_ID)
|
|
211
|
+
.option('--token <symbol>', 'Filter by token symbol')
|
|
212
|
+
.option('--status <status>', 'Filter by status: PENDING, EXECUTED, CANCELLED, EXPIRED')
|
|
213
|
+
.option('--limit <n>', 'Maximum results', parseInt, 50)
|
|
490
214
|
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
491
|
-
.action(async (
|
|
492
|
-
// Input validation
|
|
493
|
-
if (!['LONG', 'SHORT'].includes(options.direction.toUpperCase())) {
|
|
494
|
-
console.error('Error: --direction must be LONG or SHORT');
|
|
495
|
-
process.exitCode = 1;
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
if (options.confidence < 0 || options.confidence > 1) {
|
|
499
|
-
console.error('Error: --confidence must be between 0 and 1');
|
|
500
|
-
process.exitCode = 1;
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
if (options.riskPercent <= 0 || options.riskPercent > 100) {
|
|
504
|
-
console.error('Error: --risk-percent must be between 0 and 100');
|
|
505
|
-
process.exitCode = 1;
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
// Price validation: all prices must be positive
|
|
509
|
-
if (options.entryTarget <= 0 || options.invalidation <= 0 || options.takeProfit <= 0) {
|
|
510
|
-
console.error('Error: --entry-target, --invalidation, and --take-profit must be positive numbers');
|
|
511
|
-
process.exitCode = 1;
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
// Price ordering validation
|
|
515
|
-
const dir = options.direction.toUpperCase();
|
|
516
|
-
if (dir === 'LONG' && !(options.invalidation < options.entryTarget && options.entryTarget < options.takeProfit)) {
|
|
517
|
-
console.error('Error: LONG requires invalidation < entry-target < take-profit');
|
|
518
|
-
process.exitCode = 1;
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
if (dir === 'SHORT' && !(options.takeProfit < options.entryTarget && options.entryTarget < options.invalidation)) {
|
|
522
|
-
console.error('Error: SHORT requires take-profit < entry-target < invalidation');
|
|
523
|
-
process.exitCode = 1;
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
const client = createNeo4jClient();
|
|
527
|
-
const redis = createRedisClient();
|
|
528
|
-
const tsClient = createTimescaleClient();
|
|
215
|
+
.action(async (options) => {
|
|
529
216
|
try {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
preState = await buildPreState({ tsClient, redis }, tokenSymbol);
|
|
539
|
-
}
|
|
540
|
-
catch {
|
|
541
|
-
preState = buildDefaultPreState();
|
|
542
|
-
}
|
|
543
|
-
// Compute Kelly suggestion from historical setup stats
|
|
544
|
-
let kellySuggestion = null;
|
|
545
|
-
try {
|
|
546
|
-
const setupStats = await getStatsBySetupType(client, options.trader);
|
|
547
|
-
const setupKey = options.setup.toUpperCase();
|
|
548
|
-
const stats = setupStats.get(setupKey);
|
|
549
|
-
if (stats && stats.totalTrades > 0) {
|
|
550
|
-
// Need to get raw decisions for avg win/loss calculation
|
|
551
|
-
// Use extractSetupStats from Kelly module with the stats we have
|
|
552
|
-
const decisions = await getDecisionChain(client, options.trader, {
|
|
553
|
-
tokenSymbol: undefined,
|
|
554
|
-
limit: 500,
|
|
555
|
-
});
|
|
556
|
-
const setupDecisions = decisions.filter((d) => d.setupType === setupKey && d.pnlAbsolute !== null);
|
|
557
|
-
const kellyStats = extractSetupStats(setupDecisions);
|
|
558
|
-
const kellyResult = computeKellySizing(kellyStats, preState.portfolioValueUsd);
|
|
559
|
-
kellySuggestion = kellyResult.fractionalKelly;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
// Non-fatal — proceed without Kelly
|
|
564
|
-
logger.debug('Could not compute Kelly suggestion');
|
|
565
|
-
}
|
|
566
|
-
const plan = await createPlan(tsClient, {
|
|
567
|
-
traderId: options.trader,
|
|
568
|
-
tokenSymbol,
|
|
569
|
-
direction,
|
|
570
|
-
thesis: options.thesis,
|
|
571
|
-
setupType: options.setup.toUpperCase(),
|
|
572
|
-
entryTarget: options.entryTarget,
|
|
573
|
-
invalidationPrice: options.invalidation,
|
|
574
|
-
takeProfitPrice: options.takeProfit,
|
|
575
|
-
riskPercent: options.riskPercent,
|
|
576
|
-
confidence: options.confidence,
|
|
577
|
-
emotionalTag: options.emotion.toUpperCase(),
|
|
578
|
-
preState,
|
|
579
|
-
kellySuggestion,
|
|
580
|
-
});
|
|
217
|
+
const params = new URLSearchParams();
|
|
218
|
+
params.set('traderId', options.trader);
|
|
219
|
+
if (options.token)
|
|
220
|
+
params.set('tokenSymbol', options.token);
|
|
221
|
+
if (options.status)
|
|
222
|
+
params.set('status', options.status);
|
|
223
|
+
params.set('limit', String(options.limit));
|
|
224
|
+
const data = await apiRequest(`/api/v1/journal/plan?${params.toString()}`);
|
|
581
225
|
if (options.format === 'json') {
|
|
582
|
-
console.log(JSON.stringify(
|
|
583
|
-
id: plan.id,
|
|
584
|
-
tokenSymbol: plan.tokenSymbol,
|
|
585
|
-
direction: plan.direction,
|
|
586
|
-
setupType: plan.setupType,
|
|
587
|
-
thesis: plan.thesis,
|
|
588
|
-
entryTarget: plan.entryTarget,
|
|
589
|
-
invalidationPrice: plan.invalidationPrice,
|
|
590
|
-
takeProfitPrice: plan.takeProfitPrice,
|
|
591
|
-
riskRewardRatio: plan.riskRewardRatio,
|
|
592
|
-
riskPercent: plan.riskPercent,
|
|
593
|
-
confidence: plan.confidence,
|
|
594
|
-
kellySuggestion,
|
|
595
|
-
status: plan.status,
|
|
596
|
-
createdAt: plan.createdAt,
|
|
597
|
-
}, null, 2));
|
|
226
|
+
console.log(JSON.stringify(data, null, 2));
|
|
598
227
|
}
|
|
599
228
|
else {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
console.log(`Invalidation: $${plan.invalidationPrice.toFixed(2)}`);
|
|
610
|
-
console.log(`Take Profit: $${plan.takeProfitPrice.toFixed(2)}`);
|
|
611
|
-
console.log(`R:R Ratio: ${plan.riskRewardRatio.toFixed(2)}`);
|
|
612
|
-
console.log(`Risk: ${plan.riskPercent}% of portfolio`);
|
|
613
|
-
console.log(`Confidence: ${plan.confidence}`);
|
|
614
|
-
if (kellySuggestion !== null && kellySuggestion > 0) {
|
|
615
|
-
const posSize = preState.portfolioValueUsd * kellySuggestion;
|
|
616
|
-
console.log(`Kelly (Half): ${(kellySuggestion * 100).toFixed(1)}% → $${posSize.toFixed(0)}`);
|
|
229
|
+
if (data.plans.length === 0) {
|
|
230
|
+
console.log(' No pre-trade plans found.');
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(`\n Pre-Trade Plans (${data.count}):\n`);
|
|
234
|
+
for (const plan of data.plans) {
|
|
235
|
+
console.log(` ${padRight(String(plan.id ?? ''), 14)} ${padRight(String(plan.tokenSymbol ?? ''), 8)} ${padRight(String(plan.status ?? ''), 12)} ${plan.direction ?? ''}`);
|
|
236
|
+
}
|
|
237
|
+
console.log('');
|
|
617
238
|
}
|
|
618
|
-
console.log('');
|
|
619
|
-
console.log(`To execute: trading-boy journal log entry ${tokenSymbol} --plan ${plan.id}`);
|
|
620
239
|
}
|
|
621
240
|
}
|
|
622
241
|
catch (error) {
|
|
623
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
624
|
-
logger.error({ error: message }, 'Failed to create plan');
|
|
242
|
+
const message = error instanceof ApiError ? error.message : (error instanceof Error ? error.message : String(error));
|
|
625
243
|
console.error(`Error: ${message}`);
|
|
626
|
-
|
|
627
|
-
console.error('Hint: Run migrations to create the pre_trade_plans table:');
|
|
628
|
-
console.error(' pnpm --filter @trading-boy/timeseries-db migrate');
|
|
629
|
-
}
|
|
630
|
-
const guidance = formatConnectionError(message);
|
|
631
|
-
if (guidance)
|
|
632
|
-
console.error(guidance);
|
|
633
|
-
process.exitCode = 1;
|
|
634
|
-
}
|
|
635
|
-
finally {
|
|
636
|
-
await redis.quit().catch(() => { });
|
|
637
|
-
await tsClient.disconnect().catch(() => { });
|
|
638
|
-
await client.disconnect();
|
|
244
|
+
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
639
245
|
}
|
|
640
246
|
});
|
|
641
247
|
// ─── journal size ───
|
|
642
248
|
journal
|
|
643
249
|
.command('size')
|
|
644
250
|
.description('Kelly criterion position sizing recommendation')
|
|
645
|
-
.requiredOption('--
|
|
646
|
-
.option('--capital <
|
|
647
|
-
.option('--fraction <
|
|
648
|
-
.option('--trader <traderId>', 'Trader ID', DEFAULT_TRADER_ID)
|
|
251
|
+
.requiredOption('--trader <traderId>', 'Trader ID')
|
|
252
|
+
.option('--capital <usd>', 'Portfolio capital in USD', parseFloat, 10000)
|
|
253
|
+
.option('--fraction <f>', 'Kelly fraction (0-1, default 0.5 = Half Kelly)', parseFloat, 0.5)
|
|
649
254
|
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
650
255
|
.action(async (options) => {
|
|
651
|
-
const client = createNeo4jClient();
|
|
652
|
-
const redis = createRedisClient();
|
|
653
|
-
const tsClient = createTimescaleClient();
|
|
654
256
|
try {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
let capitalUsd = options.capital;
|
|
661
|
-
if (!capitalUsd) {
|
|
662
|
-
try {
|
|
663
|
-
const preState = await buildPreState({ tsClient, redis }, 'SOL');
|
|
664
|
-
capitalUsd = preState.portfolioValueUsd;
|
|
665
|
-
}
|
|
666
|
-
catch {
|
|
667
|
-
capitalUsd = 10000; // fallback
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
// Get all decisions for this setup
|
|
671
|
-
const decisions = await getDecisionChain(client, options.trader, {
|
|
672
|
-
tokenSymbol: undefined,
|
|
673
|
-
limit: 500,
|
|
674
|
-
});
|
|
675
|
-
const setupDecisions = decisions.filter((d) => d.setupType === setupKey && d.pnlAbsolute !== null);
|
|
676
|
-
const stats = extractSetupStats(setupDecisions);
|
|
677
|
-
if (stats.totalTrades === 0) {
|
|
678
|
-
console.log(`No completed trades found for setup type: ${setupKey}`);
|
|
679
|
-
console.log('Kelly sizing requires historical trade data with outcomes.');
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
const fullResult = computeKellySizing(stats, capitalUsd, 1.0);
|
|
683
|
-
const halfResult = computeKellySizing(stats, capitalUsd, 0.5);
|
|
684
|
-
const quarterResult = computeKellySizing(stats, capitalUsd, 0.25);
|
|
257
|
+
const params = new URLSearchParams();
|
|
258
|
+
params.set('traderId', options.trader);
|
|
259
|
+
params.set('capitalUsd', String(options.capital));
|
|
260
|
+
params.set('fraction', String(options.fraction));
|
|
261
|
+
const data = await apiRequest(`/api/v1/journal/size?${params.toString()}`);
|
|
685
262
|
if (options.format === 'json') {
|
|
686
|
-
|
|
687
|
-
console.log(JSON.stringify({
|
|
688
|
-
setupType: setupKey,
|
|
689
|
-
sampleSize: stats.totalTrades,
|
|
690
|
-
winRate: stats.winRate,
|
|
691
|
-
avgWin: stats.avgWin,
|
|
692
|
-
avgLoss: stats.avgLoss,
|
|
693
|
-
payoffRatio,
|
|
694
|
-
capitalUsd: capitalUsd,
|
|
695
|
-
fullKelly: { fraction: fullResult.fullKelly, positionSizeUsd: fullResult.positionSizeUsd },
|
|
696
|
-
halfKelly: { fraction: halfResult.fractionalKelly, positionSizeUsd: halfResult.positionSizeUsd },
|
|
697
|
-
quarterKelly: { fraction: quarterResult.fractionalKelly, positionSizeUsd: quarterResult.positionSizeUsd },
|
|
698
|
-
confidenceNote: halfResult.confidenceNote,
|
|
699
|
-
}, null, 2));
|
|
263
|
+
console.log(JSON.stringify(data, null, 2));
|
|
700
264
|
}
|
|
701
265
|
else {
|
|
702
|
-
console.log('');
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
console.log(` Win Rate (p): ${(stats.winRate * 100).toFixed(1)}%`);
|
|
707
|
-
console.log(` Avg Win: $${stats.avgWin.toFixed(2)}`);
|
|
708
|
-
console.log(` Avg Loss: $${stats.avgLoss.toFixed(2)}`);
|
|
709
|
-
const payoffRatio = stats.avgLoss !== 0 ? stats.avgWin / Math.abs(stats.avgLoss) : 0;
|
|
710
|
-
console.log(` Payoff Ratio: ${payoffRatio.toFixed(2)}`);
|
|
711
|
-
console.log('');
|
|
712
|
-
console.log(` Full Kelly: ${(fullResult.fullKelly * 100).toFixed(1)}% of capital ($${fullResult.positionSizeUsd.toFixed(0)})`);
|
|
713
|
-
console.log(` Half Kelly: ${(halfResult.fractionalKelly * 100).toFixed(1)}% of capital ($${halfResult.positionSizeUsd.toFixed(0)}) ← RECOMMENDED`);
|
|
714
|
-
console.log(` Quarter Kelly: ${(quarterResult.fractionalKelly * 100).toFixed(1)}% of capital ($${quarterResult.positionSizeUsd.toFixed(0)})`);
|
|
715
|
-
console.log('');
|
|
716
|
-
console.log(` ${halfResult.confidenceNote}`);
|
|
266
|
+
console.log('\n Kelly Position Sizing:\n');
|
|
267
|
+
for (const [key, value] of Object.entries(data)) {
|
|
268
|
+
console.log(` ${padRight(key, 24)} ${value}`);
|
|
269
|
+
}
|
|
717
270
|
console.log('');
|
|
718
271
|
}
|
|
719
272
|
}
|
|
720
273
|
catch (error) {
|
|
721
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
722
|
-
logger.error({ error: message }, 'Failed to compute Kelly sizing');
|
|
274
|
+
const message = error instanceof ApiError ? error.message : (error instanceof Error ? error.message : String(error));
|
|
723
275
|
console.error(`Error: ${message}`);
|
|
724
|
-
|
|
725
|
-
if (guidance)
|
|
726
|
-
console.error(guidance);
|
|
727
|
-
process.exitCode = 1;
|
|
728
|
-
}
|
|
729
|
-
finally {
|
|
730
|
-
await redis.quit().catch(() => { });
|
|
731
|
-
await tsClient.disconnect().catch(() => { });
|
|
732
|
-
await client.disconnect();
|
|
276
|
+
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
733
277
|
}
|
|
734
278
|
});
|
|
735
279
|
}
|
|
736
|
-
//
|
|
737
|
-
function padRight(str, len) {
|
|
738
|
-
return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
|
|
739
|
-
}
|
|
280
|
+
// PlanOptions and SizeOptions removed — commands hidden until API endpoints exist
|
|
740
281
|
//# sourceMappingURL=journal.js.map
|