dexbot 1.5.0 → 1.5.2
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/CHANGELOG.md +44 -0
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/trend_detection/package.json +1 -1
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/dexbot.js +5 -5
- package/dist/dexbot.js.map +1 -1
- package/dist/modules/account_orders.d.ts +7 -1
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +34 -1
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/constants.d.ts +2 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +12 -0
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +1 -0
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +19 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +335 -78
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +7 -1
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +54 -4
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +7 -0
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +34 -0
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +199 -5
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/export.d.ts +122 -15
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +310 -46
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +22 -17
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +7 -3
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +98 -28
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +52 -6
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +156 -21
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +17 -2
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +213 -17
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +70 -4
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +136 -5
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +117 -5
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +274 -13
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +14 -0
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +51 -3
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +29 -2
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +207 -35
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/scripts/analyze-credit.d.ts.map +1 -1
- package/dist/scripts/analyze-credit.js +53 -33
- package/dist/scripts/analyze-credit.js.map +1 -1
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +7 -5
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +3 -1
- package/docs/GRID_RECONCILE.md +1 -0
- package/docs/LOGGING.md +3 -0
- package/docs/README.md +2 -2
- package/docs/WORKFLOW.md +1 -1
- package/package.json +1 -1
|
@@ -13,17 +13,18 @@
|
|
|
13
13
|
* - JSON: Sanitized bot settings (excludes private keys)
|
|
14
14
|
*
|
|
15
15
|
* ===============================================================================
|
|
16
|
-
* TABLE OF CONTENTS (
|
|
16
|
+
* TABLE OF CONTENTS (6 exported functions + internal helpers)
|
|
17
17
|
* ===============================================================================
|
|
18
18
|
*
|
|
19
|
-
* PUBLIC EXPORTS (
|
|
19
|
+
* PUBLIC EXPORTS (6 functions)
|
|
20
20
|
* 1. exportBotTrades(botKey, botConfig, outputDir) - Main export function (async)
|
|
21
21
|
* Orchestrates trade extraction and writing CSV/JSON exports
|
|
22
22
|
* Returns: { success, trades_exported, csv_path, settings_path, output_dir, timestamp }
|
|
23
23
|
*
|
|
24
|
-
* 2. parseLogFile(logFilePath) - Parse PM2 log file to extract trades (async)
|
|
25
|
-
* Reads line-by-line, extracts FILL entries
|
|
26
|
-
*
|
|
24
|
+
* 2. parseLogFile(logFilePath, assetContext?) - Parse PM2 log file to extract trades (async)
|
|
25
|
+
* Reads line-by-line, extracts legacy FILL entries AND multi-line
|
|
26
|
+
* FILL DETECTED blocks, links with fee information
|
|
27
|
+
* Returns: Array of trade objects with { timestamp, side, amount, price, proceeds, fee_asset, fee_amount, order_id }
|
|
27
28
|
*
|
|
28
29
|
* 3. writeTradesCSV(trades, outputPath) - Write trades to CSV file (async)
|
|
29
30
|
* Generates standardized CSV with proper escaping and formatting
|
|
@@ -33,30 +34,136 @@
|
|
|
33
34
|
* Exports bot parameters and configuration (excludes private keys)
|
|
34
35
|
* Returns: { success } or { success: false, error }
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* Returns: { timestamp, side, amount, price, proceeds } or null
|
|
37
|
+
* 5. parseFillLine(line) - Parse legacy fill entry from a single log line
|
|
38
|
+
* Expected format: [TIMESTAMP] [DEBUG] [FILL] side fill: size=X, price=Y, proceeds=Z [order=1.7.N]
|
|
39
|
+
* Returns: { timestamp, side, amount, price, proceeds, order_id? } or null
|
|
40
40
|
*
|
|
41
41
|
* 6. parseFeeLine(line) - Parse fee information from log line
|
|
42
42
|
* Expected format: [TIMESTAMP] [INFO] [FEES] N maker fills @ FEE ASSET = TOTAL
|
|
43
43
|
* Returns: { timestamp, count, fee_per_fill, fee_asset, total_fee } or null
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* INTERNAL HELPERS
|
|
46
|
+
* - isoToUnixSeconds(iso) - Shared ISO-timestamp to unix-seconds conversion
|
|
47
|
+
* - extractLogTimestamp(line) - Pull unix-seconds timestamp from a log line prefix
|
|
48
|
+
* - rawToHuman(raw, precision) - Scale satoshi amounts (soft variant of
|
|
49
|
+
* blockchainToFloat that returns NaN instead of throwing; kept local so
|
|
50
|
+
* the offline exporter avoids pulling in utils/math's dependency tree)
|
|
51
|
+
* - buildOrientationHypothesis(...) - Trade fields for one pays-asset orientation
|
|
52
|
+
* - deriveTradeFromFillBlock(block, assetContext) - Derive a trade from a
|
|
53
|
+
* parsed FILL DETECTED block (side from pays-asset orientation, price from
|
|
54
|
+
* human-scaled amounts). Bare pre-#22 blocks without asset IDs are
|
|
55
|
+
* disambiguated via log-distance to the grid median price.
|
|
56
|
+
* Returns: FillEntry or null (with reason logged by caller)
|
|
57
|
+
* - resolveExportAssetContext(botKey, botConfig) - Resolve offline asset metadata
|
|
58
|
+
* Reads profiles/orders/<botKey>.json for assetA/assetB ids+precisions and
|
|
59
|
+
* grid price stats. No chain access (export stays offline/browser-safe).
|
|
60
|
+
* Returns: { assetA, assetB, gridMedianPrice?, ... } or null
|
|
48
61
|
*
|
|
49
62
|
* ===============================================================================
|
|
50
63
|
*
|
|
51
64
|
* LOG FORMAT PATTERNS:
|
|
52
|
-
*
|
|
53
|
-
*
|
|
65
|
+
* Legacy fill line (kept for backward compat; may carry an order= suffix):
|
|
66
|
+
* [2026-01-15T15:29:06.185Z] [DEBUG] [FILL] sell fill: size=0.0316, price=1791.30065898866, proceeds=56.60510082 BTS
|
|
67
|
+
* Legacy fee line:
|
|
68
|
+
* [2026-01-15T15:29:06.185Z] [INFO] [FEES] BTS fees calculated: 1 maker fills @ 0.04826000 BTS = 0.04826000 BTS
|
|
69
|
+
* Current fill block (emitted by dexbot_fill_runtime.ts; asset IDs in parens
|
|
70
|
+
* present on logs written after the issue-#22 fix, absent on older logs):
|
|
71
|
+
* [2026-08-28T18:19:51.192Z] [INFO] [DEXBot] ===== FILL DETECTED =====
|
|
72
|
+
* [2026-08-28T18:19:51.192Z] [INFO] [DEXBot] Order ID: 1.7.573895253
|
|
73
|
+
* [2026-08-28T18:19:51.192Z] [INFO] [DEXBot] Pays: 1036496 (1.3.5537), Receives: 74071699 (1.3.0)
|
|
74
|
+
* [2026-08-28T18:19:51.192Z] [INFO] [DEXBot] Block: 113873892 (History ID: 1.11.1395319063)
|
|
54
75
|
*
|
|
55
76
|
* CSV HEADER:
|
|
56
77
|
* unix, price, amount, side, fee_asset, fee_amount, order_id
|
|
57
78
|
*
|
|
58
79
|
* ===============================================================================
|
|
59
80
|
*/
|
|
81
|
+
/**
|
|
82
|
+
* Parse a legacy fill line from PM2 log file
|
|
83
|
+
* Expected format: [2026-01-15T15:29:06.185Z] [DEBUG] [FILL] sell fill: size=0.0316, price=1791.30065898866, proceeds=56.60510082 BTS
|
|
84
|
+
* An optional trailing `order=1.7.123` suffix is captured when present.
|
|
85
|
+
* @param {string} line - Raw log line
|
|
86
|
+
* @returns {Object|null} Parsed fill object or null on no match
|
|
87
|
+
*/
|
|
88
|
+
interface FillCore {
|
|
89
|
+
side: any;
|
|
90
|
+
amount: number;
|
|
91
|
+
price: number;
|
|
92
|
+
proceeds: number;
|
|
93
|
+
}
|
|
94
|
+
interface FillEntry extends FillCore {
|
|
95
|
+
timestamp: number;
|
|
96
|
+
fee_asset: string;
|
|
97
|
+
fee_amount: number;
|
|
98
|
+
order_id?: string;
|
|
99
|
+
}
|
|
100
|
+
interface FeeEntry {
|
|
101
|
+
count: number;
|
|
102
|
+
fee_per_fill: number;
|
|
103
|
+
fee_asset: any;
|
|
104
|
+
total_fee: number;
|
|
105
|
+
timestamp: number;
|
|
106
|
+
}
|
|
107
|
+
declare function parseFillLine(line: string): FillEntry | null;
|
|
108
|
+
/**
|
|
109
|
+
* Parse fee information from log line
|
|
110
|
+
* Expected format: [2026-01-15T15:29:06.185Z] [INFO] [FEES] BTS fees calculated: 1 maker fills @ 0.04826000 BTS = 0.04826000 BTS
|
|
111
|
+
* @param {string} line - Raw log line
|
|
112
|
+
* @returns {Object|null} Parsed fee object or null on no match
|
|
113
|
+
*/
|
|
114
|
+
declare function parseFeeLine(line: string): FeeEntry | null;
|
|
115
|
+
/**
|
|
116
|
+
* Offline asset metadata for trade derivation (no chain access — export stays
|
|
117
|
+
* offline/browser-safe). Resolved from the bot's persisted grid file.
|
|
118
|
+
*/
|
|
119
|
+
interface ExportAsset {
|
|
120
|
+
id: string;
|
|
121
|
+
precision: number;
|
|
122
|
+
symbol?: string;
|
|
123
|
+
}
|
|
124
|
+
interface ExportAssetContext {
|
|
125
|
+
assetA: ExportAsset;
|
|
126
|
+
assetB: ExportAsset;
|
|
127
|
+
/** Median grid price (assetB per assetA), used to disambiguate bare pre-#22 blocks. */
|
|
128
|
+
gridMedianPrice?: number;
|
|
129
|
+
}
|
|
130
|
+
interface FillBlock {
|
|
131
|
+
timestamp?: number;
|
|
132
|
+
orderId?: string;
|
|
133
|
+
paysRaw?: number;
|
|
134
|
+
paysAssetId?: string;
|
|
135
|
+
receivesRaw?: number;
|
|
136
|
+
receivesAssetId?: string;
|
|
137
|
+
blockNum?: number;
|
|
138
|
+
historyId?: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Derive a trade from a parsed FILL DETECTED block.
|
|
142
|
+
* Side comes from the pays/receives asset orientation, price from
|
|
143
|
+
* precision-scaled amounts. Bare pre-#22 blocks carry no asset IDs; they are
|
|
144
|
+
* disambiguated by log-distance to the grid median price (the two hypotheses
|
|
145
|
+
* sit symmetric around 10^(precA-precB) on a log scale, so the grid median —
|
|
146
|
+
* which tracks the market — picks the right one decisively; near-ties are
|
|
147
|
+
* treated as ambiguous and skipped).
|
|
148
|
+
* @returns FillEntry or null when the block cannot be derived exactly
|
|
149
|
+
*/
|
|
150
|
+
declare function deriveTradeFromFillBlock(block: FillBlock, ctx: ExportAssetContext | null): FillEntry | null;
|
|
151
|
+
/**
|
|
152
|
+
* Resolve offline asset metadata + grid price stats for a bot from its
|
|
153
|
+
* persisted grid file (profiles/orders/<botKey>.json). Falls back to scanning
|
|
154
|
+
* all persisted grids for a matching meta.key/meta.name. No chain access.
|
|
155
|
+
* @returns ExportAssetContext or null when nothing usable was found
|
|
156
|
+
*/
|
|
157
|
+
declare function resolveExportAssetContext(botKey: string, botConfig: any): ExportAssetContext | null;
|
|
158
|
+
/**
|
|
159
|
+
* Parse PM2 log file to extract trades
|
|
160
|
+
* Handles legacy single-line [FILL] entries AND multi-line FILL DETECTED
|
|
161
|
+
* blocks (current fill-runtime format, see issue #22).
|
|
162
|
+
* @param {string} logFilePath - Path to PM2 log file
|
|
163
|
+
* @param {ExportAssetContext|null} [assetContext] - Offline asset metadata for block derivation
|
|
164
|
+
* @returns {Promise<Array>} Array of trade objects
|
|
165
|
+
*/
|
|
166
|
+
declare function parseLogFile(logFilePath: any, assetContext?: ExportAssetContext | null): Promise<any[]>;
|
|
60
167
|
/**
|
|
61
168
|
* Export bot trades from PM2 log file to CSV
|
|
62
169
|
* @param {string} botKey - Bot identifier (e.g., 'aaa-bbb-0')
|
|
@@ -83,5 +190,5 @@ declare function exportBotTrades(botKey: any, botConfig: any, outputDir?: any):
|
|
|
83
190
|
output_dir?: undefined;
|
|
84
191
|
timestamp?: undefined;
|
|
85
192
|
}>;
|
|
86
|
-
export { exportBotTrades };
|
|
193
|
+
export { exportBotTrades, parseLogFile, parseFillLine, parseFeeLine, deriveTradeFromFillBlock, resolveExportAssetContext };
|
|
87
194
|
//# sourceMappingURL=export.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../modules/order/export.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../modules/order/export.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+EG;AAqBH;;;;;;GAMG;AACH,UAAU,QAAQ;IACd,IAAI,EAAE,GAAG,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,SAAU,SAAQ,QAAQ;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,QAAQ;IACd,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,GAAG,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAOD,iBAAS,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAerD;AAED;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAYnD;AAED;;;GAGG;AACH,UAAU,WAAW;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,kBAAkB;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,UAAU,SAAS;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AA0CD;;;;;;;;;GASG;AACH,iBAAS,wBAAwB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAgDpG;AAED;;;;;GAKG;AACH,iBAAS,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI,CAuD5F;AAED;;;;;;;GAOG;AACH,iBAAe,YAAY,CAAC,WAAW,EAAE,GAAG,EAAE,YAAY,GAAE,kBAAkB,GAAG,IAAW,kBAiH3F;AAyFD;;;;;;GAMG;AACH,iBAAe,eAAe,CAAC,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAE,GAAiB;;;;;;;;;;;;;;;;;;GAmEvF;AAED,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,CAAA"}
|
|
@@ -13,17 +13,18 @@
|
|
|
13
13
|
* - JSON: Sanitized bot settings (excludes private keys)
|
|
14
14
|
*
|
|
15
15
|
* ===============================================================================
|
|
16
|
-
* TABLE OF CONTENTS (
|
|
16
|
+
* TABLE OF CONTENTS (6 exported functions + internal helpers)
|
|
17
17
|
* ===============================================================================
|
|
18
18
|
*
|
|
19
|
-
* PUBLIC EXPORTS (
|
|
19
|
+
* PUBLIC EXPORTS (6 functions)
|
|
20
20
|
* 1. exportBotTrades(botKey, botConfig, outputDir) - Main export function (async)
|
|
21
21
|
* Orchestrates trade extraction and writing CSV/JSON exports
|
|
22
22
|
* Returns: { success, trades_exported, csv_path, settings_path, output_dir, timestamp }
|
|
23
23
|
*
|
|
24
|
-
* 2. parseLogFile(logFilePath) - Parse PM2 log file to extract trades (async)
|
|
25
|
-
* Reads line-by-line, extracts FILL entries
|
|
26
|
-
*
|
|
24
|
+
* 2. parseLogFile(logFilePath, assetContext?) - Parse PM2 log file to extract trades (async)
|
|
25
|
+
* Reads line-by-line, extracts legacy FILL entries AND multi-line
|
|
26
|
+
* FILL DETECTED blocks, links with fee information
|
|
27
|
+
* Returns: Array of trade objects with { timestamp, side, amount, price, proceeds, fee_asset, fee_amount, order_id }
|
|
27
28
|
*
|
|
28
29
|
* 3. writeTradesCSV(trades, outputPath) - Write trades to CSV file (async)
|
|
29
30
|
* Generates standardized CSV with proper escaping and formatting
|
|
@@ -33,24 +34,44 @@
|
|
|
33
34
|
* Exports bot parameters and configuration (excludes private keys)
|
|
34
35
|
* Returns: { success } or { success: false, error }
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* Returns: { timestamp, side, amount, price, proceeds } or null
|
|
37
|
+
* 5. parseFillLine(line) - Parse legacy fill entry from a single log line
|
|
38
|
+
* Expected format: [TIMESTAMP] [DEBUG] [FILL] side fill: size=X, price=Y, proceeds=Z [order=1.7.N]
|
|
39
|
+
* Returns: { timestamp, side, amount, price, proceeds, order_id? } or null
|
|
40
40
|
*
|
|
41
41
|
* 6. parseFeeLine(line) - Parse fee information from log line
|
|
42
42
|
* Expected format: [TIMESTAMP] [INFO] [FEES] N maker fills @ FEE ASSET = TOTAL
|
|
43
43
|
* Returns: { timestamp, count, fee_per_fill, fee_asset, total_fee } or null
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* INTERNAL HELPERS
|
|
46
|
+
* - isoToUnixSeconds(iso) - Shared ISO-timestamp to unix-seconds conversion
|
|
47
|
+
* - extractLogTimestamp(line) - Pull unix-seconds timestamp from a log line prefix
|
|
48
|
+
* - rawToHuman(raw, precision) - Scale satoshi amounts (soft variant of
|
|
49
|
+
* blockchainToFloat that returns NaN instead of throwing; kept local so
|
|
50
|
+
* the offline exporter avoids pulling in utils/math's dependency tree)
|
|
51
|
+
* - buildOrientationHypothesis(...) - Trade fields for one pays-asset orientation
|
|
52
|
+
* - deriveTradeFromFillBlock(block, assetContext) - Derive a trade from a
|
|
53
|
+
* parsed FILL DETECTED block (side from pays-asset orientation, price from
|
|
54
|
+
* human-scaled amounts). Bare pre-#22 blocks without asset IDs are
|
|
55
|
+
* disambiguated via log-distance to the grid median price.
|
|
56
|
+
* Returns: FillEntry or null (with reason logged by caller)
|
|
57
|
+
* - resolveExportAssetContext(botKey, botConfig) - Resolve offline asset metadata
|
|
58
|
+
* Reads profiles/orders/<botKey>.json for assetA/assetB ids+precisions and
|
|
59
|
+
* grid price stats. No chain access (export stays offline/browser-safe).
|
|
60
|
+
* Returns: { assetA, assetB, gridMedianPrice?, ... } or null
|
|
48
61
|
*
|
|
49
62
|
* ===============================================================================
|
|
50
63
|
*
|
|
51
64
|
* LOG FORMAT PATTERNS:
|
|
52
|
-
*
|
|
53
|
-
*
|
|
65
|
+
* Legacy fill line (kept for backward compat; may carry an order= suffix):
|
|
66
|
+
* [2026-01-15T15:29:06.185Z] [DEBUG] [FILL] sell fill: size=0.0316, price=1791.30065898866, proceeds=56.60510082 BTS
|
|
67
|
+
* Legacy fee line:
|
|
68
|
+
* [2026-01-15T15:29:06.185Z] [INFO] [FEES] BTS fees calculated: 1 maker fills @ 0.04826000 BTS = 0.04826000 BTS
|
|
69
|
+
* Current fill block (emitted by dexbot_fill_runtime.ts; asset IDs in parens
|
|
70
|
+
* present on logs written after the issue-#22 fix, absent on older logs):
|
|
71
|
+
* [2026-08-28T18:19:51.192Z] [INFO] [DEXBot] ===== FILL DETECTED =====
|
|
72
|
+
* [2026-08-28T18:19:51.192Z] [INFO] [DEXBot] Order ID: 1.7.573895253
|
|
73
|
+
* [2026-08-28T18:19:51.192Z] [INFO] [DEXBot] Pays: 1036496 (1.3.5537), Receives: 74071699 (1.3.0)
|
|
74
|
+
* [2026-08-28T18:19:51.192Z] [INFO] [DEXBot] Block: 113873892 (History ID: 1.11.1395319063)
|
|
54
75
|
*
|
|
55
76
|
* CSV HEADER:
|
|
56
77
|
* unix, price, amount, side, fee_asset, fee_amount, order_id
|
|
@@ -75,18 +96,23 @@ function getReadline() {
|
|
|
75
96
|
return _readline;
|
|
76
97
|
}
|
|
77
98
|
const exportLogger = new Logger('Export');
|
|
99
|
+
/** Shared ISO-8601 to unix-seconds conversion for all log parsers. */
|
|
100
|
+
function isoToUnixSeconds(iso) {
|
|
101
|
+
return new Date(iso).getTime() / TIMING.MILLISECONDS_PER_SECOND;
|
|
102
|
+
}
|
|
78
103
|
function parseFillLine(line) {
|
|
79
|
-
const fillMatch = line.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\].*\[FILL\]\s+(\w+)\s+fill:\s+size=([\d.]+),\s+price=([\d.]+),\s+proceeds=([\d.]+)
|
|
104
|
+
const fillMatch = line.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\].*\[FILL\]\s+(\w+)\s+fill:\s+size=([\d.]+),\s+price=([\d.]+),\s+proceeds=([\d.]+)(?:\s+[A-Za-z.]+)?(?:\s+order=(1\.7\.\d+))?/);
|
|
80
105
|
if (!fillMatch)
|
|
81
106
|
return null;
|
|
82
107
|
return {
|
|
83
|
-
timestamp:
|
|
108
|
+
timestamp: isoToUnixSeconds(fillMatch[1]), // Unix timestamp in seconds
|
|
84
109
|
side: fillMatch[2], // 'buy' or 'sell'
|
|
85
110
|
amount: parseFloat(fillMatch[3]), // Size in base asset
|
|
86
111
|
price: parseFloat(fillMatch[4]), // Execution price
|
|
87
112
|
proceeds: parseFloat(fillMatch[5]), // Proceeds/cost
|
|
88
113
|
fee_asset: 'BTS',
|
|
89
|
-
fee_amount: 0
|
|
114
|
+
fee_amount: 0,
|
|
115
|
+
order_id: fillMatch[6] || ''
|
|
90
116
|
};
|
|
91
117
|
}
|
|
92
118
|
/**
|
|
@@ -104,35 +130,204 @@ function parseFeeLine(line) {
|
|
|
104
130
|
fee_per_fill: parseFloat(feeMatch[3]),
|
|
105
131
|
fee_asset: feeMatch[4],
|
|
106
132
|
total_fee: parseFloat(feeMatch[5]),
|
|
107
|
-
timestamp:
|
|
133
|
+
timestamp: isoToUnixSeconds(feeMatch[1])
|
|
108
134
|
};
|
|
109
135
|
}
|
|
136
|
+
const FILL_BLOCK_HEADER_RE = /=+\s*FILL DETECTED\s*=+/;
|
|
137
|
+
const LOG_TIMESTAMP_RE = /\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\]/;
|
|
138
|
+
const FILL_BLOCK_ORDER_RE = /Order ID:\s*(1\.7\.\d+)/;
|
|
139
|
+
// Matches both variants (asset IDs in parens were added by the issue-#22 fix):
|
|
140
|
+
// Pays: 1036496 (1.3.5537), Receives: 74071699 (1.3.0) (new)
|
|
141
|
+
// Pays: 1036496, Receives: 74071699 (old/bare)
|
|
142
|
+
const FILL_BLOCK_AMOUNTS_RE = /Pays:\s*(\S+?)(?:\s*\(\s*([\d.]+)\s*\))?\s*,?\s*Receives:\s*(\S+?)(?:\s*\(\s*([\d.]+)\s*\))?\s*$/;
|
|
143
|
+
const FILL_BLOCK_HEIGHT_RE = /Block:\s*(\d+)\s*\(History ID:\s*([^)]+)\)/;
|
|
144
|
+
/** Extract a unix-seconds timestamp from a log line prefix, or undefined. */
|
|
145
|
+
function extractLogTimestamp(line) {
|
|
146
|
+
const m = line.match(LOG_TIMESTAMP_RE);
|
|
147
|
+
return m ? isoToUnixSeconds(m[1]) : undefined;
|
|
148
|
+
}
|
|
149
|
+
function rawToHuman(raw, precision) {
|
|
150
|
+
return raw / Math.pow(10, precision);
|
|
151
|
+
}
|
|
110
152
|
/**
|
|
111
|
-
*
|
|
112
|
-
* @param {
|
|
113
|
-
* @
|
|
114
|
-
* @returns {void}
|
|
153
|
+
* Build a trade hypothesis for one pays-asset orientation.
|
|
154
|
+
* @param {boolean} paysIsA - True when the pays asset is assetA (a SELL)
|
|
155
|
+
* @returns Trade fields or null when amounts are not positive finite numbers
|
|
115
156
|
*/
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
function buildOrientationHypothesis(paysRaw, receivesRaw, paysIsA, ctx) {
|
|
158
|
+
const paysHuman = rawToHuman(paysRaw, paysIsA ? ctx.assetA.precision : ctx.assetB.precision);
|
|
159
|
+
const receivesHuman = rawToHuman(receivesRaw, paysIsA ? ctx.assetB.precision : ctx.assetA.precision);
|
|
160
|
+
if (!Number.isFinite(paysHuman) || paysHuman <= 0 || !Number.isFinite(receivesHuman) || receivesHuman <= 0)
|
|
161
|
+
return null;
|
|
162
|
+
// Price convention (assetB per assetA), mirroring parseChainOrder:
|
|
163
|
+
// SELL pays A and receives B, BUY pays B and receives A.
|
|
164
|
+
const price = paysIsA ? receivesHuman / paysHuman : paysHuman / receivesHuman;
|
|
165
|
+
if (!Number.isFinite(price) || price <= 0)
|
|
166
|
+
return null;
|
|
167
|
+
return {
|
|
168
|
+
side: paysIsA ? 'sell' : 'buy',
|
|
169
|
+
amount: paysIsA ? paysHuman : receivesHuman, // base-asset (A) quantity traded
|
|
170
|
+
price,
|
|
171
|
+
proceeds: paysIsA ? receivesHuman : paysHuman // quote-asset (B) value: BTS proceeds on SELL, BTS cost on BUY
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Derive a trade from a parsed FILL DETECTED block.
|
|
176
|
+
* Side comes from the pays/receives asset orientation, price from
|
|
177
|
+
* precision-scaled amounts. Bare pre-#22 blocks carry no asset IDs; they are
|
|
178
|
+
* disambiguated by log-distance to the grid median price (the two hypotheses
|
|
179
|
+
* sit symmetric around 10^(precA-precB) on a log scale, so the grid median —
|
|
180
|
+
* which tracks the market — picks the right one decisively; near-ties are
|
|
181
|
+
* treated as ambiguous and skipped).
|
|
182
|
+
* @returns FillEntry or null when the block cannot be derived exactly
|
|
183
|
+
*/
|
|
184
|
+
function deriveTradeFromFillBlock(block, ctx) {
|
|
185
|
+
if (!ctx)
|
|
186
|
+
return null;
|
|
187
|
+
if (block.timestamp == null)
|
|
188
|
+
return null;
|
|
189
|
+
if (block.paysRaw == null || block.receivesRaw == null)
|
|
190
|
+
return null;
|
|
191
|
+
let hypothesis = null;
|
|
192
|
+
if (block.paysAssetId && block.receivesAssetId) {
|
|
193
|
+
const sellOrientation = block.paysAssetId === ctx.assetA.id && block.receivesAssetId === ctx.assetB.id;
|
|
194
|
+
const buyOrientation = block.paysAssetId === ctx.assetB.id && block.receivesAssetId === ctx.assetA.id;
|
|
195
|
+
if (!sellOrientation && !buyOrientation)
|
|
196
|
+
return null; // foreign-market fill, not ours
|
|
197
|
+
hypothesis = buildOrientationHypothesis(block.paysRaw, block.receivesRaw, sellOrientation, ctx);
|
|
198
|
+
}
|
|
199
|
+
else if (block.paysAssetId || block.receivesAssetId) {
|
|
200
|
+
// Half-present block (truncated log line): exactly one side survived.
|
|
201
|
+
// Pays present orients directly; receives present inverts
|
|
202
|
+
// (receives A => pays B => BUY; receives B => pays A => SELL).
|
|
203
|
+
// A present side matching neither local asset is a foreign-market fill.
|
|
204
|
+
let paysIsA;
|
|
205
|
+
if (block.paysAssetId) {
|
|
206
|
+
if (block.paysAssetId !== ctx.assetA.id && block.paysAssetId !== ctx.assetB.id)
|
|
207
|
+
return null;
|
|
208
|
+
paysIsA = block.paysAssetId === ctx.assetA.id;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
if (block.receivesAssetId !== ctx.assetA.id && block.receivesAssetId !== ctx.assetB.id)
|
|
212
|
+
return null;
|
|
213
|
+
paysIsA = block.receivesAssetId === ctx.assetB.id;
|
|
214
|
+
}
|
|
215
|
+
hypothesis = buildOrientationHypothesis(block.paysRaw, block.receivesRaw, paysIsA, ctx);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Bare pre-#22 block: no asset orientation logged.
|
|
219
|
+
const sellHyp = buildOrientationHypothesis(block.paysRaw, block.receivesRaw, true, ctx);
|
|
220
|
+
const buyHyp = buildOrientationHypothesis(block.paysRaw, block.receivesRaw, false, ctx);
|
|
221
|
+
if (sellHyp && !buyHyp)
|
|
222
|
+
hypothesis = sellHyp;
|
|
223
|
+
else if (buyHyp && !sellHyp)
|
|
224
|
+
hypothesis = buyHyp;
|
|
225
|
+
else if (sellHyp && buyHyp && ctx.gridMedianPrice && ctx.gridMedianPrice > 0) {
|
|
226
|
+
const sellDist = Math.abs(Math.log(sellHyp.price) - Math.log(ctx.gridMedianPrice));
|
|
227
|
+
const buyDist = Math.abs(Math.log(buyHyp.price) - Math.log(ctx.gridMedianPrice));
|
|
228
|
+
if (Math.abs(sellDist - buyDist) < 0.5)
|
|
229
|
+
return null; // ambiguous, skip
|
|
230
|
+
hypothesis = sellDist < buyDist ? sellHyp : buyHyp;
|
|
125
231
|
}
|
|
232
|
+
else
|
|
233
|
+
return null;
|
|
126
234
|
}
|
|
235
|
+
if (!hypothesis)
|
|
236
|
+
return null;
|
|
237
|
+
return {
|
|
238
|
+
timestamp: block.timestamp,
|
|
239
|
+
...hypothesis,
|
|
240
|
+
fee_asset: 'BTS',
|
|
241
|
+
fee_amount: 0,
|
|
242
|
+
order_id: block.orderId || ''
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Resolve offline asset metadata + grid price stats for a bot from its
|
|
247
|
+
* persisted grid file (profiles/orders/<botKey>.json). Falls back to scanning
|
|
248
|
+
* all persisted grids for a matching meta.key/meta.name. No chain access.
|
|
249
|
+
* @returns ExportAssetContext or null when nothing usable was found
|
|
250
|
+
*/
|
|
251
|
+
function resolveExportAssetContext(botKey, botConfig) {
|
|
252
|
+
const readGridFile = (fileName) => {
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(storage.readFile(path.join(PATHS.ORDERS_DIR, fileName), 'utf8'));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
// Exact bot file first, then a meta.key/meta.name scan over all grids.
|
|
261
|
+
let dirFiles = [];
|
|
262
|
+
try {
|
|
263
|
+
dirFiles = storage.readdir(PATHS.ORDERS_DIR).filter((f) => f.endsWith('.json'));
|
|
264
|
+
}
|
|
265
|
+
catch { /* no orders dir */ }
|
|
266
|
+
const exactName = `${botKey}.json`;
|
|
267
|
+
const ordered = [exactName, ...dirFiles.filter((f) => f !== exactName)];
|
|
268
|
+
const wantKey = String(botKey).toLowerCase();
|
|
269
|
+
const wantName = String(botConfig?.name || botKey).toLowerCase();
|
|
270
|
+
let doc = null;
|
|
271
|
+
for (const fileName of ordered) {
|
|
272
|
+
const candidate = readGridFile(fileName);
|
|
273
|
+
if (!candidate || !candidate.assets)
|
|
274
|
+
continue;
|
|
275
|
+
if (fileName === exactName) {
|
|
276
|
+
doc = candidate;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
const metaKey = candidate.meta?.key ? String(candidate.meta.key).toLowerCase() : '';
|
|
280
|
+
const metaName = candidate.meta?.name ? String(candidate.meta.name).toLowerCase() : '';
|
|
281
|
+
if ((metaKey && metaKey === wantKey) || (metaName && metaName === wantName)) {
|
|
282
|
+
doc = candidate;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!doc)
|
|
287
|
+
return null;
|
|
288
|
+
const assetA = doc.assets?.assetA;
|
|
289
|
+
const assetB = doc.assets?.assetB;
|
|
290
|
+
if (!assetA?.id || !assetB?.id)
|
|
291
|
+
return null;
|
|
292
|
+
if (!Number.isFinite(assetA.precision) || !Number.isFinite(assetB.precision))
|
|
293
|
+
return null;
|
|
294
|
+
const ctx = {
|
|
295
|
+
assetA: { id: String(assetA.id), precision: assetA.precision, symbol: assetA.symbol },
|
|
296
|
+
assetB: { id: String(assetB.id), precision: assetB.precision, symbol: assetB.symbol }
|
|
297
|
+
};
|
|
298
|
+
try {
|
|
299
|
+
const grid = Array.isArray(doc.grid) ? doc.grid : Object.values(doc.grid || {});
|
|
300
|
+
const prices = grid
|
|
301
|
+
.map((o) => Number(o?.price))
|
|
302
|
+
.filter((p) => Number.isFinite(p) && p > 0)
|
|
303
|
+
.sort((a, b) => a - b);
|
|
304
|
+
if (prices.length > 0)
|
|
305
|
+
ctx.gridMedianPrice = prices[Math.floor(prices.length / 2)];
|
|
306
|
+
}
|
|
307
|
+
catch { /* grid stats are best-effort */ }
|
|
308
|
+
return ctx;
|
|
127
309
|
}
|
|
128
310
|
/**
|
|
129
311
|
* Parse PM2 log file to extract trades
|
|
312
|
+
* Handles legacy single-line [FILL] entries AND multi-line FILL DETECTED
|
|
313
|
+
* blocks (current fill-runtime format, see issue #22).
|
|
130
314
|
* @param {string} logFilePath - Path to PM2 log file
|
|
315
|
+
* @param {ExportAssetContext|null} [assetContext] - Offline asset metadata for block derivation
|
|
131
316
|
* @returns {Promise<Array>} Array of trade objects
|
|
132
317
|
*/
|
|
133
|
-
async function parseLogFile(logFilePath) {
|
|
318
|
+
async function parseLogFile(logFilePath, assetContext = null) {
|
|
134
319
|
const fills = [];
|
|
135
320
|
const fees = [];
|
|
321
|
+
let pendingBlock = null;
|
|
322
|
+
let skippedBlocks = 0;
|
|
323
|
+
const finalizeBlock = (block) => {
|
|
324
|
+
// Fee defaults already set by deriveTradeFromFillBlock.
|
|
325
|
+
const trade = deriveTradeFromFillBlock(block, assetContext);
|
|
326
|
+
if (trade)
|
|
327
|
+
fills.push(trade);
|
|
328
|
+
else
|
|
329
|
+
skippedBlocks++;
|
|
330
|
+
};
|
|
136
331
|
try {
|
|
137
332
|
const fileStream = storage.createReadStream(logFilePath);
|
|
138
333
|
const rl = getReadline().createInterface({
|
|
@@ -140,29 +335,87 @@ async function parseLogFile(logFilePath) {
|
|
|
140
335
|
crlfDelay: Infinity
|
|
141
336
|
});
|
|
142
337
|
for await (const line of rl) {
|
|
143
|
-
// Parse fill lines
|
|
338
|
+
// Parse legacy fill lines (fee defaults already set by the parser)
|
|
144
339
|
const fill = parseFillLine(line);
|
|
145
340
|
if (fill) {
|
|
146
|
-
fill.fee_asset = 'BTS'; // Default, will be overwritten if found
|
|
147
|
-
fill.fee_amount = 0;
|
|
148
341
|
fills.push(fill);
|
|
149
342
|
continue;
|
|
150
343
|
}
|
|
151
|
-
//
|
|
344
|
+
// Collect fee lines; linked to fills in one pass after the loop
|
|
152
345
|
const fee = parseFeeLine(line);
|
|
153
346
|
if (fee) {
|
|
154
347
|
fees.push(fee);
|
|
155
|
-
linkFillWithFee(fills, fees);
|
|
156
348
|
continue;
|
|
157
349
|
}
|
|
350
|
+
// FILL DETECTED block header starts a new block (a missing Block
|
|
351
|
+
// line in the previous block finalizes it best-effort).
|
|
352
|
+
if (FILL_BLOCK_HEADER_RE.test(line)) {
|
|
353
|
+
if (pendingBlock)
|
|
354
|
+
finalizeBlock(pendingBlock);
|
|
355
|
+
pendingBlock = { timestamp: extractLogTimestamp(line) };
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (pendingBlock) {
|
|
359
|
+
const ts = extractLogTimestamp(line);
|
|
360
|
+
if (ts !== undefined && pendingBlock.timestamp === undefined)
|
|
361
|
+
pendingBlock.timestamp = ts;
|
|
362
|
+
const orderMatch = line.match(FILL_BLOCK_ORDER_RE);
|
|
363
|
+
if (orderMatch) {
|
|
364
|
+
pendingBlock.orderId = orderMatch[1];
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const amountsMatch = line.match(FILL_BLOCK_AMOUNTS_RE);
|
|
368
|
+
if (amountsMatch) {
|
|
369
|
+
const pays = Number(amountsMatch[1]);
|
|
370
|
+
const receives = Number(amountsMatch[3]);
|
|
371
|
+
if (Number.isFinite(pays))
|
|
372
|
+
pendingBlock.paysRaw = pays;
|
|
373
|
+
if (Number.isFinite(receives))
|
|
374
|
+
pendingBlock.receivesRaw = receives;
|
|
375
|
+
if (amountsMatch[2])
|
|
376
|
+
pendingBlock.paysAssetId = amountsMatch[2];
|
|
377
|
+
if (amountsMatch[4])
|
|
378
|
+
pendingBlock.receivesAssetId = amountsMatch[4];
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const blockMatch = line.match(FILL_BLOCK_HEIGHT_RE);
|
|
382
|
+
if (blockMatch) {
|
|
383
|
+
pendingBlock.blockNum = parseInt(blockMatch[1], 10);
|
|
384
|
+
pendingBlock.historyId = blockMatch[2].trim();
|
|
385
|
+
// Block line is the last line the emitter writes per fill.
|
|
386
|
+
finalizeBlock(pendingBlock);
|
|
387
|
+
pendingBlock = null;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
158
391
|
}
|
|
159
|
-
//
|
|
392
|
+
// Best-effort: close a trailing block whose Block line never arrived.
|
|
393
|
+
if (pendingBlock)
|
|
394
|
+
finalizeBlock(pendingBlock);
|
|
395
|
+
if (skippedBlocks > 0) {
|
|
396
|
+
exportLogger.warn(`Skipped ${skippedBlocks} fill block(s) that could not be derived (missing asset context or ambiguous orientation)`);
|
|
397
|
+
}
|
|
398
|
+
// Link any remaining fills with fees. A FEES line aggregates N fills
|
|
399
|
+
// ("N maker fills @ per_fill = total"), so each fill in the window gets
|
|
400
|
+
// the per-fill share (== total when N == 1, preserving legacy behavior
|
|
401
|
+
// for the single-fill case); the closest fee line in the window wins.
|
|
160
402
|
for (const fill of fills) {
|
|
161
403
|
if (fill.fee_amount === 0 && fees.length > 0) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
404
|
+
let best = null;
|
|
405
|
+
let bestDist = Infinity;
|
|
406
|
+
for (const f of fees) {
|
|
407
|
+
const dist = Math.abs(f.timestamp - fill.timestamp);
|
|
408
|
+
if (dist < 5 && dist < bestDist) {
|
|
409
|
+
best = f;
|
|
410
|
+
bestDist = dist;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (best) {
|
|
414
|
+
fill.fee_asset = best.fee_asset;
|
|
415
|
+
const perFill = Number.isFinite(best.fee_per_fill)
|
|
416
|
+
? best.fee_per_fill
|
|
417
|
+
: (best.count > 0 ? best.total_fee / best.count : best.total_fee);
|
|
418
|
+
fill.fee_amount = perFill;
|
|
166
419
|
}
|
|
167
420
|
}
|
|
168
421
|
}
|
|
@@ -174,7 +427,7 @@ async function parseLogFile(logFilePath) {
|
|
|
174
427
|
}
|
|
175
428
|
}
|
|
176
429
|
/**
|
|
177
|
-
* Write trades to CSV file
|
|
430
|
+
* Write trades to CSV file for local analysis/ tooling
|
|
178
431
|
* @param {Array} trades - Array of trade objects
|
|
179
432
|
* @param {string} outputPath - Path to output CSV file
|
|
180
433
|
* @returns {Promise<Object>} { success: boolean, count: number } or { success: false, error: string }
|
|
@@ -270,7 +523,12 @@ async function exportBotTrades(botKey, botConfig, outputDir = './exports') {
|
|
|
270
523
|
let logFilePath = null;
|
|
271
524
|
try {
|
|
272
525
|
const logFiles = storage.readdir(logsDir);
|
|
273
|
-
const
|
|
526
|
+
const want = String(botKey).toLowerCase();
|
|
527
|
+
// Case-insensitive: log files use the display name (XRP-BTS.log)
|
|
528
|
+
// while bot keys are lowercase (xrp-bts). Prefer the exact
|
|
529
|
+
// "<key>.log" hit, fall back to any non-error log containing the key.
|
|
530
|
+
const exactLog = logFiles.find((f) => f.toLowerCase() === `${want}.log`);
|
|
531
|
+
const matchingLog = exactLog || logFiles.find((f) => f.toLowerCase().includes(want) && f.endsWith('.log') && !f.toLowerCase().includes('error'));
|
|
274
532
|
if (matchingLog) {
|
|
275
533
|
logFilePath = path.join(logsDir, matchingLog);
|
|
276
534
|
}
|
|
@@ -278,8 +536,14 @@ async function exportBotTrades(botKey, botConfig, outputDir = './exports') {
|
|
|
278
536
|
catch (err) {
|
|
279
537
|
exportLogger.warn(`Could not read logs directory: ${getErrorMessage(err)}`);
|
|
280
538
|
}
|
|
539
|
+
// Resolve offline asset metadata so FILL DETECTED blocks can be
|
|
540
|
+
// derived to side/price/amount (issue #22). Pure file reads, no chain.
|
|
541
|
+
const assetContext = resolveExportAssetContext(botKey, botConfig);
|
|
542
|
+
if (!assetContext) {
|
|
543
|
+
exportLogger.warn(`No persisted asset metadata found for ${botKey}; fill blocks without asset IDs will be skipped`);
|
|
544
|
+
}
|
|
281
545
|
// Parse trades from log file
|
|
282
|
-
const trades = logFilePath ? await parseLogFile(logFilePath) : [];
|
|
546
|
+
const trades = logFilePath ? await parseLogFile(logFilePath, assetContext) : [];
|
|
283
547
|
if (trades.length === 0) {
|
|
284
548
|
exportLogger.warn(`No trades found in log file for ${botKey}`);
|
|
285
549
|
}
|
|
@@ -307,5 +571,5 @@ async function exportBotTrades(botKey, botConfig, outputDir = './exports') {
|
|
|
307
571
|
};
|
|
308
572
|
}
|
|
309
573
|
}
|
|
310
|
-
export { exportBotTrades };
|
|
574
|
+
export { exportBotTrades, parseLogFile, parseFillLine, parseFeeLine, deriveTradeFromFillBlock, resolveExportAssetContext };
|
|
311
575
|
//# sourceMappingURL=export.js.map
|