@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.
Files changed (109) hide show
  1. package/dist/api-client.d.ts +5 -0
  2. package/dist/api-client.d.ts.map +1 -1
  3. package/dist/api-client.js +37 -3
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +39 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/audit.d.ts +0 -15
  9. package/dist/commands/audit.d.ts.map +1 -1
  10. package/dist/commands/audit.js +17 -79
  11. package/dist/commands/audit.js.map +1 -1
  12. package/dist/commands/behavioral.d.ts +34 -1
  13. package/dist/commands/behavioral.d.ts.map +1 -1
  14. package/dist/commands/behavioral.js +56 -76
  15. package/dist/commands/behavioral.js.map +1 -1
  16. package/dist/commands/benchmark-cmd.js +2 -2
  17. package/dist/commands/benchmark-cmd.js.map +1 -1
  18. package/dist/commands/billing.d.ts.map +1 -1
  19. package/dist/commands/billing.js +15 -10
  20. package/dist/commands/billing.js.map +1 -1
  21. package/dist/commands/catalysts.d.ts +10 -1
  22. package/dist/commands/catalysts.d.ts.map +1 -1
  23. package/dist/commands/catalysts.js +15 -84
  24. package/dist/commands/catalysts.js.map +1 -1
  25. package/dist/commands/coaching-cmd.d.ts.map +1 -1
  26. package/dist/commands/coaching-cmd.js +21 -10
  27. package/dist/commands/coaching-cmd.js.map +1 -1
  28. package/dist/commands/config-cmd.d.ts.map +1 -1
  29. package/dist/commands/config-cmd.js +35 -19
  30. package/dist/commands/config-cmd.js.map +1 -1
  31. package/dist/commands/context.d.ts +0 -7
  32. package/dist/commands/context.d.ts.map +1 -1
  33. package/dist/commands/context.js +47 -199
  34. package/dist/commands/context.js.map +1 -1
  35. package/dist/commands/decisions.d.ts +38 -2
  36. package/dist/commands/decisions.d.ts.map +1 -1
  37. package/dist/commands/decisions.js +35 -134
  38. package/dist/commands/decisions.js.map +1 -1
  39. package/dist/commands/edge-cmd.d.ts +55 -45
  40. package/dist/commands/edge-cmd.d.ts.map +1 -1
  41. package/dist/commands/edge-cmd.js +76 -32
  42. package/dist/commands/edge-cmd.js.map +1 -1
  43. package/dist/commands/edge-guard-cmd.d.ts.map +1 -1
  44. package/dist/commands/edge-guard-cmd.js +7 -3
  45. package/dist/commands/edge-guard-cmd.js.map +1 -1
  46. package/dist/commands/events.d.ts.map +1 -1
  47. package/dist/commands/events.js +78 -162
  48. package/dist/commands/events.js.map +1 -1
  49. package/dist/commands/infra.d.ts +2 -4
  50. package/dist/commands/infra.d.ts.map +1 -1
  51. package/dist/commands/infra.js +65 -163
  52. package/dist/commands/infra.js.map +1 -1
  53. package/dist/commands/journal.d.ts.map +1 -1
  54. package/dist/commands/journal.js +138 -597
  55. package/dist/commands/journal.js.map +1 -1
  56. package/dist/commands/login.js +3 -3
  57. package/dist/commands/login.js.map +1 -1
  58. package/dist/commands/narratives.d.ts.map +1 -1
  59. package/dist/commands/narratives.js +90 -311
  60. package/dist/commands/narratives.js.map +1 -1
  61. package/dist/commands/query.d.ts +0 -7
  62. package/dist/commands/query.d.ts.map +1 -1
  63. package/dist/commands/query.js +35 -157
  64. package/dist/commands/query.js.map +1 -1
  65. package/dist/commands/replay-cmd.d.ts.map +1 -1
  66. package/dist/commands/replay-cmd.js +6 -8
  67. package/dist/commands/replay-cmd.js.map +1 -1
  68. package/dist/commands/review.d.ts.map +1 -1
  69. package/dist/commands/review.js +45 -83
  70. package/dist/commands/review.js.map +1 -1
  71. package/dist/commands/risk.d.ts +29 -1
  72. package/dist/commands/risk.d.ts.map +1 -1
  73. package/dist/commands/risk.js +20 -89
  74. package/dist/commands/risk.js.map +1 -1
  75. package/dist/commands/social.d.ts +36 -2
  76. package/dist/commands/social.d.ts.map +1 -1
  77. package/dist/commands/social.js +74 -148
  78. package/dist/commands/social.js.map +1 -1
  79. package/dist/commands/strategy-cmd.d.ts.map +1 -1
  80. package/dist/commands/strategy-cmd.js +2 -4
  81. package/dist/commands/strategy-cmd.js.map +1 -1
  82. package/dist/commands/subscribe.js +4 -4
  83. package/dist/commands/subscribe.js.map +1 -1
  84. package/dist/commands/suggestions-cmd.d.ts +24 -0
  85. package/dist/commands/suggestions-cmd.d.ts.map +1 -0
  86. package/dist/commands/suggestions-cmd.js +142 -0
  87. package/dist/commands/suggestions-cmd.js.map +1 -0
  88. package/dist/commands/thesis-cmd.js +4 -4
  89. package/dist/commands/thesis-cmd.js.map +1 -1
  90. package/dist/commands/trader.d.ts +18 -1
  91. package/dist/commands/trader.d.ts.map +1 -1
  92. package/dist/commands/trader.js +105 -225
  93. package/dist/commands/trader.js.map +1 -1
  94. package/dist/commands/watch.d.ts +0 -10
  95. package/dist/commands/watch.d.ts.map +1 -1
  96. package/dist/commands/watch.js +5 -66
  97. package/dist/commands/watch.js.map +1 -1
  98. package/dist/commands/whoami.d.ts.map +1 -1
  99. package/dist/commands/whoami.js +18 -2
  100. package/dist/commands/whoami.js.map +1 -1
  101. package/dist/index.d.ts +4 -4
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +4 -4
  104. package/dist/index.js.map +1 -1
  105. package/dist/utils.d.ts +45 -0
  106. package/dist/utils.d.ts.map +1 -1
  107. package/dist/utils.js +97 -0
  108. package/dist/utils.js.map +1 -1
  109. package/package.json +5 -10
@@ -1,43 +1,9 @@
1
1
  import { Option } from 'commander';
2
- import ioredis from 'ioredis';
3
- const { Redis } = ioredis;
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 client.connect();
129
- await redis.connect();
130
- await tsClient.connect();
131
- const tokenSymbol = symbol.toUpperCase();
132
- // If --plan provided, look up the plan and auto-fill fields
133
- let linkedPlan = null;
134
- if (options.plan) {
135
- linkedPlan = await getPlan(tsClient, options.plan);
136
- if (!linkedPlan) {
137
- console.error(`Error: Plan ${options.plan} not found`);
138
- process.exitCode = 1;
139
- return;
140
- }
141
- if (linkedPlan.status !== PlanStatus.DRAFT) {
142
- console.error(`Error: Plan ${options.plan} is already ${linkedPlan.status}`);
143
- process.exitCode = 1;
144
- return;
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
- logger.info({ id, hash }, 'Entry logged');
211
- console.log(`Entry logged: id=${id} hash=${hash}`);
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
- const guidance = formatConnectionError(message);
218
- if (guidance)
219
- console.error(guidance);
220
- process.exitCode = 1;
221
- }
222
- finally {
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 client.connect();
278
- await redis.connect();
279
- await tsClient.connect();
280
- const tokenSymbol = symbol.toUpperCase();
281
- // Build real pre-state (fallback to defaults on failure)
282
- let preState;
283
- try {
284
- preState = await buildPreState({ tsClient, redis }, tokenSymbol);
285
- }
286
- catch {
287
- preState = buildDefaultPreState();
288
- }
289
- const eventTime = new Date();
290
- // Fetch entry for linking if provided
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
- logger.info({ id, hash }, 'Exit logged');
352
- console.log(`Exit logged: id=${id} hash=${hash}`);
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
- const guidance = formatConnectionError(message);
359
- if (guidance)
360
- console.error(guidance);
361
- process.exitCode = 1;
362
- }
363
- finally {
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
- // ─── Remote API mode ───
378
- if (await isRemoteMode()) {
379
- try {
380
- const params = new URLSearchParams();
381
- if (options.trader)
382
- params.set('traderId', options.trader);
383
- if (options.token)
384
- params.set('tokenSymbol', options.token.toUpperCase());
385
- if (options.limit)
386
- params.set('limit', String(options.limit));
387
- const qs = params.toString() ? `?${params.toString()}` : '';
388
- const result = await apiRequest(`/api/v1/decisions${qs}`);
389
- const decisions = result.decisions ?? [];
390
- if (decisions.length === 0) {
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
- console.log('');
415
- console.log(`Total: ${decisions.length} decision(s)`);
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
- process.exitCode = 1;
166
+ return;
426
167
  }
427
- return;
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
- const guidance = formatConnectionError(message);
468
- if (guidance)
469
- console.error(guidance);
470
- process.exitCode = 1;
471
- }
472
- finally {
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 <symbol> ───
206
+ // ─── journal plan ───
477
207
  journal
478
- .command('plan <symbol>')
479
- .description('Create a pre-trade plan before entry')
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 (symbol, options) => {
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
- await client.connect();
531
- await redis.connect();
532
- await tsClient.connect();
533
- const tokenSymbol = symbol.toUpperCase();
534
- const direction = options.direction.toUpperCase();
535
- // Build pre-state
536
- let preState;
537
- try {
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
- console.log('');
601
- console.log(`═══ Pre-Trade Plan Created ═══`);
602
- console.log(`Plan ID: ${plan.id}`);
603
- console.log(`Token: ${plan.tokenSymbol}`);
604
- console.log(`Direction: ${plan.direction}`);
605
- console.log(`Setup: ${plan.setupType}`);
606
- console.log(`Thesis: ${plan.thesis}`);
607
- console.log('');
608
- console.log(`Entry Target: $${plan.entryTarget.toFixed(2)}`);
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
- if (message.includes('pre_trade_plans') && message.includes('does not exist')) {
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('--setup <setup>', 'Setup type (BREAKOUT, MOMENTUM, etc.)')
646
- .option('--capital <capital>', 'Portfolio capital in USD', parseFloat)
647
- .option('--fraction <fraction>', 'Kelly fraction (default 0.5 = Half Kelly)', parseFloat, 0.5)
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
- await client.connect();
656
- await redis.connect();
657
- await tsClient.connect();
658
- const setupKey = options.setup.toUpperCase();
659
- // Get capital from option or pre-state
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
- const payoffRatio = stats.avgLoss !== 0 ? stats.avgWin / Math.abs(stats.avgLoss) : 0;
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
- console.log(`═══ Kelly Position Sizing ${setupKey} ═══`);
704
- console.log('');
705
- console.log(`Based on last ${stats.totalTrades} ${setupKey} trades:`);
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
- const guidance = formatConnectionError(message);
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
- // ─── Helpers ───
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