App-Chart

 view release on metacpan or  search on metacpan

lib/App/Chart/Download.pm  view on Meta::CPAN

  foreach my $dividend (@{$h->{'dividends'}}) {
    my $latest_dividend_sth = $dbh->prepare_cached
      ('UPDATE latest SET dividend=? WHERE symbol=? AND last_date=?');
    my $symbol = $dividend->{'symbol'};
    if ($latest_dividend_sth->execute ($dividend->{'amount'},
                                       $symbol,
                                       $dividend->{'ex_date'})) {
      $latest_changed{$symbol} = 1;
    }
  }

  App::Chart::chart_dirbroadcast()->send ('latest-changed', \%latest_changed);

  require App::Chart::Annotation;
  foreach my $symbol (keys %latest_changed) {
    App::Chart::Annotation::Alert::update_alert ($symbol);
  }
}


#------------------------------------------------------------------------------

sub write_latest_group {
  my ($h) = @_;
  ### write_latest_group(): $h

  crunch_h ($h);
  ### crunched: $h

  my $fetch_timestamp = timestamp_now();
  my $prefer_decimals = $h->{'prefer_decimals'};
  my $source = $h->{'source'}
    or croak 'missing "source" for latest records';
  my %latest;

  my $dbh = App::Chart::DBI->instance;
  App::Chart::Database::call_with_transaction
      ($dbh, sub {

         my $sth = $dbh->prepare_cached
           ('INSERT OR REPLACE INTO latest
            (symbol, name, month, exchange, currency,
             quote_date, quote_time, bid, offer,
             last_date, last_time, open, high, low, last, change, volume,
             note, error, dividend, copyright, source,
             fetch_timestamp, url, etag, last_modified)
            VALUES (?,?,?,?,?, ?,?,?,?, ?,?,?,?,?,?,?,?, ?,?,?,?,?, ?,?,?,?)');

         my $resp = $h->{'resp'};
         my $etag = (defined $resp ? scalar $resp->header('ETag') : undef);
         my $last_modified = (defined $resp ? $resp->last_modified : undef);

         foreach my $data (@{$h->{'data'}}) {
           my $symbol = $data->{'symbol'};
           my $this_date = $data->{'date'};
           if ($latest{$symbol}) {
             my $got_date = $latest{$symbol}->{'date'};
             if (! defined $got_date || ! defined $this_date) {
               carp "write_latest_group: $source: two records for '$symbol', but no 'date' field";
               if (DEBUG || 1) {
                 require Data::Dumper;
                 print Data::Dumper->Dump([$data,$latest{$symbol}],
                                          ['data','latest-so-far']);
               }
               next;
             }
             if ($got_date ge $this_date) { next; }
           }
           $latest{$symbol} = $data;
         }

         my $error = $h->{'error'};
         if (! defined $error && defined $resp && ! $resp->is_success) {
           $error = $resp->status_line;
         }

         foreach my $data (values %latest) {
           my $symbol = $data->{'symbol'};

           my $bid     = $data->{'bid'};
           my $offer   = $data->{'offer'};

           # disallow 0 for prices
           if ($bid    && $bid   == 0)   { $bid   = undef; }
           if ($offer  && $offer == 0)   { $offer = undef; }

           my $quote_date = crunch_date ($data->{'quote_date'});
           my $quote_time = crunch_time ($data->{'quote_time'});
           if ($quote_time && ! $quote_date) {
             croak "quote_time without quote_date for $symbol";
           }
           # default quote date/time to now
           if (($bid || $offer) && ! $quote_date) {
             my $symbol_timezone = App::Chart::TZ->for_symbol ($symbol);
             ($quote_date, $quote_time)
               = $symbol_timezone->iso_date_time
                 (time() - 60 * ($data->{'quote_delay_minutes'} || 0));
           }

           my $last_date = crunch_date ($data->{'last_date'} || $data->{'date'});
           my $last_time = crunch_time ($data->{'last_time'});

           my $open    = $data->{'open'};
           my $high    = $data->{'high'};
           my $low     = $data->{'low'};
           my $last    = $data->{'last'} || $data->{'close'};
           my $change  = $data->{'change'};
           my $prev    = crunch_price ($data->{'prev'}, $prefer_decimals);
           my $volume  = $data->{'volume'};

           if (! defined $last) {
             # if there's no last price then try to use the prev
             $open = $high = $low = undef;
             $last = $prev;
             $prev = undef;
             $change = undef;
             $last_date = undef;
             $last_time = undef;

           } elsif (! defined $change) {
             # if no change given then try to calculate it from last and prev

lib/App/Chart/Download.pm  view on Meta::CPAN

  return $ret;
}

sub tdate_today_after {
  my ($after_hour, $after_minute, $timezone) = @_;

  { local $Tie::TZ::TZ = $timezone->tz;
    my ($year,$month,$day, $hour,$min,$sec, $doy,$dow,$dst) =
      Date::Calc::System_Clock();

    my $tdate = App::Chart::ymd_to_tdate_floor ($year, $month, $day);
    if ($dow <= 5  # is a weekday
        && (App::Chart::hms_to_seconds ($hour, $min, 0)
            < App::Chart::hms_to_seconds ($after_hour, $after_minute, 0))) {
      $tdate--;
    }
    return $tdate;
  }
}


#-----------------------------------------------------------------------------
# selecting among possibly overlapping files

# $files is an arrayref containing hash records with keys
#
#     lo_tdate,hi_tdate   inclusive coverage of the record
#     lo_year,hi_year     alterative form for date range
#     cost                size of the file in bytes
#
sub choose_files {
  my ($files, $lo_tdate, $hi_tdate) = @_;
  if ($lo_tdate > $hi_tdate) { return []; }

  if (DEBUG) { print "choose_files $lo_tdate to $hi_tdate\n"; }

  foreach my $f (@$files) {
    if (! defined $f->{'lo_tdate'}) {
      if (my $m = $f->{'month_iso'}) {
        $f->{'lo_tdate'} = App::Chart::Download::iso_to_tdate_ceil ($m);
      } elsif ($f->{'lo_year'}) {
        $f->{'lo_tdate'}
          = App::Chart::ymd_to_tdate_ceil ($f->{'lo_year'}, 1, 1);
      } else {
        croak 'choose_files: missing lo date';
      }
    }

    if (! defined $f->{'hi_tdate'}) {
      if (my $m = $f->{'month_iso'}) {
        $f->{'hi_tdate'}
          = tdate_end_of_month (App::Chart::Download::iso_to_tdate_ceil ($m));
      } elsif ($f->{'hi_year'}) {
        $f->{'hi_tdate'}
          = App::Chart::ymd_to_tdate_floor($f->{'hi_year'}, 12, 31);
      } else {
        croak 'choose_files: missing hi date';
      }
    }
  }
  if (DEBUG >= 2) { require Data::Dumper;
                    print Data::Dumper::Dumper($files); }

  # restrict wanted range to what's available
  my $lo_available = min (map {$_->{'lo_tdate'}} @$files);
  my $hi_available = max (map {$_->{'hi_tdate'}} @$files);
  $lo_tdate = max ($lo_tdate, $lo_available);
  $hi_tdate = min ($hi_tdate, $hi_available);
  if (DEBUG) { print "  available $lo_available to $hi_available\n";
               print "  restricted range $lo_tdate to $hi_tdate\n"; }
  if ($lo_tdate > $hi_tdate) { return []; }

  # ignore file elements not covering any of the desired range
  $files = [ grep {App::Chart::overlap_inclusive_p ($lo_tdate, $hi_tdate,
                                                    $_->{'lo_tdate'},
                                                    $_->{'hi_tdate'})}
             @$files ];

  # Algorithm::ChooseSubsets would be another way to iterate, or
  # Math::Subset::List to get all combinations
  my $best_cost;
  my $best_files;
  foreach my $this_files (all_combinations ($files)) {
    if (! cover_p ($this_files, $lo_tdate, $hi_tdate)) { next; }
    my $cost = List::Util::sum (map {$_->{'cost'}||0} @$this_files);
    $cost += $App::Chart::option{'http_get_cost'} * scalar(@$this_files);
    if (! defined $best_cost || $cost < $best_cost) {
      $best_cost = $cost;
      $best_files = $this_files;
    }
  }
  return $best_files;
}

# return true if the set of file records in arrayref $files covers all of
# $lo_tdate through $hi_tdate inclusive
#
sub cover_p {
  my ($files, $lo_tdate, $hi_tdate) = @_;
  require Set::IntSpan::Fast;
  my $set = Set::IntSpan::Fast->new;
  foreach my $f (@$files) {
    $set->add_range ($f->{'lo_tdate'}, $f->{'hi_tdate'});
  }
  $set->contains_all_range ($lo_tdate, $hi_tdate);
}

# return a list which is all the combinations of elements of @$aref
# for example $aref == [ 10, 20 ] would return ([], [10], [20], [10,20])
# there's 2**N combinations for aref length N
#
sub all_combinations {
  my ($aref) = @_;
  my @ret = ([]);
  foreach my $i (0 .. $#$aref) {
    push @ret, map {[ @$_, $aref->[$i] ]} @ret;
  }
  return @ret;
}

# return the last tdate in the month containing the given $tdate
sub tdate_end_of_month {



( run in 0.750 second using v1.01-cache-2.11-cpan-39bf76dae61 )