Анализ логов протокола FIX

Сразу оговорюсь - тематика будет интересна не всем, просто хотел показать один из аспектов использования perl.

Если Вы занимаетесь разработкой ПО для торговых систем, иногда может возникнуть необходимость анализа логов FIX протокола.
Причин может быть несколько, самая простая - определить, насколько соответствуют показатели разрабатываемого приложения действительности. То-есть: Ваша база данных Вам говорит одно, мол, отправлено было столько-то ордеров, открыто столько-то, закрыто столько-то, а на самом деле все может быть иначе.
Если показатели, полученные при анализе базы данных будут отличаться от показателей, полученных при анализе логов FIX engine - пора бить тревогу. Кто-то врет, и скорее всего этот кто-то - Ваше приложение.

Лог-файл FIX сервера обычно представляет собой набор текстовых строк. Выбор инструмента для их разбора пришел сразу - конечно же, perl:). Одна из проблем состоит в том, что поддержки FIX для perl просто нет. Существует большое количество серверов, написанных на всевозможных языках(в основном, это Java или C++), среди которых можно встретить даже Ruby и Python(я так понимаю, дань моде), но горячо любимого нами Perl нет. Ладно, не проблема. Раз нет библиотек для работы с самим протоколом, написать разбор текстовых данных мы сможем.

Была задача анализа файла торгов с FXCM. Основное приложение с нашей стороны было написано на Java с использованием движка QuickFIX. Полученный данные торгов складируются в отдельном файле, растущем достаточно быстро. На момент запуска тестового скрипта размер лог файла превышал 1.5 GB. Достаточно быстро был разработан скрипт, пробегающийся по всем строкам, выбирающий все необходимые типы сообщения и анализирующий состояния ордеров.
Результат(отчет объема продаж за день) выводится на печать с помощью format(вот и живой пример к теме http://kiev.pm.org/node/215 :)).

Сам скрипт далеко не идеален ввиду жесткой привязки переменных, да и есть прямая зависимость от FXCM(они несколько расширили стандартный протокол своими типами переменных), но общая идея более-менее ясна.

В любом случае, скрипт сделал свою работу, и сделал ее быстро. Я думаю, что выбор perl в качестве инструмента анализа был более чем оправдан ввиду скорости разработки и общей удобочитаемости приложения. Выводы - я был очень доволен.

Код скрипта:

#!/usr/bin/perl -w
use strict;
use Order;
my $log_file = $ARGV[0];
my %data;
open (LOG, $log_file) or die "$!\n";
while() {
    $_ =~ /35=(\w\D)/;
    if($1 eq "AP") {
        #We are looking for PositionReport message only
        process_position_report();
    }
}
print_volume_report();
sub process_position_report {
    my @tags = split(/\x01/);
    my %message;
    foreach (@tags) {
        my ($key, $value) = split(/=/);
        $message{$key} = $value;
    }
    #Get transaction date
    my $date = (split(/-/,$message{60}))[0];
    my $symbols = $data{$date};
    unless(defined($symbols)){
        $symbols = {};
        $data{$date} = $symbols;
    }
    #Get Symbol name
    my $symbol = $message{55};
    my $orders = $symbols->{$symbol};
    unless(defined($orders)) {
        $orders = {};
        $symbols->{$symbol} = $orders;
    }
    #Get ClOrdID
    my $order_id = $message{11};
    #Get FXCMPosID
    my $external_id = $message{9041};
    my ($size, $side);
    #Here we get Size and Side of Position
    if(defined($message{705})) {
        $size = $message{705};
        $side = "S";
    }else{
        $size = $message{704};
        $side = "B";
    }
    my $order = $orders->{$external_id};
    unless(defined($order)) {
        $order = Order->new();
        $order->id($order_id);
        $order->symbol($symbol);
        $order->open_date($message{60});
        $order->external_id($external_id);
        $order->size($size);
        $order->side($side);
        $orders->{$external_id} = $order;
    }else{
        if(defined($message{9052})) {
            $order->size(2*$order->size);
        }
    }
}
sub print_volume_report {
    my %volume_data;
    my ($symbol, $symbol_volume);
    my $total_volume = 0;
format =
    @<<<<<<<< @>>>>>>>>>>
    $symbol, $symbol_volume
.
    foreach(keys %data) {
        my $volume_symbols = {};
        $volume_data{$_} = $volume_symbols;
        my $symbols = $data{$_};
        foreach my $symbol (keys (%$symbols)) {
            my $volume = 0;
            my $orders = $symbols->{$symbol};
            foreach(keys(%$orders)) {
                $volume+=$orders->{$_}->size;
            }
            $volume_symbols->{$symbol} = $volume;
        }
    }
    foreach(keys %volume_data) {
        my $day_volume = 0;
        print $_,"\n";
        my $symbols = $volume_data{$_};
        foreach(keys(%$symbols)) {
            $symbol = $_;
            $day_volume+=$symbols->{$_};
            $symbol_volume = format_size($symbols->{$_});
            write;
        }
        #Print footer. Day volume
        $symbol = "Total";
        $symbol_volume = format_size($day_volume);
        write;
    }
}
sub format_size {
    my $size = shift;
    if($size < 999999) {
        return ($size/1000)."K";
    }elsif($size < 999999999) {
        return ($size / 1000000)."M";
    }else{
        return ($size / 1000000000)."B";
    }
}

Код используемого класса Order:

package Order;
use strict;
use base 'Class::Accessor::Fast';
sub new {
    my $class = shift;
    my $self = bless {}, $class;
    return $self;
}
__PACKAGE__->mk_accessors(qw/
    id           symbol      side        size
    open_date    open_price  close_date  close_price
    type         external_id
/);
1;

Пример вывода:

20080813
    EUR/AUD          1.9M
    EUR/CHF          1.5M
    USD/CHF          2.1M
    EUR/USD        252.6M
    EUR/GBP            2M
    NZD/USD          2.7M
    EUR/NZD          2.9M
    AUD/USD          1.4M
    EUR/JPY         79.1M
    USD/JPY         32.9M
    USD/SGD          4.3M
    USD/HKD          1.3M
    USD/CAD          8.2M
    EUR/CAD          2.4M
    CAD/JPY          4.7M
    GBP/USD          2.4M
    Total          402.4M
20080811
    CHF/JPY          1.2M
    EUR/CHF         26.7M
    EUR/USD       1.0603B
    GBP/JPY            1M
    NZD/USD         26.7M
    USD/SGD         25.9M
    USD/CAD         26.8M
    EUR/CAD         24.2M
    CAD/JPY          1.2M
    EUR/AUD         28.3M
    USD/CHF           30M
    EUR/GBP         24.8M
    EUR/NZD         26.1M
    EUR/JPY        691.9M
    AUD/USD         24.1M
    NZD/JPY          1.6M
    USD/JPY        461.5M
    USD/HKD         17.1M
    GBP/USD         29.9M
    Total         2.5293B
20080812
    CHF/JPY         21.1M
    EUR/CHF           27M
    EUR/USD       1.3162B
    GBP/JPY         19.2M
    NZD/USD         24.9M
    USD/SGD         29.6M
    USD/CAD         47.9M
    CAD/JPY         32.3M
    EUR/CAD         37.3M
    EUR/AUD         33.3M
    USD/CHF         29.2M
    EUR/GBP         36.4M
    EUR/NZD         38.7M
    AUD/USD         24.6M
    EUR/JPY        332.1M
    NZD/JPY         22.7M
    USD/JPY        211.6M
    USD/HKD         14.3M
    GBP/USD         25.8M
    Total         2.3242B

килобайты, мегабайты...

Когда я тоже выводил размер в килобайтах и мегабайтах в зависимости от размера, то делил на 1024 и 1024**1024 соответственно :)

Это не

Это не килобайты:) Это тысячи и миллионы долларов:)

"Настоящий

"Настоящий программист считает что в килограмме 1024 грамма" :)