product-runner 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -0
- package/common/agents/README.md +68 -0
- package/common/agents/agente-conceituacao.md +141 -0
- package/common/agents/agente-documentacao-funcional.md +107 -0
- package/common/agents/agente-gerador-spec.md +106 -0
- package/common/agents/agente-kickoff.md +121 -0
- package/common/agents/agente-prod-runner.md +107 -0
- package/common/agents/agente-review-code.md +97 -0
- package/common/agents/agente-review-llm.md +94 -0
- package/common/agents/agente-review-product.md +98 -0
- package/common/agents/agente-user-review.md +99 -0
- package/common/agents/protocolo-de-gates.md +51 -0
- package/common/claude-md.template.md +210 -0
- package/common/design-principles.md +229 -0
- package/common/lessons-learned.md +440 -0
- package/common/pipeline.md +143 -0
- package/common/spec-guide.md +327 -0
- package/common/specs/_open-issues.md +46 -0
- package/common/specs/_overview.md +75 -0
- package/dist/cli.js +187 -0
- package/dist/migrations.js +147 -0
- package/dist/scaffold.js +276 -0
- package/dist/update.js +400 -0
- package/migrations/0.3.0.md +54 -0
- package/migrations/0.4.0.md +76 -0
- package/migrations/0.5.0.md +55 -0
- package/migrations/README.md +68 -0
- package/package.json +41 -0
- package/profile-cli/README.md +54 -0
- package/profile-cli/claude-md.extension.md +102 -0
- package/profile-cli/code-patterns.md +363 -0
- package/profile-ssr/DESIGN-SYSTEM.md +795 -0
- package/profile-ssr/README.md +51 -0
- package/profile-ssr/api-patterns.md +70 -0
- package/profile-ssr/claude-md.extension.md +113 -0
- package/profile-ssr/code-patterns.md +175 -0
- package/profile-ssr/ui-patterns.md +97 -0
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "product-runner",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Scaffold de docs para projetos TypeScript com desenvolvimento assistido por AI: gera CLAUDE.md + docs/ a partir de templates vivos (common + perfil cli/ssr).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"product-runner": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"!dist/**/*.test.js",
|
|
12
|
+
"common",
|
|
13
|
+
"profile-cli",
|
|
14
|
+
"profile-ssr",
|
|
15
|
+
"migrations"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"dev": "node --experimental-strip-types src/cli.ts",
|
|
23
|
+
"test": "npm run build && node --test dist/*.test.js",
|
|
24
|
+
"prepare": "npm run build",
|
|
25
|
+
"prepack": "npm run build",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"scaffold",
|
|
30
|
+
"template",
|
|
31
|
+
"claude",
|
|
32
|
+
"ai-assisted",
|
|
33
|
+
"typescript",
|
|
34
|
+
"docs"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"typescript": "^5.6.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Perfil: CLI / script Node em loop
|
|
2
|
+
|
|
3
|
+
Use este perfil em projetos que:
|
|
4
|
+
|
|
5
|
+
- Rodam como **script Node terminal** (não servidor HTTP).
|
|
6
|
+
- Têm um **loop principal** (infinito ou periódico).
|
|
7
|
+
- Fazem I/O com **lib externa** (broker, AI API, queue, etc.) que precisa ser isolada.
|
|
8
|
+
- Persistem estado via **arquivos locais** (JSON) ou DB.
|
|
9
|
+
- Não têm UI — observability via logs + arquivos consumidos por
|
|
10
|
+
ferramentas tipo OpenSearch/Kibana.
|
|
11
|
+
|
|
12
|
+
## Conteúdo
|
|
13
|
+
|
|
14
|
+
| Arquivo | Pra quê |
|
|
15
|
+
|---|---|
|
|
16
|
+
| [code-patterns](./code-patterns.md) | Estrutura de pastas, schemas Zod, port/adapter pra integrações, padrões de erro, persistência |
|
|
17
|
+
| [claude-md.extension](../CLAUDE.md) | Seções específicas pra CLI (comandos `npm run`, configuração `.env`, observability via arquivos) |
|
|
18
|
+
|
|
19
|
+
## Como combinar com `common/`
|
|
20
|
+
|
|
21
|
+
Use o CLI — ele copia `common/` + este perfil pra `docs/` e gera o
|
|
22
|
+
`CLAUDE.md` raiz (mescla template + extension, substitui `{...}`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx product-runner --name meu-projeto --profile cli --dir .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Equivalente manual, se preferir sem npm:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cp common/*.md meu-projeto/docs/
|
|
32
|
+
cp profile-cli/*.md meu-projeto/docs/
|
|
33
|
+
cat common/claude-md.template.md profile-cli/claude-md.extension.md \
|
|
34
|
+
> meu-projeto/CLAUDE.md
|
|
35
|
+
# Adapta valores entre {} pelos do projeto.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Origem
|
|
39
|
+
|
|
40
|
+
Conteúdo extraído de:
|
|
41
|
+
- **tradeBot** (snapshot tradebot-202605, refactor 11 specs)
|
|
42
|
+
|
|
43
|
+
Adaptações feitas pra generalizar:
|
|
44
|
+
- `BrokerClient` virou termo genérico "ExternalAdapter" em alguns
|
|
45
|
+
exemplos.
|
|
46
|
+
- `tradeBot`/`Binance` → `{ProjectName}` / `{ExternalSystem}` quando
|
|
47
|
+
apropriado.
|
|
48
|
+
- Defaults específicos de trade (multipliers, etc.) foram trocados
|
|
49
|
+
por placeholders.
|
|
50
|
+
|
|
51
|
+
## Anti-pattern: usar este perfil pra projeto SSR
|
|
52
|
+
|
|
53
|
+
Web SSR tem fronteira HTTP, request/response, UI. Estrutura aqui
|
|
54
|
+
não cobre. Use `profile-ssr/` no lugar.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Extensão do CLAUDE.md — Perfil CLI.
|
|
3
|
+
Este arquivo NÃO é concatenado: cada bloco abaixo declara, via diretiva
|
|
4
|
+
`prod-runner-merge`, como dobra numa seção do template-base (common/claude-md.template.md).
|
|
5
|
+
Modos: replace (troca a seção), append (acrescenta ao fim da seção),
|
|
6
|
+
after (insere logo após a seção). Edite o conteúdo, não as diretivas.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
<!-- prod-runner-merge: append section="Stack" -->
|
|
10
|
+
|
|
11
|
+
- **Broker / lib externa:** {ex: `@binance/connector`, `openai`, etc.} —
|
|
12
|
+
abstraído via `BrokerClient` (port/adapter)
|
|
13
|
+
- **Config:** `dotenv` + `.env`
|
|
14
|
+
- **Persistência:** arquivos JSON locais (`actualState.json`, `history/`)
|
|
15
|
+
- **Observability:** arquivos JSON formato Kibana consumidos por OpenSearch
|
|
16
|
+
local (ou similar)
|
|
17
|
+
- **Containerização:** Docker (`docker-compose.yml`) — opcional
|
|
18
|
+
|
|
19
|
+
<!-- prod-runner-merge: replace section="Princípio central" -->
|
|
20
|
+
|
|
21
|
+
### Princípio central
|
|
22
|
+
|
|
23
|
+
Lógica de domínio (cálculo, decisão, regras) é código TypeScript
|
|
24
|
+
puro, sem acoplamento com lib externa, file system ou env. Acesso
|
|
25
|
+
à lib externa via interface (port). Persistência via funções
|
|
26
|
+
dedicadas. Loop principal é casca fina que orquestra.
|
|
27
|
+
|
|
28
|
+
<!-- prod-runner-merge: replace section="Estrutura de pastas" -->
|
|
29
|
+
|
|
30
|
+
### Estrutura de pastas
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{project}/
|
|
34
|
+
├── CLAUDE.md
|
|
35
|
+
├── index.ts ← entrypoint mínimo (loop fino)
|
|
36
|
+
├── docs/
|
|
37
|
+
├── specs/
|
|
38
|
+
├── domain/
|
|
39
|
+
│ ├── {externalAdapter}.ts ← interface (port)
|
|
40
|
+
│ └── errors.ts ← classes de erro de domínio
|
|
41
|
+
├── services/
|
|
42
|
+
│ ├── {dominio}/ ← lógica pura + schemas
|
|
43
|
+
│ ├── persistence/ ← load/save JSON, mappers
|
|
44
|
+
│ └── integrations/ ← adapters (implementações dos ports)
|
|
45
|
+
├── tests/
|
|
46
|
+
└── dist/ ← outDir do tsc (gitignored)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
<!-- prod-runner-merge: replace section="Comandos úteis" -->
|
|
50
|
+
|
|
51
|
+
## Comandos úteis
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install # deps
|
|
55
|
+
npm run start # compilar e rodar (loop infinito)
|
|
56
|
+
npm run start:reset # apaga estado e recomeça do zero (se aplicável)
|
|
57
|
+
npm test # testes
|
|
58
|
+
npx tsc --noEmit # typecheck
|
|
59
|
+
docker compose up -d # infra (OpenSearch local, etc.) — opcional
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
<!-- prod-runner-merge: replace section="Configuração" -->
|
|
63
|
+
|
|
64
|
+
## Configuração
|
|
65
|
+
|
|
66
|
+
| Arquivo | Conteúdo | Comitado? |
|
|
67
|
+
| ------------------------ | ----------------------------------------- | --------- |
|
|
68
|
+
| `.env` | Segredos da lib externa (API keys, etc.) | ❌ NUNCA |
|
|
69
|
+
| `.env.example` | Mesmas chaves sem valor | ✅ |
|
|
70
|
+
| `global.json` ou similar | Defaults globais | ✅ |
|
|
71
|
+
| `actualState.json` | Estado runtime (auto-gerado) | ❌ |
|
|
72
|
+
| `history/*` | Logs estruturados | ❌ |
|
|
73
|
+
|
|
74
|
+
Hot-reload de configs (se aplicável): a cada N segundos no loop principal
|
|
75
|
+
(definir `configCacheTime`).
|
|
76
|
+
|
|
77
|
+
<!-- prod-runner-merge: append section="Convenções de código" -->
|
|
78
|
+
|
|
79
|
+
### Comportamento do loop após erro (CLI)
|
|
80
|
+
|
|
81
|
+
O loop principal **NÃO deve morrer silenciosamente**. Erros não
|
|
82
|
+
tratados disparam `console.error` + exit code != 0. Pattern:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
runTradingLoop({ broker }).catch((error) => {
|
|
86
|
+
console.error("FATAL: bot loop crashed", error);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Loop interno tem `try/catch` por par/tarefa pra continuar processando
|
|
92
|
+
outros, mas erro de orquestração mata o processo.
|
|
93
|
+
|
|
94
|
+
### Observability (CLI)
|
|
95
|
+
|
|
96
|
+
Status quo recomendado:
|
|
97
|
+
|
|
98
|
+
- Logs estruturados em `history/*.json` formato Kibana (`_index`,
|
|
99
|
+
`_source`, `_id`).
|
|
100
|
+
- OpenSearch local via Docker pra dashboards (ou similar).
|
|
101
|
+
- `console.log` apenas pra orquestração (logger estruturado entra
|
|
102
|
+
em spec dedicada se necessário).
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# Code patterns
|
|
2
|
+
|
|
3
|
+
Padrões para schemas, tipos, services, broker abstrato,
|
|
4
|
+
erros e persistência neste projeto.
|
|
5
|
+
|
|
6
|
+
Este documento descreve o **alvo arquitetural**. O código atual
|
|
7
|
+
não está todo nesse formato — chegamos lá pelas specs em `specs/`.
|
|
8
|
+
Quando houver divergência entre este doc e o código, este doc
|
|
9
|
+
ganha; o código é o que está sendo refatorado.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Estrutura de pastas (alvo)
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
tradeBotRefatoring/
|
|
17
|
+
├── CLAUDE.md
|
|
18
|
+
├── index.ts ← entrypoint mínimo (loop fino)
|
|
19
|
+
├── docs/ ← este e outros docs de referência
|
|
20
|
+
├── specs/ ← specs por domínio
|
|
21
|
+
├── domain/
|
|
22
|
+
│ ├── broker.ts ← interface BrokerClient
|
|
23
|
+
│ └── errors.ts ← TradeError e subclasses
|
|
24
|
+
├── services/
|
|
25
|
+
│ ├── waves/
|
|
26
|
+
│ │ ├── schema.ts ← WavesHistorySchema, derivados
|
|
27
|
+
│ │ ├── waves-history.ts ← classe / lógica de detecção
|
|
28
|
+
│ │ └── tendency.ts ← cálculos de tendência
|
|
29
|
+
│ ├── trading/
|
|
30
|
+
│ │ ├── schema.ts ← TradingDataSchema, BuyOrder, SellOrder
|
|
31
|
+
│ │ ├── limits.ts ← recalculateLimits puro
|
|
32
|
+
│ │ ├── decisions.ts ← canBuy / canSell
|
|
33
|
+
│ │ └── loop.ts ← orquestração do loop
|
|
34
|
+
│ ├── persistence/
|
|
35
|
+
│ │ ├── schema.ts ← BotStateSchema (root do estado)
|
|
36
|
+
│ │ ├── load.ts ← reconstrução com schema
|
|
37
|
+
│ │ ├── save.ts ← serialização
|
|
38
|
+
│ │ └── kibana.ts ← mapper toKibana
|
|
39
|
+
│ └── integrations/
|
|
40
|
+
│ └── binance-broker.ts ← adapter @binance/connector → BrokerClient
|
|
41
|
+
└── tests/ ← Vitest (a partir de setup/01)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Schemas e tipos
|
|
47
|
+
|
|
48
|
+
### Entity raiz
|
|
49
|
+
|
|
50
|
+
Cada domínio tem `schema.ts` com a entity Zod. Tipos derivam
|
|
51
|
+
sempre via `z.infer`.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// services/trading/schema.ts
|
|
55
|
+
import { z } from "zod";
|
|
56
|
+
|
|
57
|
+
export const TradingDataSchema = z.object({
|
|
58
|
+
tradeSymbol: z.string(),
|
|
59
|
+
totalToBuy: z.number().nonnegative(),
|
|
60
|
+
sliceToBuy: z.number().positive(),
|
|
61
|
+
currentSliceToBuy: z.number().positive(),
|
|
62
|
+
buyOrdersWhithoutStack: z.number().int().nonnegative(),
|
|
63
|
+
lastTurnDateTime: z.coerce.date().nullable(),
|
|
64
|
+
lastMaxPrice: z.number().nullable(),
|
|
65
|
+
lastMinPrice: z.number().nullable(),
|
|
66
|
+
smallerBuyNotSelledPrice: z.number().nullable(),
|
|
67
|
+
lastCheckedPrice: z.number().nullable(),
|
|
68
|
+
maxLimitPriceToBuy: z.number().nullable(),
|
|
69
|
+
maxLimitPriceToBuy_calc: z.string().nullable(),
|
|
70
|
+
minLimitPriceToSell: z.number().nullable(),
|
|
71
|
+
brokeUpFlowWaitingLimitPrice: z.number().nullable(),
|
|
72
|
+
brokeDownFlowWaitingLimitPrice: z.number().nullable(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export type TradingData = z.infer<typeof TradingDataSchema>;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Regras
|
|
79
|
+
|
|
80
|
+
- Naming: `XxxSchema` pra Zod, `Xxx` pra type inferido.
|
|
81
|
+
- Nullable explícito (`.nullable()`), não `.optional()` por engano.
|
|
82
|
+
- Datas no JSON viram `string`; usar `z.coerce.date()` se quiser `Date` em runtime.
|
|
83
|
+
- Sem `interface` ou `type` paralelo ao schema.
|
|
84
|
+
|
|
85
|
+
### Derivações
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// Input com subset + override
|
|
89
|
+
export const CreateBuyOrderInput = BuyOrderSchema.pick({ gotPrice: true, quantity: true, tradeSymbol: true }).extend({
|
|
90
|
+
orderType: z.enum(["LIMIT_MAKER", "MARKET"]),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export type CreateBuyOrderInput = z.infer<typeof CreateBuyOrderInput>;
|
|
94
|
+
|
|
95
|
+
// Output sem campos internos
|
|
96
|
+
export const BuyOrderOutput = BuyOrderSchema.omit({ binanceOrderId: true }).extend({ ageMs: z.number() });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Validação na fronteira
|
|
102
|
+
|
|
103
|
+
Toda entrada do mundo externo passa por `safeParse` ou `parse`
|
|
104
|
+
ANTES de chegar na lógica.
|
|
105
|
+
|
|
106
|
+
### Boot — leitura de config
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// services/persistence/load.ts
|
|
110
|
+
import { GlobalConfigSchema } from "../trading/schema";
|
|
111
|
+
|
|
112
|
+
export function loadGlobalConfig(): GlobalConfig {
|
|
113
|
+
const raw = JSON.parse(fs.readFileSync("global.json", "utf-8"));
|
|
114
|
+
const result = GlobalConfigSchema.safeParse(raw);
|
|
115
|
+
if (!result.success) {
|
|
116
|
+
console.error("FATAL: global.json inválido", result.error.format());
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
return result.data;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Resposta de broker
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const BinanceTickerResponseSchema = z.object({
|
|
127
|
+
symbol: z.string(),
|
|
128
|
+
price: z.string().regex(/^\d+(\.\d+)?$/),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// dentro do BinanceBroker:
|
|
132
|
+
const result = BinanceTickerResponseSchema.parse(response.data);
|
|
133
|
+
return { symbol: result.symbol, priceCents: toCents(result.price) };
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Reload de estado
|
|
137
|
+
|
|
138
|
+
Substituir `Object.setPrototypeOf` (vindo da spec 00) por
|
|
139
|
+
reconstrução validada (a partir de refactor/05):
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// services/persistence/load.ts
|
|
143
|
+
export function loadBotState(): BotState | null {
|
|
144
|
+
if (!fs.existsSync("actualTradeEnviroments.json")) return null;
|
|
145
|
+
const raw = JSON.parse(fs.readFileSync("actualTradeEnviroments.json", "utf-8"));
|
|
146
|
+
return BotStateSchema.parse(raw);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Services
|
|
153
|
+
|
|
154
|
+
### Estrutura de um domínio
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
services/{domínio}/
|
|
158
|
+
├── schema.ts ← entity + derivados
|
|
159
|
+
├── {lógica}.ts ← funções puras
|
|
160
|
+
└── ...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Regras
|
|
164
|
+
|
|
165
|
+
- Funções puras quando possível: input → output, sem side-effects.
|
|
166
|
+
- Side-effects (leitura, escrita, log) ficam em arquivos dedicados:
|
|
167
|
+
`persistence/`, `integrations/`, ou em camada de orquestração.
|
|
168
|
+
- Services podem importar outros services do MESMO nível ou inferiores.
|
|
169
|
+
- Services NUNCA importam `@binance/connector`, `dotenv`, `fs`, `process`.
|
|
170
|
+
- Services NUNCA chamam `console.log`. Logs são responsabilidade
|
|
171
|
+
do orquestrador (loop) ou de logger estruturado.
|
|
172
|
+
|
|
173
|
+
### Exemplo: lógica pura
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// services/trading/limits.ts
|
|
177
|
+
|
|
178
|
+
export function recalculateLimits(input: {
|
|
179
|
+
actualPrice: number;
|
|
180
|
+
waves: WavesSnapshot;
|
|
181
|
+
quantityOfBuyOrdersNotSelled: number;
|
|
182
|
+
config: ConfigValues;
|
|
183
|
+
trade: TradingData;
|
|
184
|
+
}): RecalculatedLimits {
|
|
185
|
+
// ...lógica determinística, sem ed.log, sem process, sem fs
|
|
186
|
+
return {
|
|
187
|
+
maxLimitPriceToBuy,
|
|
188
|
+
minLimitPriceToSell,
|
|
189
|
+
brokeUpFlowWaitingLimitPrice,
|
|
190
|
+
brokeDownFlowWaitingLimitPrice,
|
|
191
|
+
maxLimitPriceToBuy_calc,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Função pura é alvo direto de teste unitário: alimentar inputs,
|
|
197
|
+
verificar outputs.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## BrokerClient (port)
|
|
202
|
+
|
|
203
|
+
Interface no domínio. Implementação em `services/integrations/`.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// domain/broker.ts
|
|
207
|
+
export interface BrokerClient {
|
|
208
|
+
getTickerPrice(symbol: string): Promise<{ symbol: string; price: number }>;
|
|
209
|
+
placeOrder(input: PlaceOrderInput): Promise<PlacedOrder>;
|
|
210
|
+
getAccountState(): Promise<AccountState>;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// services/integrations/binance-broker.ts
|
|
216
|
+
import { Spot } from "@binance/connector";
|
|
217
|
+
|
|
218
|
+
export class BinanceBroker implements BrokerClient {
|
|
219
|
+
constructor(
|
|
220
|
+
private apiKey: string,
|
|
221
|
+
private apiSecret: string
|
|
222
|
+
) {
|
|
223
|
+
this.client = new Spot(apiKey, apiSecret);
|
|
224
|
+
}
|
|
225
|
+
async getTickerPrice(symbol: string) {
|
|
226
|
+
const { data } = await this.client.tickerPrice(symbol);
|
|
227
|
+
const parsed = BinanceTickerResponseSchema.parse(data);
|
|
228
|
+
return { symbol: parsed.symbol, price: Number(parsed.price) };
|
|
229
|
+
}
|
|
230
|
+
// ...
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Por quê
|
|
235
|
+
|
|
236
|
+
- Lógica de trade testável com mock de broker (`InMemoryBroker`).
|
|
237
|
+
- Trocar Binance por outra exchange é trocar 1 arquivo.
|
|
238
|
+
- Validação da resposta vive no adapter, não espalhada.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Erros de domínio
|
|
243
|
+
|
|
244
|
+
Lógica nunca lança `Error` puro. Usa classe específica.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// domain/errors.ts
|
|
248
|
+
export class TradeError extends Error {
|
|
249
|
+
constructor(
|
|
250
|
+
message: string,
|
|
251
|
+
public code: string
|
|
252
|
+
) {
|
|
253
|
+
super(message);
|
|
254
|
+
this.name = "TradeError";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export class SafeLimitError extends TradeError {
|
|
259
|
+
constructor(message: string) {
|
|
260
|
+
super(message, "SAFE_LIMIT_VIOLATED");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export class BrokerError extends TradeError {
|
|
265
|
+
constructor(
|
|
266
|
+
message: string,
|
|
267
|
+
public binanceMsg?: string
|
|
268
|
+
) {
|
|
269
|
+
super(message, "BROKER_ERROR");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export class InvalidConfigError extends TradeError {
|
|
274
|
+
constructor(message: string) {
|
|
275
|
+
super(message, "INVALID_CONFIG");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Regras
|
|
281
|
+
|
|
282
|
+
- Mensagens em pt-BR (consistente com logs existentes).
|
|
283
|
+
- `code` em SCREAMING_SNAKE_CASE.
|
|
284
|
+
- Stack trace preservado: `throw error` ou `throw new XxxError(msg, error)` nunca `throw error.message`.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Mapper toKibana
|
|
289
|
+
|
|
290
|
+
Centralizado em `services/persistence/kibana.ts`.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
export function toKibana<T>(record: T, id: string, index = "tradebot"): KibanaRecord<T> {
|
|
294
|
+
return {
|
|
295
|
+
_index: index,
|
|
296
|
+
_type: "_doc",
|
|
297
|
+
_id: id,
|
|
298
|
+
_score: 1,
|
|
299
|
+
_source: record,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function toKibanaJsonl<T>(records: Array<{ data: T; id: string }>): string {
|
|
304
|
+
return records.map(({ data, id }) => JSON.stringify(toKibana(data, id))).join("\n") + "\n";
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Substitui o `formatDocumentToKibana` espalhado no `index.ts` atual.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Persistência
|
|
313
|
+
|
|
314
|
+
### Save
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
// services/persistence/save.ts
|
|
318
|
+
export function saveBotState(state: BotState) {
|
|
319
|
+
const validated = BotStateSchema.parse(state);
|
|
320
|
+
fs.writeFileSync("actualTradeEnviroments.json", JSON.stringify(validated), "utf-8");
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Validação em save protege contra escrever estado corrompido
|
|
325
|
+
(útil na transição da spec 00 pra 05).
|
|
326
|
+
|
|
327
|
+
### Load
|
|
328
|
+
|
|
329
|
+
Já mostrado em "Validação na fronteira > Reload de estado".
|
|
330
|
+
|
|
331
|
+
### Substituição do `Object.setPrototypeOf`
|
|
332
|
+
|
|
333
|
+
Spec 00 deixou `Object.setPrototypeOf(item, BotEnviromentData.prototype)`
|
|
334
|
+
como solução temporária. Quando schemas Zod existirem (setup/02),
|
|
335
|
+
refactor/05 substitui por:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const state = BotStateSchema.parse(raw);
|
|
339
|
+
// state já é tipado, sem precisar reanexar prototype manualmente
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Classes podem virar funções/objetos puros, ou continuar como classes
|
|
343
|
+
com método estático `from(plain): T` que invoca o construtor.
|
|
344
|
+
Decisão fica pra refactor/05.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Convenções de naming
|
|
349
|
+
|
|
350
|
+
| Item | Padrão | Exemplo |
|
|
351
|
+
| --------------------------- | -------------------------- | --------------------------------------- |
|
|
352
|
+
| Schema Zod | `XxxSchema` | `TradingDataSchema` |
|
|
353
|
+
| Type inferido | `Xxx` | `TradingData` |
|
|
354
|
+
| Funções | `camelCase` verbo + objeto | `recalculateLimits`, `loadBotState` |
|
|
355
|
+
| Classes (quando inevitável) | `PascalCase` substantivo | `BinanceBroker`, `WavesHistory` |
|
|
356
|
+
| Arquivos | `kebab-case.ts` | `binance-broker.ts`, `waves-history.ts` |
|
|
357
|
+
| Constantes | `SCREAMING_SNAKE_CASE` | `DEFAULT_TIMER_MS` |
|
|
358
|
+
| Erro | `XxxError` | `SafeLimitError` |
|
|
359
|
+
| Test files | `xxx.test.ts` | `waves-history.test.ts` |
|
|
360
|
+
|
|
361
|
+
Exceção: arquivos do código atual (`BotEnviromentData.ts`, etc.) ficam
|
|
362
|
+
como estão até serem refatorados em suas specs respectivas. Nada de
|
|
363
|
+
renomeação fora do escopo da spec corrente.
|