外為レートをTatsumakiで流してみた

ニコ生コメントに続き性懲りもなく新しいネタを考えました。

外国為替の通貨(ドル円など)を非同期に表示させてみます。

一般的にFX取引会社のHPではFlashやらJavaを使って通貨データをリアルタイムに表示していてそれ以外の方法(Ajax等)を使ってる所はほとんど見かけない。 実際問題Flashとかで表示させたほうがブラウザの依存もなく簡単だから?まぁ当然ですが・・・・

前回のニコ生アプリで Tatsumaki の素晴らしさを体感してしまった "Tatsumaki厨" な私ですが別に後悔はしていません><

Tatsumaki のような非同期アクセスに対応したWAFがあるから作ってみたくなっただけ(山登りと同じ)なんだってばよ!

Tatsumaki の良い所は MessageQueue にデータを入れれば後はWAFがよきに計らってくれる所にあります。 どのクライアントのセッションなのかとか余計なことは考えずに済むんですね。

MessageQueueを押さえておけばTatsumakiを使ったアプリを作るのは簡単です。基本構造がほとんど同じですからね (キリッ★

外為レートをリアルタイム(似非)表示するサンプルアプリ

http://fx.omakase.org/fx/100/

fx.psgi

use Tatsumaki::Application;
use FxHandler;
use FxStreamHandler;
use File::Basename;

my $app = Tatsumaki::Application->new(
    [
        '/fx/poll/(\d+)' => 'FxStreamHandler',
        '/fx/100/'       => 'FxHandler',
    ]
);

$app->template_path( dirname(__FILE__) . "/template" );
$app->static_path(dirname(__FILE__) . "/static");

return $app;

fx.html

% my $uid = $_[0]->{handler}->args->[0];
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<link rel="stylesheet" type="text/css" href="/static/gaitame.css" />
<title>Foreign Exchange Currency Stream for Tatsumaki</title>
<script type="text/javascript" src="/static/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="/static/jquery.ev.js"></script>
<!--
<script type="text/javascript" src="/static/jquery.dump.js"></script>
-->
<script type="text/javascript">
var style = new Array(2);
style[0]='<div class="pink">';
style[1]='<div class="white">';
var currency_span = '</span><span class="cn">';
var i = 0;
var title_header = 
    '<div id="currency_title"><span class="cr">currency' + 
    currency_span + 'ask' + currency_span + 'bid' + 
    currency_span + 'max' + currency_span + 
    'min</span></div>';

jQuery( function ($) {
    $.ev.loop('/fx/poll/' +"100"+ '?session=' + Math.random(), {

    currency : function(ev) {

//      $("#last").html("<div>"  + $.dump(ev) + "</div>");
        var tmp = "";

        jQuery.each(ev.currency, function() {
            tmp += style[i++ % 2];
            tmp +=  '<span class="cr">' + this.name + 
                    currency_span + this.ask + 
                    currency_span + this.bid + 
                    currency_span + this.max + 
                    currency_span + this.min + 
                    '</span></div>';
            $('#gaitame').html(  title_header + tmp );
        });
    }
    });
});

</script>
</head>
<body>
<div id="last"></div>
<h1>Foreign Exchange</h1>
<h2>Currency Stream for Tatsumaki</h2>
<div id="gaitame"></div>
</body>
</html>

FxHandler.pm

package FxHandler;

use parent qw(Tatsumaki::Handler);

sub get {
    my $self = shift;
    my $uid = shift || "100";
    $self->render('fx.html');
}

1;

FxStreamHandler

package FxStreamHandler;

use strict;
use warnings;
use utf8;
use parent qw(Tatsumaki::Handler);
__PACKAGE__->asynchronous(1);
use Tatsumaki::MessageQueue;
use Tatsumaki::Error;
use FX::AnyEvent;

my %fx_stream;

sub create_stream {
    my $self = shift;
    my $uid  = shift;
    my $cnt ||= 0;

    my $mq = Tatsumaki::MessageQueue->instance($uid);

    $fx_stream{$uid} ||= FX::AnyEvent->new(

        on_currency => sub {
            my $currency = shift;
            $mq->publish( { type => 'currency', currency => $currency } );
          }
    );
}

sub get {
    my $self = shift;
    my $uid  = shift;

    my $session = $self->request->param('session')
      or Tatsumaki::Error::HTTP->throw( 500, "'session' needed" );

    $fx_stream{$uid} or $self->create_stream($uid);

    my $mq = Tatsumaki::MessageQueue->instance($uid);

    $mq->poll_once(
        $session,
        sub {
            my @events_published = @_;
            $self->write( \@events_published );

            $self->response->header(
                'Cache-Control' => 'no-store, no-cache, must-revalidate,'
                  . 'post-check=0, pre-check=0, max-age=0',
                'Pragma'  => 'no-cache',
                'Expires' => 'Thu, 01 Jan 1970 00:00:00 GMT'
            );

            $self->finish;
        }
    );
}
1;

FX::AnyEvent.pm

※モジュール名がアレなんですが・・・

package FX::AnyEvent;

use strict;
use warnings;
use utf8;
use Encode qw(decode);
use AnyEvent::HTTP qw(http_get);

our $VERSION = '0.0.1';

my $currency_split_item_regex = qr{_円_|_ドル_|_スイスフラン_};
my $last_data                 = "";

sub new {
    my ( $class, %args ) = @_;
    my $on_error    = delete $args{on_error}    || sub { die @_ };
    my $on_eof      = delete $args{on_eof}      || sub { die @_ };
    my $on_currency = delete $args{on_currency} || sub { warn $_[0] };

    my $self ||= bless {
        url => delete $args{url}
          || 'http://GAITAME_TRADER_RATE_FETCH_URL',
        encode_type => delete $args{encode_type}
          || "utf8",
        content        => [],
        currency_types => &types,
        %args,

    }, $class;

    my $sender       = \&http_get;
    my @initial_args = ( $self->{url} );

    my $interval_add = 5;
    Scalar::Util::weaken( $self->{connection_guard} );
    $self->{connection_guard} ||=

      AnyEvent->timer(
        interval => $interval_add,
        cb       => sub {

            $sender->(
                @initial_args,

                sub {
                    my ( $data, $handle ) = @_;
                    if ($data) {
                        $self->{content} = [
                            split /$currency_split_item_regex/,

                            Encode::decode( 'shift_jis', $data )
                        ];
                        $self->_currency_parser;
                        my $currencies = $self->get_currencies(
                            qw(usd/jpy eur/usd eur/jpy gbp/usd));
                          if ( $data ne $last_data )
                        {
                            $on_currency->($currencies);
                            $last_data = $data;
                            if ( $interval_add != 5 ) {
                                $self->{connection_guard}->set( 0, 5 );
                                $interval_add = 5;
                            }
                        }
                        else {
                            $interval_add += 5;
                            $self->{connection_guard}->set( 0, $interval_add );
                        }
                    }
                }
            );
        }
      );

    $self;
}

sub _currency_parser {
    my $self = shift;

    for my $currency ( @{ $self->{content} } ) {

        my $currency_items = [ split '_', $currency ];

        for my $type_key ( keys %{ $self->{currency_types} } ) {
            next
              if $self->{currency_types}->{$type_key}->{name} ne
                  $currency_items->[0];
            $self->{currency_types}->{$type_key}->{ask} = $currency_items->[1];
            $self->{currency_types}->{$type_key}->{bid} = $currency_items->[2];
            $self->{currency_types}->{$type_key}->{min} = $currency_items->[4];
            $self->{currency_types}->{$type_key}->{max} = $currency_items->[5];

            #  last;
        }
    }
}

sub get_currencies {
    my ( $self, @args ) = @_;

    if ( $args[1] ) {    # args more than 1
        return [ map { $self->{currency_types}->{ uc $_ } } @args ];
    }
    elsif ( $args[0] ) {    # args 1 time;
        my $currency_item = $self->{currency_types}->{ uc $args[0] };
        defined $currency_item ? $currency_item : undef;

    }
    elsif ( !$args[0] ) {    # no args return all currencies
        return map { $self->{currency_types}->{$_} }
          keys %{ $self->{currency_types} };
    }
}

sub types {
    +{
        'USD/JPY' => {
            name => '米ドル円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 1,

        },
        'EUR/JPY' => {
            name => 'ユーロ円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 2,

        },
        'EUR/USD' => {
            name => 'ユーロドル',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 3,

        },
        'AUD/JPY' => {
            name => '豪ドル円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 4,

        },
        'GBP/JPY' => {
            name => 'ポンド円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 5,

        },
        'NZ/JPY' => {
            name => 'NZドル円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 6,

        },
        'CAD/JPY' => {
            name => 'カナダドル円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 7,

        },
        'CHF/JPY' => {
            name => 'スイスフラン円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 8,

        },
        'GBP/USD' => {
            name => 'ポンド米ドル',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 9,

        },
        'USD/CHF' => {
            name => 'ドルスイスフラン',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 10,

        },
        'HK/JPY' => {
            name => '香港ドル円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 11,

        },
        'ZAR/JPY' => {
            name => '南アフリカランド円',
            ask  => '',
            bid  => '',
            max  => '',
            min  => '',
            pri  => 12,

        },
    };
}

1;    # Magic true value required at end of module
__END__
created:

Back to top