Files: f1521af2d73e96acaa4e96dfc60dbfc50373845a / mtgox2ledger
6817 bytesRaw
1 | #!/usr/bin/env perl |
2 | |
3 | use strict; |
4 | use warnings; |
5 | use Time::Piece; |
6 | |
7 | my $broker_account = 'Assets:Broker:Mt.Gox'; |
8 | my $fee_account = 'Expenses:Broker:Mt.Gox'; |
9 | my $usd_account = 'Assets'; |
10 | my $btc_account = 'Assets'; |
11 | my $other_account = 'Assets'; |
12 | |
13 | my $dateformat = '%Y-%m-%d %H:%M:%S'; |
14 | my $max_buffer_size = 30; |
15 | |
16 | if ($#ARGV != 1) { |
17 | print STDERR "Usage: $0 mtgox-history_BTC.csv mtgox-history_USD.csv\n"; |
18 | exit 1; |
19 | } |
20 | |
21 | $| = 1; |
22 | |
23 | # Make sure the arguments are the right ones |
24 | my ($btc_file, $usd_file) = @ARGV; |
25 | if ($btc_file !~ /btc/i or $btc_file =~ /usd/i or |
26 | $usd_file !~ /usd/i or $usd_file =~ /btc/i) { |
27 | print STDERR "Is $btc_file the BTC history and $usd_file the USD history?\n"; |
28 | my $input = <STDIN>; |
29 | defined $input and $input =~ /^y/ or exit 1; |
30 | } |
31 | |
32 | sub read_entry { |
33 | my $fd = shift; |
34 | my $prev_line = shift // ''; |
35 | my $line = <$fd>; |
36 | return unless defined $line; |
37 | $line = $prev_line . $line; |
38 | my ($index, $date, $type, $info, $value, $balance) = |
39 | map { s/^"(.*)"$/$1/; $_ } split ',', $line; |
40 | |
41 | # retry if caught a header |
42 | if ($index eq 'Index') { |
43 | return read_entry($fd) |
44 | } |
45 | |
46 | # combine multiple lines as theyare sometimes broken |
47 | if (!defined $value) { |
48 | return read_entry($fd, $line); |
49 | } |
50 | |
51 | $date = Time::Piece->strptime($date, $dateformat); |
52 | my $date_str = $date->ymd('/'); |
53 | |
54 | my $address; |
55 | my ($short_info, $tid) = $info =~ /^(.*): \[tid:(.*?)\]/; |
56 | if (!$tid) { |
57 | $address = $info; |
58 | $tid = ''; |
59 | } |
60 | if ($type eq 'spent' or $type eq 'out' or |
61 | $type eq 'fee' or $type eq 'withdraw') { |
62 | $value *= -1; |
63 | } |
64 | |
65 | return { |
66 | index => $index, |
67 | type => $type, |
68 | date => $date, |
69 | date_str => $date_str, |
70 | tid => $tid, |
71 | value => $value, |
72 | address => $address, |
73 | info => $short_info, |
74 | }; |
75 | } |
76 | |
77 | open BTC, '<', $btc_file or die("Unable to open BTC file: $!"); |
78 | open USD, '<', $usd_file or die("Unable to open USD file: $!"); |
79 | |
80 | my ($btc_entry, $usd_entry); |
81 | |
82 | sub get_next_entry { |
83 | if (!eof USD and (!defined $usd_entry or |
84 | (defined $btc_entry and |
85 | $btc_entry->{date} > $usd_entry->{date}))) { |
86 | |
87 | $usd_entry = read_entry \*USD; |
88 | $usd_entry->{currency} = 'USD'; |
89 | return $usd_entry; |
90 | |
91 | } elsif (!eof BTC) { |
92 | $btc_entry = read_entry \*BTC; |
93 | $btc_entry->{currency} = 'BTC'; |
94 | return $btc_entry; |
95 | } |
96 | } |
97 | |
98 | # Buffer for entries |
99 | my @buffer; |
100 | |
101 | # Does an entry match a tid and type |
102 | sub entry_matches { |
103 | my ($entry, $tid, $type_n) = @_; |
104 | my $type = $entry->{type}; |
105 | return ($entry->{tid} eq $tid) && |
106 | ($type_n > 0 ? ($type eq 'in' or $type eq 'earned') : |
107 | $type_n == 0 ? ($type eq 'out' or $type eq 'spent') : |
108 | $type_n < 0 ? ($type eq 'fee') : 0); |
109 | } |
110 | |
111 | # Get the next entry, optionally of a specific tid and type, with buffering |
112 | sub get_buffered_entry { |
113 | my ($tid, $type) = @_; |
114 | if (!defined $tid) { |
115 | # get any next entry |
116 | return shift(@buffer) // get_next_entry(); |
117 | } |
118 | |
119 | # find a particular kind of entry |
120 | # buffering non-matching ones |
121 | my ($matching, $entry); |
122 | for my $entry (@buffer) { |
123 | if (entry_matches($entry, $tid, $type)) { |
124 | $matching = $entry; |
125 | last; |
126 | } |
127 | } |
128 | if ($matching) { |
129 | # remove matching entry from buffer |
130 | @buffer = grep { $_ != $matching } @buffer; |
131 | return $matching; |
132 | } |
133 | |
134 | do { |
135 | $entry = get_next_entry(); |
136 | if ($entry and !entry_matches($entry, $tid, $type)) { |
137 | push @buffer, $entry; |
138 | } else { |
139 | return $entry; |
140 | } |
141 | } while (defined $entry); |
142 | |
143 | return $entry; |
144 | } |
145 | |
146 | sub format_money { |
147 | my ($amount, $currency) = @_; |
148 | if ($currency eq 'USD') { |
149 | return sprintf("\$%f", $amount); |
150 | } else { |
151 | return sprintf("%.8f ", $amount) . $currency; |
152 | } |
153 | } |
154 | |
155 | # Print a sale transaction consisting of input, output, and fee |
156 | sub print_sale { |
157 | my ($in, $out, $fee) = @_; |
158 | my $fee_amount = -$fee->{value} if $fee; |
159 | my $in_amount = $in->{value}; |
160 | my $out_amount = $out->{value}; |
161 | if (!$fee) { |
162 | } elsif ($in->{currency} eq $fee->{currency}) { |
163 | $in_amount -= $fee_amount; |
164 | } else { |
165 | $out_amount -= $fee_amount; |
166 | } |
167 | printf "%s %s\n", $in->{date_str}, $in->{info}; |
168 | printf " %-32s %16s\n", $broker_account, format_money($in_amount, $in->{currency}); |
169 | printf " %-32s %16s\n", $broker_account, format_money($out_amount, $out->{currency}); |
170 | printf " %-32s %16s\n", $fee_account, format_money($fee_amount, $fee->{currency}) if ($fee); |
171 | print "\n"; |
172 | } |
173 | |
174 | # Print a single transaction |
175 | sub print_entry { |
176 | my $entry = shift; |
177 | return unless $entry; |
178 | |
179 | my $type = $entry->{type}; |
180 | my $currency = $entry->{currency}; |
181 | my $account; |
182 | my $description; |
183 | |
184 | if ($currency eq 'USD') { |
185 | $account = $btc_account; |
186 | if ($type eq 'in' or $type eq 'deposit') { |
187 | $description = 'Deposit'; |
188 | } elsif ($type eq 'out' or $type eq 'withdraw') { |
189 | $description = 'Withdraw'; |
190 | } elsif ($type eq 'fee') { |
191 | $description = 'Fee'; |
192 | $account = $fee_account; |
193 | } else { |
194 | die("Unknown USD transaction type $type tid $entry->{tid}"); |
195 | } |
196 | |
197 | } elsif ($currency eq 'BTC') { |
198 | $account = $usd_account; |
199 | my $address; |
200 | if ($type eq 'in' or $type eq 'deposit') { |
201 | $description = 'Deposit'; |
202 | } elsif ($type eq 'out' or $type eq 'withdraw') { |
203 | $description = 'Withdraw'; |
204 | } elsif ($type eq 'fee') { |
205 | $description = 'Fee'; |
206 | $account = $fee_account; |
207 | } else { |
208 | die('Unknown BTC transaction type'); |
209 | } |
210 | } |
211 | |
212 | printf "%s %s\n", $entry->{date_str}, $description; |
213 | printf " %-32s %16s\n", $broker_account, format_money($entry->{value}, $currency); |
214 | printf " %s\n\n", $other_account; |
215 | } |
216 | |
217 | # Process the entries |
218 | while (my $entry = get_buffered_entry()) { |
219 | my $type = $entry->{type}; |
220 | |
221 | if (my $tid = $entry->{tid}) { |
222 | # if it has a tid, look for a set of three entries: |
223 | # BTC bought: in, fee, spent |
224 | # BTC sold: earned, fee, out |
225 | |
226 | my $in_entry = ($type eq 'in' or $type eq 'earned') ? |
227 | $entry : get_buffered_entry($tid, 1, $entry->{date}); |
228 | my $out_entry = ($type eq 'out' or $type eq 'spent') ? |
229 | $entry : get_buffered_entry($tid, 0, $entry->{date}); |
230 | my $fee_entry = ($type eq 'fee') ? |
231 | $entry : get_buffered_entry($tid, -1, $entry->{date}); |
232 | |
233 | if ($in_entry and $out_entry) { |
234 | # Render the transaction |
235 | print_sale($in_entry, $out_entry, $fee_entry); |
236 | } else { |
237 | die("Unknown tid"); |
238 | } |
239 | |
240 | } else { |
241 | print_entry($entry); |
242 | } |
243 | } |
244 | |
245 |
Built with git-ssb-web