Group
Extension

Finance-Quote/lib/Finance/Quote/MorningstarCH.pm

#!/usr/bin/perl -w
# vi: set ts=4 sw=4 noai ic showmode showmatch: 

#  MorningstarCH.pm
#
#  Obtains quotes for CH Unit Trusts from http://morningstar.ch/ - please
#  refer to the end of this file for further information.
#
#  author: Manuel Friedli (manuel@fritteli.ch)
#
#  version: 0.2 Updated version - 30 July 2025
#
#  This file is heavily based on MorningstarUK.pm by Martin Sadler
#  (martinsadler@users.sourceforge.net), version 0.1, 01 April 2013
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
#    02110-1301, USA
#


package Finance::Quote::MorningstarCH;

use strict;
use warnings;

# URLs
use vars qw($VERSION $MSTARCH_NEXT_URL $MSTARCH_LOOK_UP $MSTARCH_MAIN_URL);

use constant DEBUG => $ENV{DEBUG};
use if DEBUG, 'Smart::Comments';

use LWP::Simple;
use LWP::UserAgent;
use HTTP::Request::Common;
use HTTP::Cookies;
use JSON qw(decode_json);
use Text::Template;

our $VERSION = '1.67_01'; # TRIAL VERSION

$MSTARCH_MAIN_URL   =   "https://www.morningstar.ch";
$MSTARCH_LOOK_UP    =   "https://www.morningstar.ch/ch/funds/SecuritySearchResults.aspx?search=";
$MSTARCH_NEXT_URL = Text::Template->new( TYPE => 'STRING', SOURCE => 'https://api-global.morningstar.com/sal-service/v1/fund/quote/v7/{$secid}/data?fundServCode=&showAnalystRatingChinaFund=false&showAnalystRating=false&hideesg=false&region=EEA&languageId=en-eu&locale=en-eu&clientId=MDC&benchmarkId=mstarorcat&component=sal-mip-investment-overview&version=4.65.0' );

# FIXME - Needs cleanup

our $DISPLAY    = 'Morningstar CH';
our @LABELS = qw/name currency last date price nav source iso_date method success errormsg/;
our $METHODHASH = {subroutine => \&mstarch_fund,
                   display => $DISPLAY,
                   labels => \@LABELS};

sub methodinfo {
    return (
        morningstarch => $METHODHASH,
        mstarch       => $METHODHASH,
    );
}

sub methods {
	my %m = methodinfo();
	return map {$_ => $m{$_}{subroutine} } keys %m;
}

sub labels {
	my %m = methodinfo();
	return map {$_ => [@{$m{$_}{labels}}] } keys %m;
}

#
# =======================================================================

sub mstarch_fund  {
    my $quoter = shift;
    my @symbols = @_;

    return unless @symbols;

    my %fundquote;

    my $ua = $quoter->user_agent;
    my $cj = HTTP::Cookies->new();
    $ua->cookie_jar( $cj );

    foreach (@symbols)
    {
	    my $code = $_;

	    my $code_type = "** Invalid **";
	    if ($code =~ m/^[a-zA-Z]{2}[a-zA-Z0-9]{9}\d(.*)/ && !$1) { $code_type = "ISIN";  }
	    elsif ($code =~ m/^[a-zA-Z0-9]{6}\d(.*)/ && !$1 )        { $code_type = "SEDOL"; }
	    elsif ($code =~ m/^[a-zA-Z]{4,6}(.*)/ && !$1)            { $code_type = "MEXID"; }

# current version can only use ISIN - report an error and exit if any other type

        if ($code_type ne "ISIN")
        {
		    $fundquote {$code,"success"} = 0;
		    $fundquote {$code,"errormsg"} = "Error - invalid symbol";
		    next;
        }

	    $fundquote {$code,"success"} = 1; # ever the optimist....
	    $fundquote {$code,"errormsg"} = "Success";

# perform the look-up - if not found, return with error

        my $webdoc  = get($MSTARCH_LOOK_UP.$code);
        if (!$webdoc)
        {
	        # serious error, report it and give up
		    $fundquote {$code,"success"} = 0;
		    $fundquote {$code,"errormsg"} =
		        "Error - failed to retrieve fund data";
		    next;
	    }

		### [<now>] webdoc: $webdoc

	    $fundquote {$code, "symbol"} = $code;
	    $fundquote {$code, "source"} = $MSTARCH_MAIN_URL;

# Find name by regexp

        my ($name, $nexturl, $secid);
 		if ($webdoc =~
        m[<td class="msDataText searchLink"><a href="(.*?id=([A-Z0-9]+))">(.*?)</a></td><td class="msDataText searchIsin"><span>[a-zA-Z]{2}[a-zA-Z0-9]{9}\d(.*)</span></td>] )
        {
            $nexturl = $1;
            $secid = $2;
			### [<now>] secID: $secid
            $name = $3;
        }

		if (!defined($name)) {
			# not a serious error - don't report it ....
#			$fundquote {$code,"success"} = 0;
			# ... but set a useful message ....
			$fundquote {$code,"errormsg"} = "Warning - failed to find fund name";
			$name = "*** UNKNOWN ***";
			# ... and continue
		}
		$fundquote {$code, "name"} = $name;	# set name

		if (!defined($nexturl)) {
	        # serious error, report it and give up
		    $fundquote {$code,"success"} = 0;
		    $fundquote {$code,"errormsg"} =
		        "Error - failed to retrieve fund data";
		    next;
		}

		$nexturl = $MSTARCH_NEXT_URL->fill_in(HASH => {secid => $secid});
		$ua->default_header('Apikey' => 'lstzFDEOhfFNMLikKa0am9mgEKLBl49T');

		### [<now>] NextURL: $nexturl

		my $response = $ua->request( GET $nexturl );

		if ($response->code != 200 ) {
			$fundquote {$code,"success"} = 0;
			$fundquote {$code,"errormsg"} = "Error - $code not found";
			next;
		}

		$webdoc = $response->content;
		### [<now>] 2nd Webdoc: $webdoc

		my $json;
		eval {$json = decode_json $webdoc};
        if ($@) {
	        # serious error, report it and give up
		    $fundquote {$code,"success"} = 0;
		    $fundquote {$code,"errormsg"} =
		        "Error - failed to retrieve fund data - could not decode JSON";
		    next;
	    }
		### [<now>] JSON: $json

# Find date, currency and price all in one table row

		my ($date, $time);
		my $currency = $json->{'currency'};
		my $price    = $json->{'nav'};
		if ( $json->{'latestPriceDate'} =~ m|(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}):| ) {
			$date = $1;
			$time = $2;
		}

		if (!defined($date)) {
			# not a serious error - don't report it ....
#			$fundquote {$code,"success"} = 0;
			# ... but set a useful message ....
			$fundquote {$code,"errormsg"} = "Warning - failed to find a date";
			# use today's date
            $quoter->store_date(\%fundquote, $code, {today => 1});
			# ... and continue
		}
		else
		{
		    $quoter->store_date(\%fundquote, $code, {isodate => $date});
		}

		if (!defined($price)) {
	    	# serious error, report it and give up
			$fundquote {$code,"success"} = 0;
			$fundquote {$code,"errormsg"} = "Error - failed to find a price";
			next;
		}

		if (!defined($currency)) {
	    	# serious error, report it and give up
			$fundquote {$code,"success"} = 0;
			$fundquote {$code,"errormsg"} = "Error - failed to find a currency";
			next;
		}

		# defer setting currency and price until we've dealt with possible GBX currency...

# deal with GBX pricing of UK unit trusts

		if ($currency eq "GBX")
		{
			$currency = "GBP" ;
			$price = $price / 100 ;
		}

		# now set prices and currency

		$fundquote {$code, "price"} = $price;
		$fundquote {$code, "last"} = $price;
		$fundquote {$code, "nav"} = $price;
		$fundquote {$code, "currency"} = $currency;

		$fundquote {$code, "method"} = "morningstaruk";   # set method

	}

	return wantarray ? %fundquote : \%fundquote;
}

1;

=head1 NAME

Finance::Quote::MorningstarCH - Obtain CH Unit Trust quotes from morningstar.ch.

=head1 SYNOPSIS

    $q = Finance::Quote->new;

    %info = $q->fetch("morningstarch","<isin> ...");  # Only query morningstar.ch using ISINs

=head1 DESCRIPTION

This module fetches information from the MorningStar Funds service,
https://morningstar.com/ch/.

Funds are identified by their ISIN code.

This module is loaded by default on a Finance::Quote object. It's
also possible to load it explicitly by placing "morningstarch" in the argument
list to Finance::Quote->new().

Information obtained by this module may be covered by Morningstar
terms and conditions See https://morningstar.ch/ for details.

=head2 Stocks And Indices

This module provides the "morningstarch" fetch method for fetching CH Unit
Trusts and OEICs prices and other information from morningstar.ch.

=head1 LABELS RETURNED

The following labels may be returned by Finance::Quote::MorningstarCH :

    name, currency, last, date, time, price, nav, source, method,
    iso_date, net, p_change, success, errormsg.


=head1 SEE ALSO

Morning Star websites, https://morningstar.ch


=head1 AUTHOR

Manuel Friedli, E<lt>manuel@fritteli.chE<gt>
Based heavily on the work of Martin Sadler, E<lt>martinsadler@users.sourceforge.netE<gt>, many thanks!

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2019, 2025 by Manuel Friedli

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.1 or,
at your option, any later version of Perl 5 you may have available.


=cut

__END__


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.