本帖最后由 523066680 于 2024-1-6 20:48 编辑
基本的“数据要素”已经准备差不多,该画画了,请出的第一个接口是 Image::Magick ,
数据文件清单- index.json - 股票名称 代码映射表
- baseInfo_all.json - 基本信息表
- concept.json - 概念映射表
- stock_data.sqlite - 日K、均线历史数据
复制代码 以下是Perl模块:DrawKlineMA.pm 代码- package DrawKlineMA;
-
- use utf8;
- use Encode;
- use Modern::Perl;
- use File::Slurp;
- use List::Util qw/sum max min/;
- use List::MoreUtils qw/zip/;
- use Date::Format; # time2str
- use Date::Parse; # str2time
- use JSON qw/from_json to_json/;
- use Image::Magick;
-
- sub Draw
- {
- my ( $code, $name, $data, $concept, $export ) = @_;
-
- if ( -f gbk($export) )
- {
- # printf "png file already exists\n";
- # return;
- }
-
- if ( $#$data < 80 )
- {
- printf "the data quantity less than 80\n";
- }
- else
- {
- @$data = @{$data}[-80 .. -1];
- }
-
- # 创建一个新的图片对象
- my $image = Image::Magick->new(size => '1200x500');
- $image->Read('xc:white');
-
- my $layer1 = Image::Magick->new(size => '1200x500');
- $layer1->Read('xc:none');
-
- my ( $W, $H ) = ( $image->Get("width"), $image->Get("height") );
-
- # 设置绘图参数
- my $bar_width = 10; # K线的宽度
- my $padding = 3; # K线之间的间距
- my $max_value = max( map { $_->{'high'} } @$data );
- my $min_value = min( map { $_->{'low'} } @$data );
- my $bar_delta = $max_value - $min_value;
-
- my $max_volume = max( map { $_->{'volume'} } @$data );
-
- my $VOL_BASE = $H * 0.2;
- my $VOL_MAX_H = $H * 0.1;
-
- my $BAR_BASE = $VOL_BASE + $VOL_MAX_H + 10;
- my $BAR_MAX_H = $H * 0.5;
-
- # 两融数据 - 考虑数据中有NULL的情况
- my @margin_data = grep { defined $_ } map { $_->{utf8('融券余额')} } @$data;
- my $margin_max = max( @margin_data );
- my $margin_min = min( @margin_data );
- my $margin_delta = scalar(@margin_data) > 0 ? $margin_max - $margin_min : undef;
- my $margin_sum = sum( @margin_data );
-
- # 绘制外框
- # draw_rect_range( $image, 1, scalar(@$data)*($bar_width+$padding), $VOL_BASE, $VOL_BASE+$VOL_MAX_H, "none", "gray" );
-
- # 绘制外框
- # draw_rect_range( $image, 1, scalar(@$data)*($bar_width+$padding), $BAR_BASE, $BAR_BASE+$BAR_MAX_H, "none", "gray" );
-
- my @words = split /,/, $concept;
- my $buff = "";
- while ( @words )
- {
- $buff .= join(", ", splice(@words, 0, 10)) ."\n";
- }
-
- # 板块信息
- $image->Annotate(
- text => $buff,
- x => int($W*1/4),
- y => 20,
- fill => 'black',
- font => "Simhei",
- pointsize => 16,
- align => 'left',
- gravity => "SouthWest",
- 'word-break' => 'break-word',
- );
-
- # 股票名称
- $image->Annotate(
- text => sprintf("%s(%s)", $name, $code),
- x => 10,
- y => 28,
- fill => 'black',
- font => "Simhei",
- pointsize => 28,
- align => 'left',
- gravity => "SouthWest",
- );
-
- # 绘制K线图
- my $x = $padding;
-
- # 均线起点值
- my $prev = {};
- my @ma_list = qw/ma5 ma10 ma20 ma30 ma60 ma120 ma250/;
- my @colors = qw/black orange pink green blue purple brown cyan/;
- my $mcolor = {};
- my $cid = 0;
-
- # 初始化均线起点值,但也要考虑某些标的,长周期分均线一开始并未出现的情况
- for my $ma ( @ma_list )
- {
- $mcolor->{$ma} = $colors[$cid++];
- $prev->{$ma} = $data->[0]->{$ma};
- }
-
- $prev->{'margin'} = $data->[0]->{'融券余额'};
-
- my $prev_close = 0.0;
- for my $kline ( @$data )
- {
- my $date = $kline->{date};
- my $open = $kline->{open};
- my $high = $kline->{high};
- my $low = $kline->{low};
- my $close = $kline->{close};
- my $volume = $kline->{volume};
-
- # 计算K线的高度
- my $delta = abs($open - $close);
- my $bar_open = ($open - $min_value )/ $bar_delta * $BAR_MAX_H;
- my $bar_close = ($close - $min_value )/ $bar_delta * $BAR_MAX_H;
-
- # 上下影线位置
- my $bar_high = ($high - $min_value )/ $bar_delta * $BAR_MAX_H;
- my $bar_low = ($low - $min_value )/ $bar_delta * $BAR_MAX_H;
-
- # 颜色 - 下跌时为绿色实心,上涨或者不涨为白色实心、红色边界
- my $fill = $close > $open ? "white" : "green";
- my $stroke = $close > $open ? "red" : "green";
-
- # 如果是一字上涨
- if ( $close == $open and $close > $prev_close )
- { $fill = "white"; $stroke = "red"; }
-
- # 绘制上下影线
- draw_line_range( $image, $x+$bar_width/2, $BAR_BASE+$bar_high, $BAR_BASE+$bar_low, $stroke );
-
- # printf "%.2f %.2f %d %d\n", $high, $low, $bar_high, $bar_low;
- # 绘制K柱
- draw_rect_range( $image, $x, $x+$bar_width, $BAR_BASE+$bar_open, $BAR_BASE+$bar_close, $fill, $stroke );
-
- # 绘制量能条
- my $volume_height = $volume / $max_volume * $VOL_MAX_H;
- draw_rect_range( $image, $x, $x+$bar_width, $VOL_BASE, $VOL_BASE+$volume_height, $fill, $stroke );
-
- # 绘制融券数据
- if ( defined $kline->{utf8('融券余额')} and $margin_sum != 0 )
- {
- my $k = utf8("融券余额");
- if ( not defined $prev->{$k} )
- {
- $prev->{$k} = $kline->{$k};
- }
- else
- {
- # printf "%.2f\n", $kline->{$k};
- # pt1 是上一个点的位置 pt2是当前点的位置
- my $pt1 = { 'x' => $x-$bar_width/2-$padding, 'y' => ($prev->{$k} - $margin_min )/$margin_delta * $BAR_MAX_H };
- my $pt2 = { 'x' => $x+$bar_width/2, 'y' => ($kline->{$k} - $margin_min )/$margin_delta * $BAR_MAX_H };
- draw_line( $layer1, $pt1->{'x'}, $pt1->{'y'}+$BAR_BASE, $pt2->{'x'}, $pt2->{'y'}+$BAR_BASE, "CYAN" );
- $prev->{$k} = $kline->{$k};
- }
- }
-
- # 绘制均线
- for my $ma ( @ma_list )
- {
- # 考虑某些标的,长周期分均线一开始并未出现的情况;先记录数据,留到下一节点绘制
- if ( not defined $prev->{$ma} )
- {
- $prev->{$ma} = $kline->{$ma};
- next;
- }
-
- # pt1 是上一个点的位置 pt2是当前点的位置
- my $pt1 = { 'x' => $x-$bar_width/2-$padding, 'y' => ($prev->{$ma} - $min_value )/$bar_delta * $BAR_MAX_H };
- my $pt2 = { 'x' => $x+$bar_width/2, 'y' => ($kline->{$ma} - $min_value )/$bar_delta * $BAR_MAX_H };
- draw_line( $layer1, $pt1->{'x'}, $pt1->{'y'}+$BAR_BASE, $pt2->{'x'}, $pt2->{'y'}+$BAR_BASE, $mcolor->{$ma} );
-
- $prev->{$ma} = $kline->{$ma};
- }
-
- # 日期字符串长度
- my @mertics = $image->QueryFontMetrics(text => $date, font => 'Arial', pointsize => 12 );
- my $text_width = $mertics[4];
-
- # 绘制日期
- my $text_x = $x;
- my $text_y = $VOL_BASE;
- $image->Annotate(
- text => $date,
- x => $text_x + $padding/2,
- y => $H - $VOL_BASE + $text_width/2 + $padding,
- rotate => 90,
- fill => 'black',
- # stroke => 'black',
- font => 'Arial',
- pointsize => 12,
- align => 'Center',
- gravity => "South",
- );
-
- # 更新X轴位置
- $x += $bar_width + $padding;
- $prev_close = $close;
- }
-
- $layer1->Evaluate( channel => "Alpha", operator => "Multiply", value => 0.6 );
- $image->Composite( image => $layer1 );
-
- $image->Set( "Alpha" => "On");
-
- # 保存图像
- $image->Write( $export );
- }
-
- # 符合直觉的坐标绘制(y在底部)
- sub draw_line
- {
- my ( $cv, $x1, $y1, $x2, $y2, $color, $strokewidth ) = @_;
- my ( $h ) = $cv->Get("Height");
- $cv->Draw(
- primitive => 'line',
- points => sprintf("%d,%d %d,%d", $x1, $h-$y1, $x2, $h-$y2 ),
- stroke => $color,
- strokewidth => 1.0
- );
- }
-
- # 符合直觉的坐标绘制(y在底部)
- sub draw_line_range
- {
- my ( $cv, $x, $y1, $y2, $color ) = @_;
- my ( $h ) = $cv->Get("Height");
- $cv->Draw(
- primitive => 'line',
- points => sprintf("%d,%d %d,%d", $x, $h-$y1, $x, $h-$y2 ),
- stroke => $color
- );
- }
-
- # 符合直觉的坐标绘制(y在底部)
- sub draw_rect_range
- {
- my ( $cv, $x1, $x2, $y1, $y2, $fill, $stroke ) = @_;
- my ( $h ) = $cv->Get("Height");
- $cv->Draw(
- primitive => 'rectangle',
- points => sprintf("%d,%d %d,%d", $x1, $h-$y1, $x2, $h-$y2 ),
- fill => $fill,
- stroke => $stroke,
- );
- }
-
- sub dump_json
- {
- my ($data) = @_;
- return to_json( $data, { allow_blessed => 1, allow_tags => 1, pretty => 1, canonical => 1 });
- }
-
- sub gbk { encode('gbk', $_[0]) }
- sub utf8 { encode('utf8', $_[0]) }
- sub u2gbk { encode('gbk', decode('utf8', $_[0])) }
- sub uni { decode('utf8', $_[0]) }
-
- 1;
复制代码 现在,可以做一些特征筛选、板块组合筛选,并且批量生成走势图的操作。
按板块筛选并且批量绘图的脚本:- use utf8;
- use Encode;
- use Modern::Perl;
- use DBI;
- use File::Slurp;
- use File::Path qw/make_path/;
- use File::Basename;
- use Mojo::UserAgent;
- use Date::Format; # time2str
- use Date::Parse; # str2time
- use List::Util qw/max min sum/;
- use JSON qw/from_json to_json/;
- STDOUT->autoflush(1);
-
- use FindBin;
- use lib $FindBin::Bin;
- use DrawKlineMA;
-
- # 数据库路径不需要转换为GBK
- my $db = "stock_data.sqlite";
- my $dbh = DBI->connect("dbi:SQLite:dbname=$db") or die "can not connect DB: $DBI::errstr";
-
- my $codes_in_db = $dbh->selectcol_arrayref( "SELECT DISTINCT symbol FROM 日K" );
- my $total_in_db = scalar @$codes_in_db;
-
- # 股票代号 - 名称 对照表
- my $index = from_json( uni(scalar(read_file( "index.json" ))) );
- my $baseinfo_all = from_json( uni(scalar(read_file( "baseInfo_all.json" ))) );
- my $concept;
- for my $e ( @{$baseinfo_all->{'data'}} )
- {
- $concept->{$e->{code}} = $e->{'concept'} .",". $e->{'z52'};
- }
-
- my $output_dir = "./先进封装";
- mkdir gbk($output_dir) unless -d gbk($output_dir);
-
- for my $code ( @$codes_in_db )
- {
- next if $code =~ /TEST/i;
- # next unless $code eq "001268";
- # printf "current: %s\n", $code;
-
- my $name = exists $index->{'index_by_code'}{$code} ? $index->{'index_by_code'}{$code} : "unknow";
- my $data = load_kline_data( $dbh, $code, 90 );
-
- # 如果少于90天,PASS
- next if scalar( @$data ) < 90;
- # 工业母机 工业4.0
- # 一带一路 and 新疆
- if ( not exists $concept->{ $code } )
- {
- printf "${code}: concept not found\n";
- next;
- }
-
- next unless $concept->{ $code } =~ /钙钛/;
- next unless $concept->{ $code } =~ /半导体/;
-
- # next unless $concept->{ $code } =~ /华为/;
- # next unless $concept->{ $code } =~ /ChatGPT/;
-
- printf "%s %s\n", $code, gbk($name);
-
- # next;
-
- my $export = "${output_dir}/${code}-${name}.png";
- DrawKlineMA::Draw( $code, $name, $data, $concept->{$code}, $export );
- }
-
- sub load_kline_data
- {
- my ( $dbh, $code, $n ) = @_;
-
- # 查询数据
- my $query = "SELECT * FROM 日K WHERE symbol = ? ORDER BY date DESC LIMIT ?";
-
- # selectall_arrayref 函数可以返回带列标名称的哈希数据
- # $n 表示要获取的行数
- my $result = $dbh->selectall_arrayref($query, { Slice => {} }, $code, $n);
- # print dump_json( $result );
-
- @$result = reverse @$result;
- return $result;
- }
-
- sub dump_json
- {
- my ($data) = @_;
- return to_json( $data, { allow_blessed => 1, allow_tags => 1, pretty => 1, canonical => 1 });
- }
-
- sub gbk { encode('gbk', $_[0]) }
- sub utf8 { encode('utf8', $_[0]) }
- sub u2gbk { encode('gbk', decode('utf8', $_[0])) }
- sub uni { decode('utf8', $_[0]) }
复制代码 生成的图片样张,其中青蓝色的是融券数据(最近懒得更新了所以止步12月)
-
这样一张图竟然需要3~5秒,无法忍受,于是就重新勾起了远古的回忆 —— 为什么不用OpenGL渲染?下回再说。 |