-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathreports.ts
More file actions
329 lines (323 loc) · 14.9 KB
/
reports.ts
File metadata and controls
329 lines (323 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/* eslint-disable no-console */
import fs from 'fs';
import type { Portfolio, Order, ClosedOrderResponse } from './types.d.ts';
interface ReportInstance {
getExecuted(count: number, known: ClosedOrders): Promise<any>;
capGains(price?: number, sym?: string,
ISOStart?: string, buyFile?: string, outFile?: string): Promise<void>;
reset?(): void; // Adding this based on usage in your code
}
interface ReportConstructor {
(bot: any): ReportInstance;
}
interface ClosedOrders {
orders: { [orderId: string]: Order };
offset: number;
hasFirst?: boolean;
forward?: boolean;
keysFwd?(): string[];
keysBkwd?(): string[];
}
interface ExternalBuy {
date: string;
cost: number;
amount: number;
}
function ReportCon(bot) {
const TxIDPattern = /[0-9A-Z]{6}-[0-9A-Z]{5}-[0-9A-Z]{6}/;
const KRAKEN_GCO_MAX = 50;
// This function adds an iterator and the "forward" key to iterate
// through the orders property backwards or forwards.
// KeysFwd means getting an array in which the zeroth
// element is the trade with the lowest timestamp (first).
// ---------------------------------------------------------------
function keyOrder(keyed, pattern = TxIDPattern) {
// eslint-disable-next-line no-param-reassign
// keyed.forward = true;
const keys = Object.keys(keyed.orders);
const K = keys.filter((x) => (pattern.test(x)));
if (keys.length > K.length) { // Keys that don't match.
console.log("Ignoring", (keys.length - K.length, "non-matching keys:"));
console.log(keys.filter((x) => (!pattern.test(x))));
}
// "A negative value indicates that a should come before b."
K.sort((a, b) => (keyed.orders[a].closetm - keyed.orders[b].closetm));
const Kr = K.toReversed();
// eslint-disable-next-line no-param-reassign
keyed.keysFwd = () => K;
// eslint-disable-next-line no-param-reassign
keyed.keysBkwd = () => Kr;
// eslint-disable-next-line no-param-reassign
// keyed.orders[Symbol.iterator] = function* reportOrder () {
// (keyed.forward ? K : Kr).forEach(yield);}
return keyed;
}
let transientOffset = 0;
// This function will collect count executed orders in reverse chronological order,
// first the most recent (ofs=0) and then earlier history (ofs from known).
async function getExecuted(count: number, known: ClosedOrders): Promise<any> {
let offset = 0; // Since the last call, one or more orders may have executed.
let midway = false;
let closed: ClosedOrders = { offset: 0, forward: false, orders: {} };
let earliestInBatch = false;
const preCount = Object.keys(known.orders).length;
closed.hasFirst = known.hasFirst || known.offset === -1;
// Is old format or not collected yet?
if (!Object.prototype.hasOwnProperty.call(known, 'orders')) {
Object.assign(known, { offset: 0, forward: false, orders: {} });
console.log("known passed has no 'orders' property.");
}
if (Object.keys(known.orders).length > Object.keys(closed.orders).length)
Object.assign(closed, known);
while (count > 0) {
console.log("Known:", Object.keys(known.orders).length, "Closed:", Object.keys(closed.orders).length, [known.offset, closed.offset]);
// eslint-disable-next-line no-await-in-loop
const mixed: ClosedOrderResponse = await bot.kapi(['ClosedOrders', { ofs: offset, closetime: 'close' }]);
const total = mixed.result.count;
if (mixed.error.length > 0) {
console.log("Errors:\n", mixed.error.join("\n"));
}
console.log("At", offset, "of", total, "results.");
if (total === 0)
return keyOrder(closed);
// If more orders closed since our previous call, they will
// push everything further down the list and cause some old
// orders to be reported again. These duplicates do not
// cause a problem when the list of closed orders is an object
// because the later assignments to the TxID (key) overwrite
// the earlier ones.
// ClosedOrders might return pending, open, canceled, and expired
// orders too. Remove them.
// ------------------------------------------------------
const executed = Object.entries(mixed.result.closed).filter((e) => (e[1].vol_exec !== "0.00000000"));
const rCount = Object.keys(mixed.result.closed).length;
const elen = executed.length;
const missing = executed.find(e => (known.orders[e[0]] == undefined));
offset += rCount;
closed.offset = Math.max(transientOffset, offset);
// eslint-disable-next-line no-param-reassign
count -= elen;
if (elen > 0) {
const missed = missing ? `, including ${missing[0]} which was missing.` : '.';
console.log(`Retrieved ${elen} executed orders${missed}`);
}
Object.assign(closed.orders, Object.fromEntries(executed));
if (!closed.hasFirst) { // But do we have the latest yet?
if (rCount < KRAKEN_GCO_MAX) { // We must have reached the earliest order.
if (closed.offset < total) // Should be impossible, so...
throw Error(`${offset} still < ${total} API returns < 50`);
console.log(`Total Executed orders collected: ${Object.keys(closed.orders).length}`);
closed.hasFirst = true;
break;
}
else if (!midway && !missing) {
console.log(`Jumping to the end... (${known.offset})`);
offset = known.offset; // so jump to the end.
closed.offset = offset;
midway = true;
}
}
else if (offset < known.offset && count > 0) {
// We have the earliest order, but more may have
// executed since the last save. Let's at least
// collect what was asked for if we haven't passed the
// recorded offset.
console.log("Checking for gaps in order collection...");
}
else {
// If you wait long enough between running the bot, you
// might get here without having collected all orders.
closed.offset = known.offset;
if (missing == undefined)
break; // All new orders collected.
}
transientOffset = Math.max(offset, transientOffset);
offset = transientOffset;
}
transientOffset = Math.max(offset, transientOffset);
closed = keyOrder(closed);
const closedIDs = Object.keys(closed.orders);
// Store closed orders in portfolio
console.log(`Had ${preCount} @ ${closed.offset}, now ${closedIDs.length} orders.`);
bot.getPortfolio().Closed = closed;
if (preCount < closedIDs.length || !closed.hasFirst) {
console.log(`(Re-?)Saving ${closedIDs.length} closed orders @ ${closed.offset}.`);
bot.save();
}
return closed;
}
function yearStart() {
const now = new Date();
const ret = new Date(now.getFullYear(), 0, 1);
return ret.toISOString();
}
// This ensures all orders have been retrieved
// and provides whatever information it can about the process.
async function capGains(price = '100', sym = "BTC", ISOStart = yearStart(), buyFile = '', outFile = 'capGains.csv') {
const started = Date.now();
const notBefore = new Date(ISOStart).getTime();
const closed = bot.getPortfolio().Closed;
const previousCount = closed.orders.length;
// Let's not go back before the beginning of the year
while (closed.orders[closed.keysFwd()[0]].closetm > notBefore / 1000) {
// eslint-disable-next-line no-await-in-loop
await getExecuted(50, closed);
if(previousCount == closed.orders.length)
break;
else if (Date.now() - started > 30000) {
console.log("I'm stopping after thirty seconds.");
return;
}
}
console.log("I've collected", closed.keysFwd().length, " orders, and that goes back to ", new Date(closed.orders[closed.keysFwd()[0]].closetm * 1000));
let borrowed = 0;
let total = 0;
// keyList will include keys of the form "EBx" where x is an
// external buy saved in exb.
const exb: Order[] = [];
const keyList: string[] = Array.from(closed.keysFwd());
if (buyFile > '' ) {
if (fs.existsSync(buyFile)) {
// Put the file contents into a string
const externalBuys = fs.readFileSync(buyFile).toString();
const eb = JSON.parse(externalBuys);
let ebi = 0;
eb.forEach(b => {
const price = (b.cost / b.amount).toFixed(2);
const extBuy: Order = {
closetm: new Date(b.date).getTime() / 1000,
remaining: b.amount,
descr: { price, type: 'buy' },
price,
cost: b.cost,
fee: 0,
vol_exec: String(b.amount),
ebi
};
borrowed += b.amount;
exb.push(extBuy);
ebi += 1;
});
exb.sort((a, b) => (a.closetm - b.closetm));
let ei = 0;
while (ei < exb.length) {
const ii = keyList.findIndex(k => (((/^eb[0-9]+$/.test(k)
? exb[Number(k.slice(2))]
: closed.orders[k]).closetm) > exb[ei].closetm));
keyList.splice(ii, 0, `eb${ei}`);
ei += 1;
}
} else {
console.log(buyFile + " was not found, aborting.")
return;
}
}
keyList.forEach(oid => {
const t = /^eb[0-9]+$/.test(oid)
? exb[Number(oid.slice(2))]
: closed.orders[oid];
if (t.closetm < notBefore / 1000)
return;
const del = t.descr.type == 'buy'
? Number(t.vol_exec)
: -Number(t.vol_exec);
total += del;
if (total < 0)
borrowed = Math.min(borrowed, total);
});
// Validity requires that purchase happened earlier
// and that purchase hasn't been fully consumed:
function getValidMatch(maxTS) {
return bbp.find(x => (x.remaining > Number.EPSILON && x.closetm < maxTS));
}
let sbt: Order[] = [], bbp: Order[] = []; // SellsByTime, BuysByPrice
keyList.forEach((oid, ti) => {
const t = /^eb[0-9]+$/.test(oid)
? exb[Number(oid.slice(2))]
: closed.orders[oid];
t.ti = ti;
if (t.closetm < notBefore / 1000)
return;
(t.descr.type == "buy" ? bbp : sbt).push(t);
if (t.descr.type == "buy")
Object.assign(t, { remaining: t.vol_exec });
});
// Already sorted! sbt.sort((a,b) => (a.closetm - b.closetm));
if (borrowed < 0) {
console.log(`Using ${price} as price of ${-borrowed} ${sym}.`);
const cost = Number((Number(price) * -borrowed).toFixed(2));
const bootStrapM: Order = {
closetm: 0,
remaining: -borrowed.toFixed(8),
descr: { price: String(price), type: 'buy' },
// ti: bbp.length,
cost,
price,
fee: 0,
vol_exec: (-borrowed).toFixed(8)
};
bbp.push(bootStrapM);
}
bbp.sort((a, b) => (Number(b.descr.price) - Number(a.descr.price)));
try {
fs.writeFileSync(outFile, `Property,Acquired,Sold,Proceeds,Cost,Gain/Loss\n`);
} catch(err) {
console.error(err);
}
let accProceeds = 0;
let accCost = 0;
let accGL = 0;
let unPurchased = 0;
sbt.forEach(s => {
if (s.closetm < notBefore / 1000)
return;
let volume = Number(s.vol_exec);
const netProceeds = s.cost - s.fee;
while (volume > 10*Number.EPSILON) {
let liquidated = 0;
let m = getValidMatch(s.closetm);
let proportion = 1;
if( m === undefined ) {
console.log(`Capital Gains cannot be computed because more assets`
+` were purchased than sold. Try specifying a file that lists`
+` your purchases (${volume} closed ${new Date(1000*s.closetm)}).`);
return;
} else if (m.remaining >= volume) {
m.remaining -= volume;
liquidated += volume * Number(m.descr.price);
proportion = volume / Number(s.vol_exec);
volume = 0;
} else {
liquidated += m.remaining * Number(m.descr.price);
proportion = m.remaining / Number(s.vol_exec);
volume -= m.remaining;
m.remaining = 0;
}
const adjCost = (liquidated + Number(m.fee * proportion)).toFixed(2);
const gl = ((netProceeds * proportion) - Number(adjCost));
const gain = gl.toFixed(2);
if (netProceeds * proportion > 0.005) try {
fs.appendFileSync(outFile, `${sym},${new Date(m.closetm * 1000)
.toISOString().slice(0, 10)},${new Date(s.closetm * 1000)
.toISOString().slice(0, 10)},${(netProceeds * proportion)
.toFixed(2)},${adjCost},${gain}\n`);
} catch (err) {
console.error(err);
}
accProceeds += Number(netProceeds * proportion);
accCost += Number(adjCost);
accGL += Number(gl);
}
});
const agl = accGL < 0 ? `(${-accGL.toFixed(2)})` : accGL.toFixed(2);
fs.appendFileSync(outFile, `,,,${accProceeds.toFixed(2)},${accCost.toFixed(2)},${agl}\n`);
console.log(`Wrote ${outFile}.`);
}
// Clear state for testing.
// This function reinitializes all private data.
// Only transientOffset exists at this time.
function reset() { transientOffset = 0; }
return { getExecuted, capGains, reset };
}
export default ReportCon;
//# sourceMappingURL=reports.js.map