This commit is contained in:
Dale Mellor 2020-03-23 11:52:18 +00:00
commit 365ad5a560
95 changed files with 13240 additions and 0 deletions

25
.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
*~
*.o
*.lo
*.la
.deps
.libs
ABOUT-NLS
aclocal.m4
auto-config.h
auto-config.h.in
autom4te.cache
config.log
config.status
configure
curlpp/
libtool
m4
makefile
makefile.in
quote/
stamp-h1
TAGS
test-driver
trader-desk.pc
trader-desk/trader-desk

11
AUTHORS Normal file
View file

@ -0,0 +1,11 @@
Dale Mellor
______________________________________________________________________
Copyright (c) 2017 Dale Mellor
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. This file is offered as-is,
without any warranty.

0
COPYING Normal file
View file

26
ChangeLog Normal file
View file

@ -0,0 +1,26 @@
2017-02-10 gettextize <bug-gnu-gettext@gnu.org>
* configure.ac (AC_CONFIG_FILES): Add po/Makefile.in.
2017-02-10 gettextize <bug-gnu-gettext@gnu.org>
* configure.ac (AC_CONFIG_FILES): Add po/Makefile.in.
2017-02-09 gettextize <bug-gnu-gettext@gnu.org>
* configure.ac (AM_GNU_GETTEXT_VERSION): Bump to 0.19.4.
2017-02-09 gettextize <bug-gnu-gettext@gnu.org>
* configure.ac (AM_GNU_GETTEXT_VERSION): Bump to 0.19.3.
______________________________________________________________________
Copyright (c) 2017 Dale Mellor
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. This file is offered as-is,
without any warranty.

25
INSTALL Normal file
View file

@ -0,0 +1,25 @@
* Build instructions
*** From a GIT checkout
First, cd into the top-level source directory, then type
autoreconf --install && ./configure --prefix=$PWD/install && \
make install
(you can of course run each piece separately), after which the program will
be installed at install/bin/cosmosis.
If you make subsequent changes in the code base, you only need to run
make && make install
to have your changes implemented in the installation.
*** From a tarball
Unpack the tar file and cd into the top directory, then type
./configure --prefix=$PWD/install && make install

13
NEWS Normal file
View file

@ -0,0 +1,13 @@
2017-11-03 Planned first release to public.
2007-11-03 Development begins in earnest.
______________________________________________________________________
Copyright (c) 2017 Dale Mellor
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. This file is offered as-is,
without any warranty.

1
README Symbolic link
View file

@ -0,0 +1 @@
README.md

117
README.md Normal file
View file

@ -0,0 +1,117 @@
> Github is a secondary distribution point for this project; please see
> https://rdmp.org/dmbcs/trader-desk to be sure to see the most up to date
> version and complete details.
# DMBCS Trader-Desk Application
> This is a beta release of C++ code designed to be built and run on a
> Gnu/Linux-based system.
While stock charting applications are ten a penny, it seems like every
time you look at a different stock chart you see a completely different
picture of the situation. Especially when it is your broker: things
always look great until the time you buy in and then you see things are
all really downhill.
So we want a desk which shows us data we can believe, and allows us to
very quickly view and analyze the data in lots of ways, at lots of time
scales; we want to be live and interactive with our data.
Specifically, our requirements are
* Show full, at-a-glance, view of an entire market
* Instantly bring up all the details of any one company
* Have all data (ten years, all markets) on local machine, in a
proper database, for speed
* Application must be lightning fast, we want to pan and
zoom around the data to our hearts content
* Ultimately we want trading robots which can react to
events super-fast
* Provide plug-in infrastructure for analysis modules, and
ultimately for robot traders
## The Project
We are a team of professional C++ Linux programmers and this has been a
personal side project, and thus isnt really designed for portability or
ease of installation. But we have made an effort, and the application
does include a wizard which will go some way towards getting your database
set up correctly.
### Prerequisites
The requirements are specific and quite hard:
* `gcc 9.3`
* Yep, this is written to C++20 standards with **concepts** and the
**fmt** library in use.
* `fmt`
* `mariadb`
* `dmbcs-market-data-api`
* An account at *AlphaVantage* (free); you will need to acquire an *API
key* from that site and enter it at the bottom of the preferences dialog
box in the `trader-desk` application
It has only been built on a Debian 10 (current stable) system. The file
`setup-hint.sh` at the root of the distribution is a pseudo-script for
configuring and installing into a clean-built Debian 10 machine. It is
not expected that you would just run this blindly, but use it as a guide
for manually setting your own system up at the command line; in particular
it will inform you how to get the dependencies listed above.
## Download
The `dmbcs-trader-desk` source code is managed with *GIT* (configured with
*autotools*, built with *make* and a good C++20 compiler). Type
> `git clone http://rdmp.org/dmbcs/trader-desk.git dmbcs-trader-desk`
at the command line to obtain a copy.
This repository also comes with some database pre-population data, so that
you will have something interesting to look at as soon as you start the
application running!
## Documentation
As per above, build and installation instructions take the form of the
`setup-hint.sh` pseudo-script included with the sources. The
source is about 50% covered by *Doxygen* notes in the header files.
Real end-user documentation is non-existent right now. You should have
had a look at the video above, and then you will be able to follow your
instincts and find your own way around the application.
## Contact
Please click [here](https://rdmp.org/dmbcs/contact) if you wish to send us
a message.
### Mailing list
If you would like to receive e-mail notices of matters arising about this
application, you may request this through the contact form above.
### Contribution to development
We will happily consider contributions to the source code if you provide
the address of a GIT repository we can pull from, or send a pull request
via Github, and will consider all bug reports and feature requests.
## Donations
If you use this application please consider a bitcoin donation if you can.
A small amount informs us that there is interest and that we are providing
a useful service to the community; it will keep us motivated to continue
to make open source software. Donations can be made by bitcoin to the
address `1PWHez4zT2xt6PoyuAwKPJsgRznAKwTtF9`.
______________________________________________________________________
Copyright (c) 2017, 2020 Dale Mellor
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. This file is offered as-is,
without any warranty.

8
build-aux/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
compile
config.guess
config.rpath
config.sub
depcomp
install-sh
ltmain.sh
missing

287
build-aux/gettext.h Normal file
View file

@ -0,0 +1,287 @@
/* Convenience header for conditional use of GNU <libintl.h>.
Copyright (C) 1995-1998, 2000-2002, 2004-2006, 2009-2011 Free Software Foundation, Inc.
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 3 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, see <http://www.gnu.org/licenses/>. */
#ifndef _LIBGETTEXT_H
#define _LIBGETTEXT_H 1
/* NLS can be disabled through the configure --disable-nls option. */
#if ENABLE_NLS
/* Get declarations of GNU message catalog functions. */
# include <libintl.h>
/* You can set the DEFAULT_TEXT_DOMAIN macro to specify the domain used by
the gettext() and ngettext() macros. This is an alternative to calling
textdomain(), and is useful for libraries. */
# ifdef DEFAULT_TEXT_DOMAIN
# undef gettext
# define gettext(Msgid) \
dgettext (DEFAULT_TEXT_DOMAIN, Msgid)
# undef ngettext
# define ngettext(Msgid1, Msgid2, N) \
dngettext (DEFAULT_TEXT_DOMAIN, Msgid1, Msgid2, N)
# endif
#else
/* Solaris /usr/include/locale.h includes /usr/include/libintl.h, which
chokes if dcgettext is defined as a macro. So include it now, to make
later inclusions of <locale.h> a NOP. We don't include <libintl.h>
as well because people using "gettext.h" will not include <libintl.h>,
and also including <libintl.h> would fail on SunOS 4, whereas <locale.h>
is OK. */
#if defined(__sun)
# include <locale.h>
#endif
/* Many header files from the libstdc++ coming with g++ 3.3 or newer include
<libintl.h>, which chokes if dcgettext is defined as a macro. So include
it now, to make later inclusions of <libintl.h> a NOP. */
#if defined(__cplusplus) && defined(__GNUG__) && (__GNUC__ >= 3)
# include <cstdlib>
# if (__GLIBC__ >= 2 && !defined __UCLIBC__) || _GLIBCXX_HAVE_LIBINTL_H
# include <libintl.h>
# endif
#endif
/* Disabled NLS.
The casts to 'const char *' serve the purpose of producing warnings
for invalid uses of the value returned from these functions.
On pre-ANSI systems without 'const', the config.h file is supposed to
contain "#define const". */
# undef gettext
# define gettext(Msgid) ((const char *) (Msgid))
# undef dgettext
# define dgettext(Domainname, Msgid) ((void) (Domainname), gettext (Msgid))
# undef dcgettext
# define dcgettext(Domainname, Msgid, Category) \
((void) (Category), dgettext (Domainname, Msgid))
# undef ngettext
# define ngettext(Msgid1, Msgid2, N) \
((N) == 1 \
? ((void) (Msgid2), (const char *) (Msgid1)) \
: ((void) (Msgid1), (const char *) (Msgid2)))
# undef dngettext
# define dngettext(Domainname, Msgid1, Msgid2, N) \
((void) (Domainname), ngettext (Msgid1, Msgid2, N))
# undef dcngettext
# define dcngettext(Domainname, Msgid1, Msgid2, N, Category) \
((void) (Category), dngettext (Domainname, Msgid1, Msgid2, N))
# undef textdomain
# define textdomain(Domainname) ((const char *) (Domainname))
# undef bindtextdomain
# define bindtextdomain(Domainname, Dirname) \
((void) (Domainname), (const char *) (Dirname))
# undef bind_textdomain_codeset
# define bind_textdomain_codeset(Domainname, Codeset) \
((void) (Domainname), (const char *) (Codeset))
#endif
/* Prefer gnulib's setlocale override over libintl's setlocale override. */
#ifdef GNULIB_defined_setlocale
# undef setlocale
# define setlocale rpl_setlocale
#endif
/* A pseudo function call that serves as a marker for the automated
extraction of messages, but does not call gettext(). The run-time
translation is done at a different place in the code.
The argument, String, should be a literal string. Concatenated strings
and other string expressions won't work.
The macro's expansion is not parenthesized, so that it is suitable as
initializer for static 'char[]' or 'const char[]' variables. */
#define gettext_noop(String) String
/* The separator between msgctxt and msgid in a .mo file. */
#define GETTEXT_CONTEXT_GLUE "\004"
/* Pseudo function calls, taking a MSGCTXT and a MSGID instead of just a
MSGID. MSGCTXT and MSGID must be string literals. MSGCTXT should be
short and rarely need to change.
The letter 'p' stands for 'particular' or 'special'. */
#ifdef DEFAULT_TEXT_DOMAIN
# define pgettext(Msgctxt, Msgid) \
pgettext_aux (DEFAULT_TEXT_DOMAIN, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, LC_MESSAGES)
#else
# define pgettext(Msgctxt, Msgid) \
pgettext_aux (NULL, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, LC_MESSAGES)
#endif
#define dpgettext(Domainname, Msgctxt, Msgid) \
pgettext_aux (Domainname, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, LC_MESSAGES)
#define dcpgettext(Domainname, Msgctxt, Msgid, Category) \
pgettext_aux (Domainname, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, Category)
#ifdef DEFAULT_TEXT_DOMAIN
# define npgettext(Msgctxt, Msgid, MsgidPlural, N) \
npgettext_aux (DEFAULT_TEXT_DOMAIN, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, MsgidPlural, N, LC_MESSAGES)
#else
# define npgettext(Msgctxt, Msgid, MsgidPlural, N) \
npgettext_aux (NULL, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, MsgidPlural, N, LC_MESSAGES)
#endif
#define dnpgettext(Domainname, Msgctxt, Msgid, MsgidPlural, N) \
npgettext_aux (Domainname, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, MsgidPlural, N, LC_MESSAGES)
#define dcnpgettext(Domainname, Msgctxt, Msgid, MsgidPlural, N, Category) \
npgettext_aux (Domainname, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, MsgidPlural, N, Category)
#ifdef __GNUC__
__inline
#else
#ifdef __cplusplus
inline
#endif
#endif
static const char *
pgettext_aux (const char *domain,
const char *msg_ctxt_id, const char *msgid,
int category)
{
const char *translation = dcgettext (domain, msg_ctxt_id, category);
if (translation == msg_ctxt_id)
return msgid;
else
return translation;
}
#ifdef __GNUC__
__inline
#else
#ifdef __cplusplus
inline
#endif
#endif
static const char *
npgettext_aux (const char *domain,
const char *msg_ctxt_id, const char *msgid,
const char *msgid_plural, unsigned long int n,
int category)
{
const char *translation =
dcngettext (domain, msg_ctxt_id, msgid_plural, n, category);
if (translation == msg_ctxt_id || translation == msgid_plural)
return (n == 1 ? msgid : msgid_plural);
else
return translation;
}
/* The same thing extended for non-constant arguments. Here MSGCTXT and MSGID
can be arbitrary expressions. But for string literals these macros are
less efficient than those above. */
#include <string.h>
#if (((__GNUC__ >= 3 || __GNUG__ >= 2) && !defined __STRICT_ANSI__) \
/* || __STDC_VERSION__ >= 199901L */ )
# define _LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS 1
#else
# define _LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS 0
#endif
#if !_LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS
#include <stdlib.h>
#endif
#define pgettext_expr(Msgctxt, Msgid) \
dcpgettext_expr (NULL, Msgctxt, Msgid, LC_MESSAGES)
#define dpgettext_expr(Domainname, Msgctxt, Msgid) \
dcpgettext_expr (Domainname, Msgctxt, Msgid, LC_MESSAGES)
#ifdef __GNUC__
__inline
#else
#ifdef __cplusplus
inline
#endif
#endif
static const char *
dcpgettext_expr (const char *domain,
const char *msgctxt, const char *msgid,
int category)
{
size_t msgctxt_len = strlen (msgctxt) + 1;
size_t msgid_len = strlen (msgid) + 1;
const char *translation;
#if _LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS
char msg_ctxt_id[msgctxt_len + msgid_len];
#else
char buf[1024];
char *msg_ctxt_id =
(msgctxt_len + msgid_len <= sizeof (buf)
? buf
: (char *) malloc (msgctxt_len + msgid_len));
if (msg_ctxt_id != NULL)
#endif
{
memcpy (msg_ctxt_id, msgctxt, msgctxt_len - 1);
msg_ctxt_id[msgctxt_len - 1] = '\004';
memcpy (msg_ctxt_id + msgctxt_len, msgid, msgid_len);
translation = dcgettext (domain, msg_ctxt_id, category);
#if !_LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS
if (msg_ctxt_id != buf)
free (msg_ctxt_id);
#endif
if (translation != msg_ctxt_id)
return translation;
}
return msgid;
}
#define npgettext_expr(Msgctxt, Msgid, MsgidPlural, N) \
dcnpgettext_expr (NULL, Msgctxt, Msgid, MsgidPlural, N, LC_MESSAGES)
#define dnpgettext_expr(Domainname, Msgctxt, Msgid, MsgidPlural, N) \
dcnpgettext_expr (Domainname, Msgctxt, Msgid, MsgidPlural, N, LC_MESSAGES)
#ifdef __GNUC__
__inline
#else
#ifdef __cplusplus
inline
#endif
#endif
static const char *
dcnpgettext_expr (const char *domain,
const char *msgctxt, const char *msgid,
const char *msgid_plural, unsigned long int n,
int category)
{
size_t msgctxt_len = strlen (msgctxt) + 1;
size_t msgid_len = strlen (msgid) + 1;
const char *translation;
#if _LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS
char msg_ctxt_id[msgctxt_len + msgid_len];
#else
char buf[1024];
char *msg_ctxt_id =
(msgctxt_len + msgid_len <= sizeof (buf)
? buf
: (char *) malloc (msgctxt_len + msgid_len));
if (msg_ctxt_id != NULL)
#endif
{
memcpy (msg_ctxt_id, msgctxt, msgctxt_len - 1);
msg_ctxt_id[msgctxt_len - 1] = '\004';
memcpy (msg_ctxt_id + msgctxt_len, msgid, msgid_len);
translation = dcngettext (domain, msg_ctxt_id, msgid_plural, n, category);
#if !_LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS
if (msg_ctxt_id != buf)
free (msg_ctxt_id);
#endif
if (!(translation == msg_ctxt_id || translation == msgid_plural))
return translation;
}
return (n == 1 ? msgid : msgid_plural);
}
#endif /* _LIBGETTEXT_H */

91
configure.ac Normal file
View file

@ -0,0 +1,91 @@
# -*- Autoconf -*-
# Copyright (c) 2017, 2020 Dale Mellor
#
# This file is part of the trader-desk package.
#
# The trader-desk package 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 3 of the
# License, or (at your option) any later version.
#
# The trader-desk package 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, see http://www.gnu.org/licenses/.
# Process this file with autoconf to produce a configure script.
AC_PREREQ(2.69)
AC_INIT([trader-desk], [0.1], [dale@rdmp.org])
AC_CONFIG_AUX_DIR([build-aux])
AM_INIT_AUTOMAKE([silent-rules subdir-objects])
AM_SILENT_RULES([yes])
AC_CONFIG_SRCDIR([trader-desk/trader-desk.cc])
AC_CONFIG_HEADERS([trader-desk/auto-config.h])
AC_CONFIG_MACRO_DIRS([m4])
# Checks for programs.
AC_PROG_CC
AC_GNU_SOURCE
AC_PROG_CXX
AC_PROG_AWK
AC_PROG_CPP
AC_PROG_INSTALL
AC_PROG_LN_S
AC_PROG_MAKE_SET
AC_PROG_LIBTOOL
AM_GNU_GETTEXT([external])
AM_GNU_GETTEXT_VERSION([0.19.3])
AC_CHECK_PROGS([mysql_config], [mariadb_config mysql_config], [no])
if [[ "x$mysql_config" == "xno" ]]; then
AC_MSG_FAILURE([cannot find MySQL-type client libraries])
fi
if [[ "x$mysql_config" == "xmysql_config" ]]; then
AC_SUBST([HAVE_MYSQL], [1])
else
AC_SUBST([HAVE_MYSQL], [0])
fi
if [[ "x$mysql_config" == "xmariadb_config" ]]; then
AC_SUBST([HAVE_MARIADB], [1])
else
AC_SUBST([HAVE_MARIADB], [0])
fi
# Checks for libraries.
PKG_CHECK_MODULES([gtk_config], [gtkmm-3.0 gthread-2.0 dmbcs-market-data-api fmt])
gtk_config_CFLAGS="${pkg_cv_gtk_config_CFLAGS} `${mysql_config} --include`"
gtk_config_LIBS="${pkg_cv_gtk_config_LIBS} `${mysql_config} --libs`"
# Checks for header files.
AC_CHECK_HEADERS([libintl.h])
# Checks for typedefs, structures, and compiler characteristics.
AC_CHECK_HEADER_STDBOOL
AC_C_INLINE
AC_TYPE_SIZE_T
AC_TYPE_UINT32_T
# Checks for library functions.
AC_FUNC_MKTIME
AC_FUNC_STRTOD
AC_CHECK_FUNCS([floor localtime_r pow setlocale sqrt])
# Check for optional database pre-population data.
AM_CONDITIONAL([database_data], [test -f data.sql.xz])
AC_CONFIG_FILES([trader-desk.pc
makefile
po/Makefile.in
trader-desk/makefile])
dnl doc/makefile
AC_OUTPUT

BIN
data.sql.xz Normal file

Binary file not shown.

18
doc/.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
trader-desk.aux
trader-desk.cp
trader-desk.dvi
trader-desk.fn
trader-desk.ky
trader-desk.log
trader-desk.pg
trader-desk.tmp
trader-desk.toc
trader-desk.tp
trader-desk.vr
trader-desk.bbl
trader-desk.blg
trader-desk.info
trader-desk.man
trader-desk.html
trader-desk.cps
trader-desk.pdf

BIN
doc/detailed-analysis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
doc/detailed-analysis.xcf Normal file

Binary file not shown.

505
doc/fdl.texi Normal file
View file

@ -0,0 +1,505 @@
@c The GNU Free Documentation License.
@center Version 1.3, 3 November 2008
@c This file is intended to be included within another document,
@c hence no sectioning command or @node.
@display
Copyright @copyright{} 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc.
@uref{http://fsf.org/}
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@end display
@enumerate 0
@item
PREAMBLE
The purpose of this License is to make a manual, textbook, or other
functional and useful document @dfn{free} in the sense of freedom: to
assure everyone the effective freedom to copy and redistribute it,
with or without modifying it, either commercially or noncommercially.
Secondarily, this License preserves for the author and publisher a way
to get credit for their work, while not being considered responsible
for modifications made by others.
This License is a kind of ``copyleft'', which means that derivative
works of the document must themselves be free in the same sense. It
complements the GNU General Public License, which is a copyleft
license designed for free software.
We have designed this License in order to use it for manuals for free
software, because free software needs free documentation: a free
program should come with manuals providing the same freedoms that the
software does. But this License is not limited to software manuals;
it can be used for any textual work, regardless of subject matter or
whether it is published as a printed book. We recommend this License
principally for works whose purpose is instruction or reference.
@item
APPLICABILITY AND DEFINITIONS
This License applies to any manual or other work, in any medium, that
contains a notice placed by the copyright holder saying it can be
distributed under the terms of this License. Such a notice grants a
world-wide, royalty-free license, unlimited in duration, to use that
work under the conditions stated herein. The ``Document'', below,
refers to any such manual or work. Any member of the public is a
licensee, and is addressed as ``you''. You accept the license if you
copy, modify or distribute the work in a way requiring permission
under copyright law.
A ``Modified Version'' of the Document means any work containing the
Document or a portion of it, either copied verbatim, or with
modifications and/or translated into another language.
A ``Secondary Section'' is a named appendix or a front-matter section
of the Document that deals exclusively with the relationship of the
publishers or authors of the Document to the Document's overall
subject (or to related matters) and contains nothing that could fall
directly within that overall subject. (Thus, if the Document is in
part a textbook of mathematics, a Secondary Section may not explain
any mathematics.) The relationship could be a matter of historical
connection with the subject or with related matters, or of legal,
commercial, philosophical, ethical or political position regarding
them.
The ``Invariant Sections'' are certain Secondary Sections whose titles
are designated, as being those of Invariant Sections, in the notice
that says that the Document is released under this License. If a
section does not fit the above definition of Secondary then it is not
allowed to be designated as Invariant. The Document may contain zero
Invariant Sections. If the Document does not identify any Invariant
Sections then there are none.
The ``Cover Texts'' are certain short passages of text that are listed,
as Front-Cover Texts or Back-Cover Texts, in the notice that says that
the Document is released under this License. A Front-Cover Text may
be at most 5 words, and a Back-Cover Text may be at most 25 words.
A ``Transparent'' copy of the Document means a machine-readable copy,
represented in a format whose specification is available to the
general public, that is suitable for revising the document
straightforwardly with generic text editors or (for images composed of
pixels) generic paint programs or (for drawings) some widely available
drawing editor, and that is suitable for input to text formatters or
for automatic translation to a variety of formats suitable for input
to text formatters. A copy made in an otherwise Transparent file
format whose markup, or absence of markup, has been arranged to thwart
or discourage subsequent modification by readers is not Transparent.
An image format is not Transparent if used for any substantial amount
of text. A copy that is not ``Transparent'' is called ``Opaque''.
Examples of suitable formats for Transparent copies include plain
ASCII without markup, Texinfo input format, La@TeX{} input
format, SGML or XML using a publicly available
DTD, and standard-conforming simple HTML,
PostScript or PDF designed for human modification. Examples
of transparent image formats include PNG, XCF and
JPG@. Opaque formats include proprietary formats that can be
read and edited only by proprietary word processors, SGML or
XML for which the DTD and/or processing tools are
not generally available, and the machine-generated HTML,
PostScript or PDF produced by some word processors for
output purposes only.
The ``Title Page'' means, for a printed book, the title page itself,
plus such following pages as are needed to hold, legibly, the material
this License requires to appear in the title page. For works in
formats which do not have any title page as such, ``Title Page'' means
the text near the most prominent appearance of the work's title,
preceding the beginning of the body of the text.
The ``publisher'' means any person or entity that distributes copies
of the Document to the public.
A section ``Entitled XYZ'' means a named subunit of the Document whose
title either is precisely XYZ or contains XYZ in parentheses following
text that translates XYZ in another language. (Here XYZ stands for a
specific section name mentioned below, such as ``Acknowledgements'',
``Dedications'', ``Endorsements'', or ``History''.) To ``Preserve the Title''
of such a section when you modify the Document means that it remains a
section ``Entitled XYZ'' according to this definition.
The Document may include Warranty Disclaimers next to the notice which
states that this License applies to the Document. These Warranty
Disclaimers are considered to be included by reference in this
License, but only as regards disclaiming warranties: any other
implication that these Warranty Disclaimers may have is void and has
no effect on the meaning of this License.
@item
VERBATIM COPYING
You may copy and distribute the Document in any medium, either
commercially or noncommercially, provided that this License, the
copyright notices, and the license notice saying this License applies
to the Document are reproduced in all copies, and that you add no other
conditions whatsoever to those of this License. You may not use
technical measures to obstruct or control the reading or further
copying of the copies you make or distribute. However, you may accept
compensation in exchange for copies. If you distribute a large enough
number of copies you must also follow the conditions in section 3.
You may also lend copies, under the same conditions stated above, and
you may publicly display copies.
@item
COPYING IN QUANTITY
If you publish printed copies (or copies in media that commonly have
printed covers) of the Document, numbering more than 100, and the
Document's license notice requires Cover Texts, you must enclose the
copies in covers that carry, clearly and legibly, all these Cover
Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
the back cover. Both covers must also clearly and legibly identify
you as the publisher of these copies. The front cover must present
the full title with all words of the title equally prominent and
visible. You may add other material on the covers in addition.
Copying with changes limited to the covers, as long as they preserve
the title of the Document and satisfy these conditions, can be treated
as verbatim copying in other respects.
If the required texts for either cover are too voluminous to fit
legibly, you should put the first ones listed (as many as fit
reasonably) on the actual cover, and continue the rest onto adjacent
pages.
If you publish or distribute Opaque copies of the Document numbering
more than 100, you must either include a machine-readable Transparent
copy along with each Opaque copy, or state in or with each Opaque copy
a computer-network location from which the general network-using
public has access to download using public-standard network protocols
a complete Transparent copy of the Document, free of added material.
If you use the latter option, you must take reasonably prudent steps,
when you begin distribution of Opaque copies in quantity, to ensure
that this Transparent copy will remain thus accessible at the stated
location until at least one year after the last time you distribute an
Opaque copy (directly or through your agents or retailers) of that
edition to the public.
It is requested, but not required, that you contact the authors of the
Document well before redistributing any large number of copies, to give
them a chance to provide you with an updated version of the Document.
@item
MODIFICATIONS
You may copy and distribute a Modified Version of the Document under
the conditions of sections 2 and 3 above, provided that you release
the Modified Version under precisely this License, with the Modified
Version filling the role of the Document, thus licensing distribution
and modification of the Modified Version to whoever possesses a copy
of it. In addition, you must do these things in the Modified Version:
@enumerate A
@item
Use in the Title Page (and on the covers, if any) a title distinct
from that of the Document, and from those of previous versions
(which should, if there were any, be listed in the History section
of the Document). You may use the same title as a previous version
if the original publisher of that version gives permission.
@item
List on the Title Page, as authors, one or more persons or entities
responsible for authorship of the modifications in the Modified
Version, together with at least five of the principal authors of the
Document (all of its principal authors, if it has fewer than five),
unless they release you from this requirement.
@item
State on the Title page the name of the publisher of the
Modified Version, as the publisher.
@item
Preserve all the copyright notices of the Document.
@item
Add an appropriate copyright notice for your modifications
adjacent to the other copyright notices.
@item
Include, immediately after the copyright notices, a license notice
giving the public permission to use the Modified Version under the
terms of this License, in the form shown in the Addendum below.
@item
Preserve in that license notice the full lists of Invariant Sections
and required Cover Texts given in the Document's license notice.
@item
Include an unaltered copy of this License.
@item
Preserve the section Entitled ``History'', Preserve its Title, and add
to it an item stating at least the title, year, new authors, and
publisher of the Modified Version as given on the Title Page. If
there is no section Entitled ``History'' in the Document, create one
stating the title, year, authors, and publisher of the Document as
given on its Title Page, then add an item describing the Modified
Version as stated in the previous sentence.
@item
Preserve the network location, if any, given in the Document for
public access to a Transparent copy of the Document, and likewise
the network locations given in the Document for previous versions
it was based on. These may be placed in the ``History'' section.
You may omit a network location for a work that was published at
least four years before the Document itself, or if the original
publisher of the version it refers to gives permission.
@item
For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve
the Title of the section, and preserve in the section all the
substance and tone of each of the contributor acknowledgements and/or
dedications given therein.
@item
Preserve all the Invariant Sections of the Document,
unaltered in their text and in their titles. Section numbers
or the equivalent are not considered part of the section titles.
@item
Delete any section Entitled ``Endorsements''. Such a section
may not be included in the Modified Version.
@item
Do not retitle any existing section to be Entitled ``Endorsements'' or
to conflict in title with any Invariant Section.
@item
Preserve any Warranty Disclaimers.
@end enumerate
If the Modified Version includes new front-matter sections or
appendices that qualify as Secondary Sections and contain no material
copied from the Document, you may at your option designate some or all
of these sections as invariant. To do this, add their titles to the
list of Invariant Sections in the Modified Version's license notice.
These titles must be distinct from any other section titles.
You may add a section Entitled ``Endorsements'', provided it contains
nothing but endorsements of your Modified Version by various
parties---for example, statements of peer review or that the text has
been approved by an organization as the authoritative definition of a
standard.
You may add a passage of up to five words as a Front-Cover Text, and a
passage of up to 25 words as a Back-Cover Text, to the end of the list
of Cover Texts in the Modified Version. Only one passage of
Front-Cover Text and one of Back-Cover Text may be added by (or
through arrangements made by) any one entity. If the Document already
includes a cover text for the same cover, previously added by you or
by arrangement made by the same entity you are acting on behalf of,
you may not add another; but you may replace the old one, on explicit
permission from the previous publisher that added the old one.
The author(s) and publisher(s) of the Document do not by this License
give permission to use their names for publicity for or to assert or
imply endorsement of any Modified Version.
@item
COMBINING DOCUMENTS
You may combine the Document with other documents released under this
License, under the terms defined in section 4 above for modified
versions, provided that you include in the combination all of the
Invariant Sections of all of the original documents, unmodified, and
list them all as Invariant Sections of your combined work in its
license notice, and that you preserve all their Warranty Disclaimers.
The combined work need only contain one copy of this License, and
multiple identical Invariant Sections may be replaced with a single
copy. If there are multiple Invariant Sections with the same name but
different contents, make the title of each such section unique by
adding at the end of it, in parentheses, the name of the original
author or publisher of that section if known, or else a unique number.
Make the same adjustment to the section titles in the list of
Invariant Sections in the license notice of the combined work.
In the combination, you must combine any sections Entitled ``History''
in the various original documents, forming one section Entitled
``History''; likewise combine any sections Entitled ``Acknowledgements'',
and any sections Entitled ``Dedications''. You must delete all
sections Entitled ``Endorsements.''
@item
COLLECTIONS OF DOCUMENTS
You may make a collection consisting of the Document and other documents
released under this License, and replace the individual copies of this
License in the various documents with a single copy that is included in
the collection, provided that you follow the rules of this License for
verbatim copying of each of the documents in all other respects.
You may extract a single document from such a collection, and distribute
it individually under this License, provided you insert a copy of this
License into the extracted document, and follow this License in all
other respects regarding verbatim copying of that document.
@item
AGGREGATION WITH INDEPENDENT WORKS
A compilation of the Document or its derivatives with other separate
and independent documents or works, in or on a volume of a storage or
distribution medium, is called an ``aggregate'' if the copyright
resulting from the compilation is not used to limit the legal rights
of the compilation's users beyond what the individual works permit.
When the Document is included in an aggregate, this License does not
apply to the other works in the aggregate which are not themselves
derivative works of the Document.
If the Cover Text requirement of section 3 is applicable to these
copies of the Document, then if the Document is less than one half of
the entire aggregate, the Document's Cover Texts may be placed on
covers that bracket the Document within the aggregate, or the
electronic equivalent of covers if the Document is in electronic form.
Otherwise they must appear on printed covers that bracket the whole
aggregate.
@item
TRANSLATION
Translation is considered a kind of modification, so you may
distribute translations of the Document under the terms of section 4.
Replacing Invariant Sections with translations requires special
permission from their copyright holders, but you may include
translations of some or all Invariant Sections in addition to the
original versions of these Invariant Sections. You may include a
translation of this License, and all the license notices in the
Document, and any Warranty Disclaimers, provided that you also include
the original English version of this License and the original versions
of those notices and disclaimers. In case of a disagreement between
the translation and the original version of this License or a notice
or disclaimer, the original version will prevail.
If a section in the Document is Entitled ``Acknowledgements'',
``Dedications'', or ``History'', the requirement (section 4) to Preserve
its Title (section 1) will typically require changing the actual
title.
@item
TERMINATION
You may not copy, modify, sublicense, or distribute the Document
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense, or distribute it is void, and
will automatically terminate your rights under this License.
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, receipt of a copy of some or all of the same material does
not give you any rights to use it.
@item
FUTURE REVISIONS OF THIS LICENSE
The Free Software Foundation may publish new, revised versions
of the GNU Free Documentation License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns. See
@uref{http://www.gnu.org/copyleft/}.
Each version of the License is given a distinguishing version number.
If the Document specifies that a particular numbered version of this
License ``or any later version'' applies to it, you have the option of
following the terms and conditions either of that specified version or
of any later version that has been published (not as a draft) by the
Free Software Foundation. If the Document does not specify a version
number of this License, you may choose any version ever published (not
as a draft) by the Free Software Foundation. If the Document
specifies that a proxy can decide which future versions of this
License can be used, that proxy's public statement of acceptance of a
version permanently authorizes you to choose that version for the
Document.
@item
RELICENSING
``Massive Multiauthor Collaboration Site'' (or ``MMC Site'') means any
World Wide Web server that publishes copyrightable works and also
provides prominent facilities for anybody to edit those works. A
public wiki that anybody can edit is an example of such a server. A
``Massive Multiauthor Collaboration'' (or ``MMC'') contained in the
site means any set of copyrightable works thus published on the MMC
site.
``CC-BY-SA'' means the Creative Commons Attribution-Share Alike 3.0
license published by Creative Commons Corporation, a not-for-profit
corporation with a principal place of business in San Francisco,
California, as well as future copyleft versions of that license
published by that same organization.
``Incorporate'' means to publish or republish a Document, in whole or
in part, as part of another Document.
An MMC is ``eligible for relicensing'' if it is licensed under this
License, and if all works that were first published under this License
somewhere other than this MMC, and subsequently incorporated in whole
or in part into the MMC, (1) had no cover texts or invariant sections,
and (2) were thus incorporated prior to November 1, 2008.
The operator of an MMC Site may republish an MMC contained in the site
under CC-BY-SA on the same site at any time before August 1, 2009,
provided the MMC is eligible for relicensing.
@end enumerate
@page
@heading ADDENDUM: How to use this License for your documents
To use this License in a document you have written, include a copy of
the License in the document and put the following copyright and
license notices just after the title page:
@smallexample
@group
Copyright (C) @var{year} @var{your name}.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
Texts. A copy of the license is included in the section entitled ``GNU
Free Documentation License''.
@end group
@end smallexample
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
replace the ``with@dots{}Texts.''@: line with this:
@smallexample
@group
with the Invariant Sections being @var{list their titles}, with
the Front-Cover Texts being @var{list}, and with the Back-Cover Texts
being @var{list}.
@end group
@end smallexample
If you have Invariant Sections without Cover Texts, or some other
combination of the three, merge those two alternatives to suit the
situation.
If your document contains nontrivial examples of program code, we
recommend releasing these examples in parallel under your choice of
free software license, such as the GNU General Public License,
to permit their use in free software.
@c Local Variables:
@c ispell-local-pdict: "ispell-dict"
@c End:

BIN
doc/grid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
doc/grid.xcf Normal file

Binary file not shown.

24
doc/makefile.am Normal file
View file

@ -0,0 +1,24 @@
man1_MANS = trader-desk.man
info_INFO = trader-desk.info
pkgdata_DATA = trader-desk.pdf trader-desk.html
trader-desk : trader-desk.texinfo trader-desk.css
makeinfo --html --css-include=trader-desk.css $<
cp {detailed-analysis,grid,preferences}.png trader-desk
trader-desk.html : trader-desk.texinfo trader-desk.css
makeinfo --html --no-split --no-headers --css-include=trader-desk.css $<
trader-desk.pdf : trader-desk.texinfo
makeinfo --pdf --no-headers $<
trader-desk.info : trader-desk.texinfo
makeinfo $<
CLEANFILES = trader-desk.html trader-desk.info trader-desk.pdf \
trader-desk.aux trader-desk.bbl trader-desk.blg trader-desk.cp \
trader-desk.cps trader-desk.dvi trader-desk.fn \
trader-desk.ky trader-desk.log trader-desk.pg trader-desk.tmp \
trader-desk.toc trader-desk.tp trader-desk.vr

BIN
doc/preferences.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
doc/preferences.xcf Normal file

Binary file not shown.

5
doc/trader-desk.css Normal file
View file

@ -0,0 +1,5 @@
body { max-width: 800px;
font-family: times;
text-align: justify;
margin: 0 auto;
padding: 0 0.5em; }

1025
doc/trader-desk.texinfo Normal file

File diff suppressed because it is too large Load diff

58
makefile.am Normal file
View file

@ -0,0 +1,58 @@
# Copyright (c) 2020 Dale Mellor
#
# This file is part of the trader-desk package.
#
# The trader-desk package 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 3 of the
# License, or (at your option) any later version.
#
# The trader-desk package 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, see http://www.gnu.org/licenses/.
SUBDIRS = trader-desk . po
ACLOCAL_AMFLAGS = --install -I m4
if database_data
DTA = data.sql.xz
else
DTA =
endif
dist_pkgdata_DATA = trader-desk.png ${DTA}
pkgconfigdir = ${libdir}/pkgconfig
pkgconfig_DATA = trader-desk.pc
DISTFILES = ABOUT-NLS
MAINTAINERCLEANFILES = ABOUT-NLS \
aclocal.m4 \
build-aux/compile \
build-aux/config.guess \
build-aux/config.rpath \
build-aux/config.sub \
build-aux/depcomp \
build-aux/install-sh \
build-aux/ltmain.sh \
build-aux/missing \
config.status \
config.log \
configure \
makefile \
makefile.in \
po/en@boldquot.po \
po/en@quot.po \
po/Makefile.in.in \
po/Makevars.template \
po/trader-desk.pot
# Don't know why we need to do this.
dist-hook:
cp -rp aclocal.m4 AUTHORS build-aux ChangeLog configure configure.ac COPYING INSTALL libtool makefile.am makefile.in NEWS README trader-desk.png $(top_distdir)

19
po/.gitignore vendored Normal file
View file

@ -0,0 +1,19 @@
*.gmo
ChangeLog
en@*
Makefile
Makefile.in
Makefile.in.in
Makevars.template
messages.mo
POTFILES
Rules-quot
boldquot.sed
en@boldquot.header
en@quot.header
insert-header.sin
quot.sed
remove-potcdate.sed
remove-potcdate.sin
stamp-po
trader-desk.pot

2
po/LINGUAS Normal file
View file

@ -0,0 +1,2 @@
en@quot
en@boldquot

98
po/Makevars Normal file
View file

@ -0,0 +1,98 @@
# Copyright (c) 2017 Dale Mellor
#
#
# This file is part of the trader-desk package.
#
# The trader-desk package 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 3 of the
# License, or (at your option) any later version.
#
# The trader-desk package 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, see http://www.gnu.org/licenses/.
# Makefile variables for PO directory in any package using GNU gettext.
# Usually the message domain is the same as the package name.
DOMAIN = $(PACKAGE)
# These two variables depend on the location of this directory.
subdir = po
top_builddir = ..
# These options get passed to xgettext.
XGETTEXT_OPTIONS = --keyword=_ --keyword=N_
# This is the copyright holder that gets inserted into the header of the
# $(DOMAIN).pot file. Set this to the copyright holder of the surrounding
# package. (Note that the msgstr strings, extracted from the package's
# sources, belong to the copyright holder of the package.) Translators are
# expected to transfer the copyright for their translations to this person
# or entity, or to disclaim their copyright. The empty string stands for
# the public domain; in this case the translators are expected to disclaim
# their copyright.
COPYRIGHT_HOLDER = Dale Mellor
# This tells whether or not to prepend "GNU " prefix to the package
# name that gets inserted into the header of the $(DOMAIN).pot file.
# Possible values are "yes", "no", or empty. If it is empty, try to
# detect it automatically by scanning the files in $(top_srcdir) for
# "GNU packagename" string.
PACKAGE_GNU = no
# This is the email address or URL to which the translators shall report
# bugs in the untranslated strings:
# - Strings which are not entire sentences, see the maintainer guidelines
# in the GNU gettext documentation, section 'Preparing Strings'.
# - Strings which use unclear terms or require additional context to be
# understood.
# - Strings which make invalid assumptions about notation of date, time or
# money.
# - Pluralisation problems.
# - Incorrect English spelling.
# - Incorrect formatting.
# It can be your email address, or a mailing list address where translators
# can write to without being subscribed, or the URL of a web page through
# which the translators can contact you.
MSGID_BUGS_ADDRESS =
# This is the list of locale categories, beyond LC_MESSAGES, for which the
# message catalogs shall be used. It is usually empty.
EXTRA_LOCALE_CATEGORIES =
# This tells whether the $(DOMAIN).pot file contains messages with an 'msgctxt'
# context. Possible values are "yes" and "no". Set this to yes if the
# package uses functions taking also a message context, like pgettext(), or
# if in $(XGETTEXT_OPTIONS) you define keywords with a context argument.
USE_MSGCTXT = no
# These options get passed to msgmerge.
# Useful options are in particular:
# --previous to keep previous msgids of translated messages,
# --quiet to reduce the verbosity.
MSGMERGE_OPTIONS =
# These options get passed to msginit.
# If you want to disable line wrapping when writing PO files, add
# --no-wrap to MSGMERGE_OPTIONS, XGETTEXT_OPTIONS, and
# MSGINIT_OPTIONS.
MSGINIT_OPTIONS =
# This tells whether or not to regenerate a PO file when $(DOMAIN).pot
# has changed. Possible values are "yes" and "no". Set this to no if
# the POT file is checked in the repository and the version control
# program ignores timestamps.
PO_DEPENDS_ON_POT = yes
# This tells whether or not to forcibly update $(DOMAIN).pot and
# regenerate PO files on "make dist". Possible values are "yes" and
# "no". Set this to no if the POT file and PO files are maintained
# externally.
DIST_DEPENDS_ON_UPDATE_PO = yes

27
po/POTFILES.in Normal file
View file

@ -0,0 +1,27 @@
# Copyright (c) 2020 Dale Mellor
#
# This file is part of the trader-desk package.
#
# The trader-desk package 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 3 of the
# License, or (at your option) any later version.
#
# The trader-desk package 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, see http://www.gnu.org/licenses/.
# List of source files which contain translatable strings.
trader-desk/chart.cc
trader-desk/company-name-entry.cc
trader-desk/date-range-scale.cc
trader-desk/moving-average-analyzer.h
trader-desk/sd-envelope-analyzer.h
trader-desk/shares-scale.cc
trader-desk/trader-desk.cc

179
po/en_GB.po Normal file
View file

@ -0,0 +1,179 @@
# English translations for trader-desk package.
# Copyright (C) 2020 Dale Mellor
# This file is distributed under the same license as the trader-desk package.
#
msgid ""
msgstr ""
"Project-Id-Version: trader-desk 0.1\n"
"POT-Creation-Date: 2017-02-10 12:31+0000\n"
"PO-Revision-Date: 2017-02-10 12:35+0000\n"
"Last-Translator: Dale Mellor\n"
"Language-Team: English (British)\n"
"Language: en_GB\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: trader-desk/chart.cc:257
msgctxt "Label"
msgid "Position value (pounds)"
msgstr "Position value (£)"
#: trader-desk/company-name-entry.cc:44
msgctxt "Label"
msgid "Company name: "
msgstr "Company name: "
#: trader-desk/date-range-scale.cc:9
#, c-format
msgctxt "Label"
msgid "Date range = %.0f days"
msgstr "Date range = %.0f days"
#: trader-desk/moving-average-analyzer.h:29
#, c-format
msgctxt "Label"
msgid "Mean window = %.0f days"
msgstr "Mean window = %.0f days"
#: trader-desk/positions-widget.cc:14
msgctxt "Label"
msgid "Positions"
msgstr "Positions"
#: trader-desk/positions-widget.cc:24
msgctxt "Label"
msgid "None"
msgstr "None"
#: trader-desk/sd-envelope-analyzer.h:32
#, c-format
msgctxt "Label Abbrev:Standard deviation"
msgid "Envelope width = %.2f x std. dev."
msgstr "Envelope width = %.2f x std. dev."
#: trader-desk/shares-scale.cc:12
#, c-format
msgctxt "Label"
msgid "Number of shares = %.0f"
msgstr "Number of shares = %.0f"
#: trader-desk/trader-desk.cc:27
msgctxt "Window title"
msgid "Updating database"
msgstr "Updating database"
#: trader-desk/trader-desk.cc:36
msgctxt "Information"
msgid "trader-desk: Fetching data from Internet"
msgstr "trader-desk: Fetching data from Internet"
#: trader-desk/trader-desk.cc:55
msgctxt "Label"
msgid "Fixed cost of trading"
msgstr "Fixed cost of trading"
#: trader-desk/trader-desk.cc:59
msgctxt "Units:Worded:Monetary"
msgid "pounds"
msgstr "pounds"
#: trader-desk/trader-desk.cc:62
msgctxt "Label"
msgid "Proportional cost of trading"
msgstr "Proportional cost of trading"
#: trader-desk/trader-desk.cc:65
msgctxt "Units:Worded"
msgid "percent"
msgstr "percent"
#: trader-desk/trader-desk.cc:76
msgctxt "Window title"
msgid "Trader-Desk: Preferences"
msgstr "Trader-Desk: Preferences"
#: trader-desk/trader-desk.cc:155 trader-desk/trader-desk.cc:167
msgctxt "Instruction"
msgid "Select market"
msgstr "Select market"
#: trader-desk/trader-desk.cc:175
msgctxt "Join-A"
msgid "Sorry, there are no"
msgstr "Sorry, there are no"
#: trader-desk/trader-desk.cc:179
msgctxt "Join-A"
msgid "new markets."
msgstr "new markets."
#: trader-desk/trader-desk.cc:182 trader-desk/trader-desk.cc:232
msgctxt "Instruction"
msgid "Cancel"
msgstr "Cancel"
#: trader-desk/trader-desk.cc:208
msgctxt "Label"
msgid "Market"
msgstr "Market"
#: trader-desk/trader-desk.cc:224
msgctxt "Instruction, Join-B"
msgid "Double-click a market name"
msgstr "Double-click a market name"
#: trader-desk/trader-desk.cc:229
msgctxt "Instruction, Join-B"
msgid "to ingest its data"
msgstr "to ingest its data"
#: trader-desk/trader-desk.cc:350 trader-desk/trader-desk.cc:386
msgid "No Internet Connection"
msgstr "No Internet Connection"
#: trader-desk/trader-desk.cc:446
msgctxt "Menu"
msgid "_File"
msgstr "_File"
#: trader-desk/trader-desk.cc:448
msgctxt "Menu"
msgid "_Preferences"
msgstr "_Preferences"
#: trader-desk/trader-desk.cc:453
msgctxt "Menu"
msgid "_Display"
msgstr "_Display"
#: trader-desk/trader-desk.cc:455
msgctxt "Menu"
msgid "_Market"
msgstr "_Market"
#: trader-desk/trader-desk.cc:457
msgctxt "Menu"
msgid "_Help"
msgstr "_Help"
#: trader-desk/trader-desk.cc:458
msgctxt "Menu"
msgid "_About"
msgstr "_About"
#: trader-desk/trader-desk.cc:501
msgctxt "Menu"
msgid "Update _latest data"
msgstr "Update _latest data"
#: trader-desk/trader-desk.cc:506
msgctxt "Menu"
msgid "Update _close data"
msgstr "Update _close data"
#: trader-desk/trader-desk.cc:511
msgctxt "Menu"
msgid "_Ingest new market"
msgstr "_Ingest new market"

113
setup-hint.sh Normal file
View file

@ -0,0 +1,113 @@
#!/bin/bash
# Get an up to date system with all the packages we require.
sudo apt update
sudo apt upgrade -y
sudo apt install -y git build-essential libmariadbclient-dev \
mariadb-server autoconf autopoint \
libssl-dev libtool libgtkmm-3.0-dev \
libcurl4-openssl-dev cmake texinfo xauth \
gettext ed
# Set up the database to make it possible for anybody (in particular the
# trader-desk application) to use the database root account.
sudo mysql mysql <<-\EOF
update mysql.user set password='', plugin='';
flush privileges;
exit
EOF
# Download, build and install gcc 9.3. We use the latest C++20 standards,
# and need the best compiler we can get.
sudo bash -c 'cat > /etc/ld.so.conf.d/01-local.conf' <<-\EOF
/usr/local/lib64
/usr/local/lib
EOF
sudo ldconfig
mkdir ${HOME}/sources; cd ${HOME}/sources
wget ftp://ftp.gnu.org/gnu/gcc/gcc-9.3.0/gcc-9.3.0.tar.xz
wget ftp://ftp.gnu.org/gnu/gmp/gmp-6.2.0.tar.xz
wget ftp://ftp.gnu.org/gnu/mpc/mpc-1.1.0.tar.gz
wget ftp://ftp.gnu.org/gnu/mpfr/mpfr-4.0.2.tar.xz
tar xf gcc-9.3.0.tar.xz
cd gcc-9.3.0
tar xf ../gmp-6.2.0.tar.xz
ln -s gmp-6.2.0 gmp
tar xf ../mpc-1.1.0.tar.gz
ln -s mpc-1.1.0 mpc
tar xf ../mpfr-4.0.2.tar.xz
ln -s mpfr-4.0.2 mpfr
./configure --enable-languages=c,c++ --disable-bootstrap --disable-multilib
make -j2 # Takes a long time.
sudo make install
cd ${HOME}/sources; rm -rf gcc-9.3.0
# Make a couple of third-party packages that dont come with the Debian
# system.
cmake_build() { cd $1
mkdir build
cd build
cmake -DBUILD_SHARED_LIBS=TRUE ..
make -j2
sudo make install
}
cd ${HOME}/sources
git clone https://github.com/fmtlib/fmt.git
( cmake_build fmt )
cd ${HOME}/sources
git clone https://github.com/jpbarrette/curlpp.git
( cmake_build curlpp )
sudo ed /usr/local/lib/pkgconfig/curlpp.pc <<-\EOF
1,$s@-Llib@-L${prefix}/lib@
w
q
EOF
# Now build the DMBCS packages which make up the trader-desk application.
autotools_build() { cd $1
autoreconf --install
./configure
make -j2
sudo make install
}
cd ${HOME}
git clone https://rdmp.org/dmbcs/market-data-api.git dmbcs-market-data-api
( autotools_build dmbcs-market-data-api )
git clone https://rdmp.org/dmbcs/trader-desk.git dmbcs-trader-desk
( autotools_build dmbcs-trader-desk )
sudo ldconfig
mkdir ${HOME}/.config
# Take heed of this message!
cat <<EOF
The dmbcs-trader-desk installation is complete. If you have just done
this on a fresh Debian installation, you will now need to log out and
then back in again with the -X option on the ssh line, so that
graphics will appear in front of you.
Then just type trader-desk.
EOF

13
trader-desk.pc.in Normal file
View file

@ -0,0 +1,13 @@
prefix = @prefix@
exec_prefix = ${prefix}
libdir = ${exec_prefix}/lib
includedir = ${prefix}/include
Name : trader-desk
Description : DMBCS Trader Desk
Version : @VERSION@
Requires : gtkmm-3.0 gthread-2.0 dmbcs-market-data-api
Libs : -L${libdir} @gtk_config_LIBS@ \
-ldmbcs-market-data-api -ltrader-desk
Cflags : -I${includedir} @gtk_config_CFLAGS@ \
-DHAVE_MYSQL=@HAVE_MYSQL@ -DHAVE_MARIADB=@HAVE_MARIADB@

BIN
trader-desk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

View file

@ -0,0 +1,89 @@
#include <trader-desk/alpha-vantage--monitor.h>
namespace DMBCS::Trader_Desk {
unique_ptr<Alpha_Vantage__Monitor::Clocks> Alpha_Vantage__Monitor::clocks;
void Alpha_Vantage__Monitor::make_clock_widgets (Preferences& P,
Gtk::HBox& C)
{
initialize_clocks (P);
Clocks& c {*clocks};
C.pack_start (c.starter);
C.pack_start (c.count_down);
C.pack_start (c.spacer);
C.pack_start (c.strikes_);
C.pack_start (c.finisher);
}
void Alpha_Vantage__Monitor::remove_clock_widgets (Gtk::Container& C)
{
Clocks& c {*clocks};
C.remove (c.starter);
C.remove (c.count_down);
C.remove (c.spacer);
C.remove (c.strikes_);
C.remove (c.finisher);
clocks.reset ();
}
using system_clock = chrono::system_clock;
void Alpha_Vantage__Monitor::Clocks::hit ()
{
try
{
db.quick() << "insert ignore into alphavantage_ticks (time) value ("
<< (system_clock::to_time_t (system_clock::now ()))
<< ")";
}
catch (Mysql::DB_Connection::Exception&)
{
db.reconnect (db.current_preferences);
}
last_time = chrono::steady_clock::now ();
}
static int seconds_since (const chrono::steady_clock::time_point& T)
{ return chrono::duration_cast<chrono::seconds>
(chrono::steady_clock::now () - T)
.count (); }
using C = Alpha_Vantage__Monitor::Clocks;
const C& C::update ()
{
const time_t now {system_clock::to_time_t (system_clock::now ())};
try
{
auto I {db.instruction ()};
I << "delete from alphavantage_ticks where time<"
<< (now - 24 * 60 * 60);
I.execute ();
strikes
= db.scalar_result<int> (0, "select count(*) from alphavantage_ticks");
strikes_.set_text (to_string (strikes));
count_down.set_text (to_string (max (0,
12 - seconds_since (last_time))));
}
catch (Mysql::DB_Connection::Exception&) {}
return *this;
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,90 @@
#ifndef DMBCS__TRADER_DESK__ALPHA_VANTAGE__MONITOR__H
#define DMBCS__TRADER_DESK__ALPHA_VANTAGE__MONITOR__H
#include <trader-desk/alpha-vantage.h>
namespace DMBCS::Trader_Desk {
struct Alpha_Vantage__Monitor
{
using Error = Alpha_Vantage::Error;
using Throttled = Alpha_Vantage::Throttled;
using Bad_API_Key = Alpha_Vantage::Bad_API_Key;
using TO_DO = Alpha_Vantage::TO_DO;
struct Clocks
{
DB db;
chrono::steady_clock::time_point last_time;
int strikes {0};
Gtk::Label starter {"AlphaVantage charge: "};
Gtk::Label count_down {"0"};
Gtk::Label spacer {" "};
Gtk::Label strikes_ {"0"};
Gtk::Label finisher {"/500"};
explicit Clocks (Preferences& P) : db {P}
{ update (); }
void hit ();
const Clocks& update ();
};
static unique_ptr<Clocks> clocks;
static void initialize_clocks (Preferences& P)
{
if (! Alpha_Vantage__Monitor::clocks)
Alpha_Vantage__Monitor::clocks = make_unique<Clocks> (P);
}
static void make_clock_widgets (Preferences&, Gtk::HBox& container);
static void remove_clock_widgets (Gtk::Container&);
static TO_DO get_closing_prices /* override */
(const Update_Closing_Prices::Company& C,
const string& market_component_extension,
Preferences& P,
const function <void (const Update_Closing_Prices::Data&)> injector)
{
initialize_clocks (P);
const TO_DO ret {Alpha_Vantage::get_closing_prices
(C, market_component_extension, P, injector)};
if (ret == TO_DO::FINISHED) clocks->hit ();
return ret;
}
static Update_Latest_Prices::Data get_latest_data /* override */
(const Update_Latest_Prices::Company& C,
const string& market_component_extension,
Preferences& P)
{
initialize_clocks (P);
auto ret {Alpha_Vantage::get_latest_data
(C, market_component_extension, P)};
clocks->hit ();
return ret;
}
} ; /* End of class Alpha_Vantage__Monitor. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__ALPHA_VANTAGE__MONITOR__H. */

View file

@ -0,0 +1,251 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/alpha-vantage.h>
#include <trader-desk/time-series.h> /* For time utilities. */
#include <curlpp/Options.hpp>
#include <curlpp/Easy.hpp>
#include <fmt/format.h>
#include <regex>
namespace DMBCS::Trader_Desk {
static bool throttle ()
{
namespace C = chrono;
using clock = C::system_clock;
static clock::time_point LAST_CALL;
const C::duration wait {clock::now () - LAST_CALL};
if (wait < C::seconds {12})
{
usleep (min (number<C::microseconds> (C::seconds {12} - wait),
number<C::microseconds> (C::milliseconds {500})));
return 1;
}
LAST_CALL = clock::now ();
return 0;
}
static void throw_error (const string& query, const string& json)
{
static const regex re {"\"(Note|Error Message)\": +\"([^\"]*)"};
smatch match;
if (! regex_search (json, match, re))
throw Alpha_Vantage::Error
{"AlphaVantage returned garbled error message"};
if (match [1] == "Note") throw Alpha_Vantage::Throttled {match [2]};
static const regex re2 {"apikey.*(invalid|missing)", regex::icase};
if (regex_search (match [2].str (), re2))
throw Alpha_Vantage::Bad_API_Key {match [2]};
throw Alpha_Vantage::Error
{match [2].str ()
+ "\n\n[The query was " + query + ".]"};
}
static string get_curl_response (const string& query)
{
namespace Curl = cURLpp;
namespace Opt = Curl::Options;
string response;
Curl::Easy request;
request.setOpt (Opt::Url {query});
request.setOpt (Opt::WriteFunction
{[&response] (char* buffer, size_t size, size_t n)
{ response += string {buffer,
buffer + size * n};
return size * n; }});
request.setOpt (Opt::SslVerifyPeer {0});
request.perform ();
return response;
}
static time_t to_time_t (const Update_Closing_Prices::Data& D)
{ return t (D.year, D.month, D.day); }
inline string maybe_dot (const string& X)
{ return X.length () ? '.' + X : X; }
static string get_timeseries_csv
(const Update_Closing_Prices::Company& company,
string market_component_extension,
const string& api_key,
const bool over_100)
{
const string query
{fmt::format ("https://www.alphavantage.co/query"
"?function=TIME_SERIES_DAILY_ADJUSTED"
"&symbol={}"
"&apikey={}"
"&datatype=csv"
"&outputsize={}",
company.symbol + maybe_dot (market_component_extension),
api_key,
over_100 ? "full" : "compact") };
const string ret {get_curl_response (query)};
if (ret [0] == '{') throw_error (query, ret);
return ret;
}
static vector<string> fields (const string& csv, const char separator)
{
vector<string> ret;
for (size_t cursor {0}; ; )
{
const size_t next_cursor {csv.find (separator, cursor)};
ret.push_back (csv.substr (cursor, next_cursor - cursor));
if (next_cursor == csv.npos) return ret;
cursor = next_cursor + 1;
}
}
static istream& operator>> (istream& I, Update_Closing_Prices::Data& D)
{
static char comma;
static double dividend_amount, split_coefficient;
I >> D.year >> comma >> D.month >> comma >> D.day >> comma
>> D.open >> comma >> D.high >> comma
>> D.low >> comma >> D.close >> comma
>> D.adj_close >> comma >> D.volume >> comma
>> dividend_amount >> comma >> split_coefficient;
return I;
}
static vector<Update_Closing_Prices::Data> parse_csv
(const string& results, const int company_seqid)
{
istringstream O {results.substr (results.find ('\n'))};
vector<Update_Closing_Prices::Data> data;
for (;;) { Update_Closing_Prices::Data datum;
datum.company_seqid = company_seqid;
O >> datum;
if (! O.good ()) return data;
data.push_back (move (datum)); }
}
static int days_ago (const time_t T)
{
using Clock = chrono::system_clock;
return number<chrono::hours> (Clock::now ()
- Clock::from_time_t (T))
/ 24;
}
auto Alpha_Vantage::get_closing_prices
(const Update_Closing_Prices::Company& company,
const string& market_component_extension,
const Preferences& P,
const function <void (const Update_Closing_Prices::Data&)> injector)
-> TO_DO
{
if (throttle ()) return MORE_WORK;
vector<Update_Closing_Prices::Data> data
{parse_csv (get_timeseries_csv
(company,
market_component_extension,
P.market_data_service_key,
days_ago (company.last_close_date) > 100),
company.seqid)};
auto i { find_if (data.rbegin (), data.rend (),
[L = company.last_close_date]
(const Update_Closing_Prices::Data& D)
{ return to_time_t (D) > L; })};
if (i != data.rbegin()) --i;
for (auto j {i}; j != data.rend (); ++j)
injector (*j);
return FINISHED;
}
static string get_snap_csv (const Update_Latest_Prices::Company& company,
const string& market_component_extension,
const string& api_key)
{
const auto query
{ fmt::format ("https://www.alphavantage.co/query"
"?function=GLOBAL_QUOTE"
"&symbol={}"
"&datatype=csv"
"&apikey={}",
company.symbol + maybe_dot (market_component_extension),
api_key) };
const string ret {get_curl_response (query)};
if (ret [0] == '{') throw_error (query, ret);
return ret;
}
inline double extract_price_csv (const string& X)
{
return strtod (fields (fields (X, '\n') [1], ',') [4].data (),
nullptr);
}
Update_Latest_Prices::Data Alpha_Vantage::get_latest_data
(const Update_Latest_Prices::Company& company,
const string& market_component_extension,
const Preferences& P)
{
while (throttle ()) ;
return
{ .company_seqid = company.seqid,
.time = chrono::system_clock::now (),
.price = extract_price_csv
(get_snap_csv (company,
market_component_extension,
P.market_data_service_key)) };
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__ALPHA_VANTAGE__H
#define DMBCS__TRADER_DESK__ALPHA_VANTAGE__H
#include <trader-desk/update-closing-prices.h>
#include <trader-desk/update-latest-prices.h>
namespace DMBCS::Trader_Desk {
/* This is a struct rather than a namespace because we want to be able
* to derive from it a version with a proper API key, and want to
* alias it as a type in update_*_prices.cc.
*
* The reason for this seemingly unnecessary abstraction is that in
* the past we use Yahoo! Finance for this purpose, but they pulled
* their API. We now want to keep things flexible so that other data
* sources might be used in future. */
struct Alpha_Vantage
{
struct Error : runtime_error { using runtime_error::runtime_error; };
struct Throttled : Error { using Error::Error; };
struct Bad_API_Key : Error { using Error::Error; };
enum TO_DO : bool {FINISHED = 0, MORE_WORK = 1};
static TO_DO get_closing_prices /* override */
(const Update_Closing_Prices::Company&,
const string& market_component_extension,
const Preferences& P,
const function <void (const Update_Closing_Prices::Data&)> injector);
static Update_Latest_Prices::Data get_latest_data /* override */
(const Update_Latest_Prices::Company&,
const string& market_component_extension,
const Preferences&);
} ; /* End of class Alpha_Vantage. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__ALPHA_VANTAGE__H. */

48
trader-desk/analyzer.cc Normal file
View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/delta-analyzer.h>
#include <trader-desk/sd-envelope-analyzer.h>
/** \file
*
* Implementation of the \c Analyzer_Stack constructor, a factory which
* knows about all the detailed analyzers. If a new analyzer is to be
* added to the system, this file will need modifying to accomodate it;
* it is the *only* place that needs to know anything about specific
* analyzers. */
namespace DMBCS::Trader_Desk {
Analyzer_Stack::Analyzer_Stack (Chart_Data& chart_data, Preferences&)
{
analyzers.emplace_back (new SD_Envelope_Analyzer {chart_data});
analyzers.emplace_back (new Delta_Analyzer {chart_data});
for (auto &a : analyzers)
a -> signal_redraw_needed ()
. connect ([this] { redraw_needed_signal.emit (); });
}
} /* End of namespace DMBCS::Trader_Desk. */

195
trader-desk/analyzer.h Normal file
View file

@ -0,0 +1,195 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__ANALYZER__H
#define DMBCS__TRADER_DESK__ANALYZER__H
#include <trader-desk/chart-context.h>
#include <trader-desk/chart-data.h>
#include <trader-desk/tide-mark.h>
#include <gtkmm.h>
/** \file
*
* Declaration of the \c Analyzer pure interface, and of the \c
* Analyzer_Stack class. */
namespace DMBCS::Trader_Desk {
/** Abstract idea of an object which can draw anything it wants onto a
* chart, hopefully to provide some useful illumination of the data to
* the user. Additionally a control widget can be given which will
* appear at the right-hand side of the screen. */
struct Analyzer
{
/** Obligatory null destructor for a purely virtual base class. */
virtual ~Analyzer () = default;
/** The returned object, if not empty, will be stacked up at the right
* edge of the window. This is optional functionality for analyzers,
* so we provide a default implementation which adds no controls. */
virtual vector <Gtk::Widget *> make_control_widgets ()
{ return {}; }
/** Draw whatever into the \a chart_context. The tide_marks are the
* points in time at which tide-lines need adding, if they are to
* show on the \c hand_analysis_widget. */
virtual void graph_draw_hook
(Chart_Context &canvas,
Tide_Mark::List &notes,
unsigned number_shares,
vector <Tide_Mark::Price_Marker> const &) = 0;
/** An opportunity for the analyzer to increase the area in which
* time-series are plotted, if this is necessary to display all of
* the auxiliary data, for example. Most analyzers donʼt need this,
* so we provide a null default. */
virtual void stretch_outline (Time_Series::Range &) {}
/** Emitted internally to signal to the world that analytical results
* have changed (and probably need re-rendering on-screen). */
virtual sigc::signal<void> &signal_redraw_needed () = 0;
/** Allow an analyzer to respond to mouse presses on the chart canvas.
*
* As it stands, the system only allows for one of the analyzers to
* respond to this, and that privilege is currently taken by the \c
* Delta_Analyzer. */
virtual bool button_down (int const /*x*/, int const /*y*/) { return 0; }
/** Allow an analyzer to respond to mouse button releases. */
virtual bool button_up (int const /*x*/, int const /*y*/) { return 0; }
/** Allow an analyzer to respond to mouse drags on the chart canvas. */
virtual bool button_move (int const /*x*/, int const /*y*/) { return 0; }
}; /* End of class Analyzer. */
/** Whether there is one analyzer in the system or twenty, this class
* makes it appear to the rest of the application as if there were just
* one, thus simplifying matters. It is simply a wrapper around a
* container of analyzers. This also removes any dependencies of the
* application on the specifics of the analyzers keeping the
* abstraction purely abstract: the \c Analyzer_Stack constructor is a
* shrink-wrapped analyzer factory. */
struct Analyzer_Stack : Analyzer
{
/** The individual analyzers that we provide a home for. */
vector <unique_ptr <Analyzer>> analyzers;
/** If any analyzer indicates the need for a graphics re-draw, we will
* fire this signal which the application needs to listen out for. */
sigc::signal<void> redraw_needed_signal;
/** If not \c nullptr, this analyzer is working with the mouse. */
Analyzer *mouse_user {nullptr};
/** The class constructor is actually a comprehensive analyzer
* factory, and returns a fully populated object ready to run the
* full gamut of analysis at the userʼs behest. */
explicit Analyzer_Stack (Chart_Data &, Preferences&);
/** Provide a container-load of widgets which the various analyzers
* need to function. These are not at all life-time managed and the
* application must take responsibility to destroy the objects when
* necessary. The application is expected to allocate space
* horizontally at the right-hand edge of the data charts. */
vector <Gtk::Widget *> make_control_widgets () override
{
vector <Gtk::Widget*> ret;
for (auto &a : analyzers)
{
auto const w = a->make_control_widgets ();
ret.insert (end (ret), begin (w), end (w));
}
return ret;
}
/** Give every analyzer an opportunity to draw onto the chart
* canvas. */
void graph_draw_hook
(Chart_Context &c,
Tide_Mark::List &t,
unsigned number_shares,
vector <Tide_Mark::Price_Marker> const &p) override
{
for (auto &a : analyzers)
a->graph_draw_hook (c, t, number_shares, p);
}
/** Give every analyzer a go at stretching the size of the chart
* canvas. */
void stretch_outline (Time_Series::Range &r) override
{ for (auto &a : analyzers) a->stretch_outline (r); }
/** Return our re-draw signal. */
sigc::signal<void> &signal_redraw_needed () override
{ return redraw_needed_signal; }
/** Give each analyzer notification of a button press event until one
* responds and acts on the event. */
bool button_down (int const x, int const y) override
{
for (auto &a : analyzers)
if (a->button_down (x, y))
{
mouse_user = a.get ();
return 1;
}
return 0;
}
/** Give the active analyzer information about a mouse button-up
* event. */
bool button_up (int const x, int const y) override
{ return mouse_user && mouse_user->button_up (x, y); }
/** Give the active analyzer information about a mouse move
* event. */
bool button_move (int const x, int const y) override
{ return mouse_user && mouse_user->button_move (x, y); }
}; /* End of class Analyzer_Stack. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__ANALYZER__H. */

View file

@ -0,0 +1,205 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/application.h>
#include <dmbcs-market-data-api.h>
/** \file
*
* Implementation of the \c Application::ingest_new_market
* mega-method. */
namespace DMBCS::Trader_Desk {
void Application::ingest_new_market ()
{
try
{
DB db {user_prefs};
Markets markets {db};
try {
update_market_meta_data (markets, db);
}
catch (Market_Data_Api::Bad_Communication const &e)
{
Gtk::MessageDialog {*window, e.what (), 0, Gtk::MESSAGE_WARNING}
.run ();
return;
}
Gtk::Dialog dialog (pgettext ("Instruction", "Select market"),
*window,
Gtk::DIALOG_MODAL);
auto add_label = [&dialog] (string const &m)
{
Gtk::Label *const t = new Gtk::Label;
t->set_markup (m);
dialog . get_vbox () -> pack_start (*Gtk::manage (t),
Gtk::PACK_SHRINK);
};
add_label (pgettext ("Instruction", "Select market"));
if (find_if (begin (markets), end (markets),
[] (Markets::value_type const &m)
{ return ! m.second.tracked; })
== markets.end ())
{
add_label (string {"<span color=\"red\">"}
+ pgettext ("Join-A", "Sorry, there are no")
+ "</span>");
add_label (string {"<span color=\"red\">"}
+ pgettext ("Join-A", "new markets.")
+ "</span>");
dialog . add_button (pgettext ("Instruction", "Cancel"),
Gtk::RESPONSE_CANCEL);
dialog . set_size_request (300, 250);
dialog . show_all ();
dialog . run ();
}
else
{
Gtk::TreeModelColumn<string> name;
Gtk::TreeModelColumn<Market_Meta_Data const *> data;
Gtk::TreeModel::ColumnRecord columns;
columns.add (name);
columns.add (data);
auto list = Gtk::ListStore::create (columns);
for (auto const &u : markets)
if (! u.second.tracked)
{
auto b = list->append ();
(*b) [name] = u.second.world_data.name;
(*b) [data] = &u.second;
}
Gtk::TreeView view {list};
view . append_column (pgettext ("Label", "Market"), name);
Gtk::TreeModel::Path selected_row;
view . signal_row_activated ()
. connect ([&selected_row, &dialog]
(Gtk::TreeModel::Path const &path,
Gtk::TreeViewColumn *const)
{ selected_row = path;
dialog.response (Gtk::RESPONSE_OK); });
dialog . get_vbox () -> pack_start (view);
add_label (string {"<span color=\"red\" size=\"small\">"}
+ pgettext ("Instruction, Join-B",
"Double-click a market name")
+ "</span>");
add_label (string {"<span color=\"red\" size=\"small\">"}
+ pgettext ("Instruction, Join-B",
"to ingest its data")
+ "</span>");
dialog . add_button (pgettext ("Instruction", "Cancel"),
Gtk::RESPONSE_CANCEL);
dialog . set_size_request (300, 250);
dialog . show_all ();
if (Gtk::RESPONSE_OK == dialog . run ())
{
dialog.hide ();
auto market_data = *(*list->get_iter (selected_row)) [data];
start_tracking (markets, db, market_data.world_data.symbol);
/* Check that the seqid is not in market_grids. */
auto a = find_if (begin (market_grids),
end (market_grids),
[seqid = market_data.seqid]
(unique_ptr<Chart_Grid> const &g)
{ return g->market.seqid == seqid; });
if (a == end (market_grids))
{
/* !!!! How is this getting destroyed? */
auto *const g {new Chart_Grid {user_prefs, market_data}};
if (market_grids.empty ()) notebook.remove_page (1);
market_grids.emplace_back (g);
g->selection_signal
.connect ([this, g]
{ hand_analysis->subsume_selected (*g);
close_data_menu->set_sensitive (0);
last_data_menu->set_sensitive (1);
notebook.set_current_page (0); });
notebook.append_page (*g);
display_grid (market_grids.size () - 1);
notebook.show_all ();
/* Now fix up the menus. */
{
auto const n = market_grids.size () - 1;
ostringstream a; a << "display-grid-" << n;
actions->add (Gtk::Action::create
(a.str (), market_data.world_data.name),
[this, n] { display_grid (n); });
ui_manager->add_ui_from_string
("<ui>"
" <menubar name=\"menu\">"
" <menu action=\"display-menu\">"
" <menuitem action=\"" + a.str () + "\"/>"
" </menu>"
" </menubar>"
"</ui>");
}
/* !!!! Maybe think of re-setting the connection to
* the database here. */
update_closing_prices ();
}
}
}
}
catch (Market_Data_Api::No_Network &e)
{
Gtk::MessageDialog {*window, e.what (), 0, Gtk::MESSAGE_WARNING}
.run ();
}
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,232 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include "alpha-vantage--monitor.h"
#include "application.h"
#include "update-closing-prices.h"
#include <dmbcs-market-data-api.h>
/** \file
*
* Implementation of the \c Application::update_closing_prices
* mega-method. */
namespace DMBCS::Trader_Desk {
struct Progress_Dialog : Gtk::Dialog
{
Gtk::ProgressBar progress_bar;
Gtk::Label company_name;
explicit Progress_Dialog (Gtk::Window &parent);
};
Progress_Dialog::Progress_Dialog (Gtk::Window &parent)
: Gtk::Dialog (pgettext ("Window title", "Updating database"), parent, 1)
{
company_name.set_ellipsize (Pango::ELLIPSIZE_END);
company_name.set_alignment (Gtk::ALIGN_START);
get_vbox ()->pack_start
(*Gtk::make_managed<Gtk::Label>
(pgettext ("Information",
"trader-desk: Fetching data from Internet")));
get_vbox ()->pack_start (company_name, Gtk::PACK_EXPAND_WIDGET);
get_vbox ()->pack_start (progress_bar, Gtk::PACK_SHRINK);
add_button (Gtk::Stock::STOP, 0);
get_vbox ()->set_spacing (10);
set_default_size (400, 10);
show_all ();
}
extern "C" int queue_draw (Gtk::Widget *const w)
{ w->queue_draw (); return 0; }
struct no_connection_args
{ Gtk::Window* window; string message; };
extern "C" int run_no_internet_connection_message
(const no_connection_args *const A)
{
Gtk::MessageDialog {*A->window, A->message, 0, Gtk::MESSAGE_WARNING}
.run ();
delete A;
return 0;
}
extern "C" int delete_widget (Gtk::Widget *const w)
{ delete w; return 0; }
struct pulse_progress_bar_args
{ Progress_Dialog* dialog; double fraction; string company_name; };
extern "C" int pulse_progress_bar (const pulse_progress_bar_args *const A)
{
A -> dialog -> progress_bar . set_fraction (A->fraction);
A -> dialog -> company_name . set_text (A->company_name);
delete A;
return 0;
}
struct preferences_error_args { Application* app; string message; };
extern "C" int preferences_error_ (const preferences_error_args *const A)
{
A->app->preferences_error (A->message);
delete A;
return 0;
}
static void grid_injector (Chart_Grid& grid,
Chart** current_chart,
const Update_Closing_Prices::Data& data)
{
if (! *current_chart)
*current_chart = grid.find_chart (data.company_seqid);
if (*current_chart)
(*current_chart) -> data
. new_event
({chrono::system_clock::from_time_t
(t (data.year, data.month, data.day))
+ (*current_chart)->data.prices.market_close_time,
data.close},
Chart_Data::NO_SIGNAL);
}
void Application::update_closing_prices ()
{
const size_t a {(size_t) notebook.get_current_page ()};
if (a < 1 || a > market_grids.size ()) return;
Chart_Grid& grid {*market_grids [a - 1]};
try {
DB db {user_prefs};
grid.regenerate (db, user_prefs);
}
catch (const Market_Data_Api::Bad_Communication& e)
{
Gtk::MessageDialog {*window, e.what (), 0, Gtk::MESSAGE_WARNING}
.run ();
return;
}
for (auto& c : grid.chart) c->data.unaccurate = 1;
grid.queue_draw ();
std::thread
{[this, progress_dialog = new Progress_Dialog (*window), &grid]
{
sigc::connection call_id;
try
{
DB db {user_prefs};
Update_Closing_Prices::Work update {.market = grid.market};
call_id = progress_dialog->signal_response ()
.connect ([&update] (int)
{ update.stop = true; });
/** We donʼt want to hunt for the correct chart every time a
* new datum is reported to us, so only do the search when
* this is \c nullptr and re-use the last search result
* (stored here) otherwise. */
Chart* current_chart {nullptr};
do_update
(update,
db,
user_prefs,
/* Progress callback. */
[progress_dialog, &grid]
(const double& x,
const Update_Closing_Prices::Company& company)
{ gdk_threads_add_idle
((int(*)(void*)) pulse_progress_bar,
new pulse_progress_bar_args
{progress_dialog, x, company.name}); },
/* Datum injector. */
[&db, &grid, &current_chart]
(Update_Closing_Prices::Data const &data)
{ if (data.close == 0) return;
sql_injector (db, data);
grid_injector (grid, &current_chart, data); },
/* Company_done. */
[&grid, &current_chart] (int const &company_seqid)
{ if (! current_chart)
current_chart = grid.find_chart (company_seqid);
current_chart->data.extremes
= current_chart->data
.prices
.get_range (chrono::hours (50*24));
current_chart->data.unaccurate = 0;
gdk_threads_add_idle ((int(*)(void*))queue_draw,
current_chart);
current_chart = nullptr; });
}
catch (const Price_Server::Bad_API_Key& E)
{
gdk_threads_add_idle
((int(*)(void*)) preferences_error_,
new preferences_error_args {this, E.what ()});
}
catch (const Update_Closing_Prices::No_Connection& E)
{
gdk_threads_add_idle
((int(*)(void*)) run_no_internet_connection_message,
new no_connection_args {window, E.what ()});
}
call_id.disconnect ();
gdk_threads_add_idle ((int(*)(void*)) delete_widget,
progress_dialog);
}}
.detach ();
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2017 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/application.h>
namespace DMBCS::Trader_Desk {
void Application::display_grid (size_t const &g)
{
if (notebook.get_current_page () == 0)
{
hand_analysis->chart.data.update_extremes (Chart_Grid::DEFAULT_SPAN);
hand_analysis->chart.data.return_subsumed ();
}
notebook.set_current_page (g + 1);
close_data_menu->set_sensitive (1);
last_data_menu->set_sensitive (0);
}
} /* End of namespace DMBCS::Trader_Desk. */

119
trader-desk/application.h Normal file
View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2017 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__APPLICATION__H
#define DMBCS__TRADER_DESK__APPLICATION__H
#include <trader-desk/hand-analysis-widget.h>
#include <trader-desk/update-latest-prices.h>
/** \file
*
* Declaration of the \c Application class. */
namespace DMBCS::Trader_Desk {
/** The glue that binds everything together, really half the
* implementation of the top-level \c Window defined in
* trader-desk.cc.
*
* [This is still a work in progress. Ultimately we want this \c
* Application class to be independent of the (GTK) GUI front-end, so
* need to change the split in functionality between this class and the
* \c Window class accordingly.] */
struct Application
{
/* The following are established outside of this class, and it is
* expected that this be done as soon as the class is constructed. */
/** *The* set of user preferences. May change at infrequent but
* random times. */
Preferences user_prefs;
/** The most top-level window of this application. */
Gtk::Window *window {nullptr};
/** A function we can push to Gtk::Idle when we need to recourse to
* the preferences dialog due to a problem in the preferences. */
function<void (const string&)> preferences_error;
/** The part of the menu which is specific to the markets whose data
* we have loaded. */
Glib::RefPtr <Gtk::Action> last_data_menu;
Glib::RefPtr <Gtk::Action> close_data_menu;
/** All of the actions available to the menu. */
Glib::RefPtr <Gtk::ActionGroup> actions;
/** The menu manager. */
Glib::RefPtr <Gtk::UIManager> ui_manager;
/** The notebook which appears, without tabs, as the main widget in
* the \c window. */
Gtk::Notebook notebook;
/** All of the data, organized into a list of grids of charts. */
vector<unique_ptr<Chart_Grid>> market_grids;
/** The widget which shows detailed analysis of a particular companyʼs
* data, and allows for much interaction with the user. */
unique_ptr<Hand_Analysis_Widget> hand_analysis;
/** The application is not in a good state until the constructor has
* run, user_prefs have been updated with fixed-up values (by virtue
* of the wizard), and create_database() is run.
*
* !!!! This is far from ideal. */
explicit Application (Preferences&& P) : user_prefs {move (P)}
{}
/** This method is used to *change* the grid being displayed,
* according to their \a position amongst the notebook pages,
* i.e. counting from one upwards. */
void display_grid (size_t const &position);
/** Mega-method which does all the work (including operating the
* display machinery) to get a new market working in the system. */
void ingest_new_market ();
/** Mega-method which does all the work to bring a marketʼs data up to
* date. */
void update_closing_prices ();
} ; /* End of class Application. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__APPLICATION__H. */

View file

@ -0,0 +1,130 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/chart-context.h>
namespace DMBCS::Trader_Desk {
double Chart_Context::x (Time_Point const &t) const
{
return left_border
+ (width - left_border - right_border)
* (t - outline.start_time).count ()
/ (double) ((outline.end_time - outline.start_time).count ());
}
Time_Point Chart_Context::date (int const &x) const
{
return outline.start_time
+ chrono::duration_cast<chrono::seconds>
((x - left_border)
* (outline.end_time - outline.start_time)
/ (double) (width - left_border - right_border));
}
double Chart_Context::y (Currency_Value const &value) const
{
return top_border + (height - bottom_border - top_border)
* (1.0 - (value - outline.min_value)
/ (outline.max_value - outline.min_value));
}
Currency_Value Chart_Context::value (int const &y) const
{
return outline.max_value
- (outline.max_value - outline.min_value)
* (y - top_border)
/ (double) (height - top_border - bottom_border);
}
void Chart_Context::draw_time_series (Time_Series const &series,
Colour const &colour,
double const &alpha) const
{
if (series.size () < 2)
return;
set_source_rgb (colour, alpha);
auto i = begin (series);
move_to (*i++);
while (i != end (series) && i->time >= outline.start_time)
line_to (*i++);
cairo->stroke ();
}
void Chart_Context::add (Text &text,
string const &message,
Colour const &colour,
Text::Place const &position)
{
pango->set_text (message);
Pango::Rectangle const r {pango->get_pixel_logical_extents ()};
text += Text::Item {message,
colour,
position,
{(double) r.get_width (), (double) r.get_height ()}};
}
void Chart_Context::render (Text &text)
{
for (auto const &t : text.arrange ({left_border, width - right_border,
top_border, height - bottom_border}))
{
cairo->rectangle (t.placement.x, t.placement.y,
t.size.x, t.size.y);
set_source_rgb (Colour::MARK_LABEL_BACK, 0.5);
cairo->fill ();
pango->set_text (t.text);
cairo->move_to (t.placement.x, t.placement.y);
pango->add_to_cairo_context (cairo);
set_source_rgb (t.colour, 0.7);
cairo->fill ();
}
}
} /* End of namespace DMBCS::Trader_Desk. */

162
trader-desk/chart-context.h Normal file
View file

@ -0,0 +1,162 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__CHART_CONTEXT__H
#define DMBCS__TRADER_DESK__CHART_CONTEXT__H
#include <trader-desk/text.h>
#include <trader-desk/time-series.h>
#include <pangomm.h>
#include <cairomm/cairomm.h>
/** \file
*
* Declaration of the \c Chart_Context class. */
namespace DMBCS::Trader_Desk {
/** A wrapper around a Cairo context: a canvas on which to draw things.
* Methods are provided for drawing lines and filling areas represented
* relative to real times and prices, and for laying down and rendering
* small items of text.
*
* The class is really just an implementation detail of \c Chart
* (though references are passed out and used in several other
* classes), and is instantiated only transiently in that class; there
* is not a proper constructor but provision for the \c Chart class to
* get an uninitialized object and for that class to then perform full
* initialization. */
struct Chart_Context
{
/** The `canvas' on which the chart is painted. */
Cairo::RefPtr <Cairo::Context> cairo;
/** An object which helps with painting text onto the above canvas. */
Glib::RefPtr <Pango::Layout> pango;
/** The number of pixels between the left edge of the window and the
* side of the chart. */
double left_border;
/** The number of pixels between the right-hand side of the chart and
* the window edge. */
double right_border;
/** The number of pixels above the chart. */
double top_border;
/** The number of pixels below the chart. */
double bottom_border;
/** The width of the chart, in pixels. */
double width;
/** The height of the chart, in pixels. */
double height;
/** The (date, price) extremes of the chart. */
Time_Series::Range outline;
/** The collection of text strings which will be placed on the chart
* when it is rendered. */
Text text;
/** The application has a single (transient) instance of this class,
* created and initialized (constructed) within Chart::on_expose. We
* provide that method with the means to make an uninitialized
* object, and then prohibit any copying or moving of that object. */
Chart_Context () = default;
Chart_Context (Chart_Context const &) = delete;
Chart_Context &operator= (Chart_Context const &) = delete;
Chart_Context (Chart_Context &&) = delete;
Chart_Context &operator= (Chart_Context &&) = delete;
/** Plot the chart. */
void draw_time_series (Time_Series const &series,
Colour const &colour,
double const &alpha) const;
/** Move the `pen' to the position on the chart corresponding to the
* \a event. */
void move_to (Event const &event) const
{ cairo->move_to (x (event.time), y (event.price)); }
/** Draw a line on the chart to the position corresponding to \a
* event. */
void line_to (Event const &event) const
{ cairo->line_to (x (event.time), y (event.price)); }
/** Set the colour for the subsequent drawing operations. */
void set_source_rgb (Colour const &c, double const &alpha) const
{ cairo->set_source_rgba (c.red, c.green, c.blue, alpha); }
/** Ditto. */
void set_source_rgb (Colour const &c) const
{ cairo->set_source_rgb (c.red, c.green, c.blue); }
/* While this class is designed to present the application with an
* interface expressed entirely in terms of stock market \c Event
* points, when we are placing text onto the charts it is necessary
* to expose the raw coordinates, for reasons involving the
* displacement of the text position to allow for size and anchor
* orientation. */
Time_Point date (int const &x) const;
Currency_Value value (int const &y) const;
double x (Time_Point const &t) const;
double y (Currency_Value const &value) const;
Text::Place x (Event const &e) const
{ return {x (e.time), y (e.price)}; }
/** Add the \a message into the \a text object. */
void add (Text &text,
string const &message,
Colour const &,
Text::Place const &);
/** Render all the messages which have previously been added to the \a
* text object (these will be re-arranged so that there are no
* overlaps). */
void render (Text &);
}; /* End of class Chart_Context. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__CHART_CONTEXT__H. */

296
trader-desk/chart-data.cc Normal file
View file

@ -0,0 +1,296 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/chart-data.h>
#include <gtkmm.h>
namespace DMBCS::Trader_Desk {
constexpr const bool Chart_Data::NO_SIGNAL;
constexpr const Currency_Value Chart_Data::NO_POSITION;
void Chart_Data::timeseries__change_span (DB& db, const Duration& window)
try
{
const auto start {TODAY_MARK - window};
bool test;
{lock_guard l {prices_mutex};
test = prices.empty () ? 1 : start < prices.back ().time;
}
if (test)
{
reap_prefetch ();
if (last_fetch_time == Time_Point {} || start < last_fetch_time)
{
prices.extend_range (db, company_seqid, window);
last_fetch_time = start;
}
}
const Time_Series::Range hold {extremes};
update_extremes (window);
/* We are always in the GTK thread. */
if (hold != extremes) changed_signal.emit ();
}
catch (Mysql::DB_Connection::Exception&) {}
static void new_timeseries (Chart_Data *const CD,
DB& db,
const Duration& window,
const Duration& market_close_time)
{
CD->kill_prefetch ();
CD->reap_prefetch ();
const auto t {chrono::system_clock::now ()};
const auto immediate_window
{min (chrono::duration_cast<chrono::hours> (window),
chrono::hours {24*50})};
CD->prices
= Time_Series::from_database
(db, CD->company_seqid, t, immediate_window, market_close_time);
CD->last_fetch_time = t - immediate_window;
CD->update_extremes (window);
CD->changed_signal.emit ();
if (window != immediate_window)
CD->prefetch_ (db.current_preferences, {window});
}
void Chart_Data::new_company (DB& db,
const int company_seqid_,
const string& name,
const Duration& window,
const Duration& market_close_time)
{
reap_prefetch ();
return_subsumed ();
subsumed_object = nullptr;
company_seqid = company_seqid_;
company_name = name;
prices = Time_Series {market_close_time};
extremes = Time_Series::Range {};
extremes.start_time = chrono::system_clock::now () - window;
last_fetch_time = Time_Point {};
new_timeseries (this, db, window, market_close_time);
new_company_signal.emit ();
}
extern "C" int emit_changed_signal (gpointer signal)
{
((sigc::signal<void>*) signal)->emit ();
return 1;
}
static void do_prefetch
(DB& db, Chart_Data& CD, const vector<Duration> span)
{
CD.prefetch_thread_stop = false;
for (const Duration& s : span)
while (CD.last_fetch_time > TODAY_MARK - s)
{
const auto this_time
{max (CD.last_fetch_time - chrono::hours {24}*500,
TODAY_MARK - s)};
{lock_guard l {CD.prices_mutex};
CD.prefetch_series = new Time_Series {CD.prices};
}
CD.prefetch_series->extend_range
(db, CD.company_seqid, TODAY_MARK - this_time);
bool need_refresh {0};
{lock_guard l {CD.prices_mutex};
CD.prices = move (*CD.prefetch_series);
CD.prefetch_series = nullptr;
need_refresh = (CD.last_fetch_time > CD.extremes.start_time);
CD.last_fetch_time = this_time;
}
if (CD.prefetch_thread_stop) return;
if (need_refresh)
{
CD.update_extreme_prices ();
/* !!!! Unfortunate that this appears here--and the gtkmm
* header--(we have nothing to do with the graphics
* plane). Funny that it doesn't work at the point where
* we actually enter the graphics plane, but I guess
* other parts of the system react to this signal and
* they in turn will trigger the graphics plane. We are
* going to have to investigate all the points where
* signals are emitted! (Maybe it is just occurrences of
* this one particular signal?) */
/* ALWAYS outside the GTK thread. */
gdk_threads_add_idle (emit_changed_signal, &CD.changed_signal);
}
}
}
void Chart_Data::prefetch_ (Preferences& P,
const vector<Duration>& span)
{
if (! prefetch_thread)
prefetch_thread.reset (new thread ([this, span, &P]
{ DB db {P};
do_prefetch (db, *this, span); }));
}
void Chart_Data::reap_prefetch ()
{
if (! prefetch_thread) return;
prefetch_thread->join ();
prefetch_thread.reset ();
}
void Chart_Data::update_extreme_prices ()
{
lock_guard l {prices_mutex};
const auto extremes_
{prices.get_range (extremes.end_time - extremes.start_time)};
extremes.min_value = extremes_.min_value;
extremes.max_value = extremes_.max_value;
}
void Chart_Data::note_current_price (DB& db, Currency_Value const &value)
{
{lock_guard l {prices_mutex};
latest_price = {chrono::system_clock::now (), value};
/* Expensive! */
prices.insert (begin (prices), latest_price);
extremes.end_time = latest_price.time;
}
update_extreme_prices ();
db.quick ()
<< "update company set last_price=" << latest_price.price
<< ", last_price_date=from_unixtime("
<< number<chrono::seconds> (latest_price.time.time_since_epoch ())
<< ") where seqid=" << company_seqid;
/* Always in GTK thread. */
changed_signal.emit ();
}
void Chart_Data::subsume (Chart_Data *const c)
{
return_subsumed ();
subsumed_object = c;
{
lock_guard l {c->prices_mutex};
lock_guard m {prices_mutex};
extremes = c->extremes;
prices = c->prices;
last_fetch_time = c->last_fetch_time;
}
company_seqid = c->company_seqid;
company_name = c->company_name;
latest_price = c->latest_price;
changed_signal.emit ();
new_company_signal.emit ();
}
void Chart_Data::return_subsumed ()
{
if (! subsumed_object) return;
kill_prefetch ();
reap_prefetch ();
{
lock_guard l {subsumed_object->prices_mutex};
lock_guard m {prices_mutex};
subsumed_object->extremes = extremes;
subsumed_object->latest_price = latest_price;
subsumed_object->prices = prices;
subsumed_object->last_fetch_time = last_fetch_time;
}
subsumed_object = nullptr;
}
void Chart_Data::new_event (const Event& e, const bool& no_signal)
{
{
lock_guard l {prices_mutex};
prices.insert_event (latest_price = e);
}
if (e.time <= extremes.start_time) return;
extremes.end_time = max (extremes.end_time, e.time);
extremes.min_value = min (extremes.min_value, e.price);
extremes.max_value = max (extremes.max_value, e.price);
if (! no_signal) { unaccurate = 0;
changed_signal.emit (); }
}
} /* End of namespace DMBCS::Trader_Desk. */

230
trader-desk/chart-data.h Normal file
View file

@ -0,0 +1,230 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__CHART_DATA__H
#define DMBCS__TRADER_DESK__CHART_DATA__H
#include <trader-desk/time-series.h>
#include <sigc++/sigc++.h>
#include <mutex>
#include <thread>
namespace DMBCS::Trader_Desk {
/** This class looks after the data which form the graph across a
* chart. It has several features.
*
* 1) It holds more data than we need, on the off-chance it might be
* wanted later.
*
* 2) It actually can get as much data as are available in the database
* in a background thread, so that they are available without delay.
*
* 3) One \c Chart_Data object is able to take control (subsume) the
* data of another \c Chart_Data object. This is required as the
* usual object creation/passing paradigms do not work when objects
* are also widgets visible on-screen. */
struct Chart_Data
{
/** A placebo value to indicate that no actual position on the chart
* is used. */
static constexpr Currency_Value const NO_POSITION {-1.0};
/** The database's unique identifier for this company. */
int company_seqid;
/** A human-readable name string. */
string company_name;
/** Access to the prices needs this mutex. */
mutex prices_mutex;
/** The actual data we hold. There may be more here than we actually
* need at the present time, i.e. they may go further back in time
* than current analyses demand. */
Time_Series prices {chrono::seconds {0}};
/** The range of data we are interested in. Will be a subset of the
* range of \c prices. */
Time_Series::Range extremes;
/** A thread to background-fetch more data. */
unique_ptr<thread> prefetch_thread;
/** A holding place for the background thread to do its work. */
Time_Series *prefetch_series {nullptr};
/** When this goes \c TRUE, that is taken as a signal to a background
* thread to abandon its operations. */
bool prefetch_thread_stop {false};
/** If true we consider the entire time-series to be inaccurate, and
* show it pink. Usually used when we are in the process of updating
* the latest known prices. */
bool unaccurate {0};
/** We can't rely on the prices time-series to tell us the earliest
* datum requested from the database, because the database might not
* have gone back that far, and we don't want to keep requesting data
* that don't exist. Hence we keep this record here. */
Time_Point last_fetch_time;
/* !! managed entirely outside the class. */
/** The number of shares for which analytical data are computed and
* shown. */
unsigned number_shares {0};
/** The point in the data space at which a current position was
* opened. */
Event open_position {0, NO_POSITION};
/** If the user hand-enters a price point, we store that here. */
Event latest_price {0, NO_POSITION};
/** If we took the data from another object, we store the source here
* so that it can subsequently be returned. */
Chart_Data *subsumed_object {nullptr};
/** We emit this signal when any aspect of the data are changed. */
sigc::signal<void> changed_signal;
/** This signal is emitted after the data have been completely
* subsumed by those for another company. */
sigc::signal<void> new_company_signal;
/** If the null constructor is used, then a call of new_company is the
* only action that will make any sense; or we could subsume the data
* of another \c Chart_Data object. */
Chart_Data () = default;
/** The sole working constructor. Sets the object up ready for
* action, but does not actually load in any data at this point. */
Chart_Data (int const s, string const &n,
Duration const &market_close_time)
: company_seqid {s},
company_name {n},
prices {market_close_time}
{}
/** The destructor simply cleans up all of its resources. */
~Chart_Data ()
{
reap_prefetch ();
}
/** Put a flag up to instruct a running background data pre-fetch
* thread to abandon its work and stop. */
void kill_prefetch ()
{
prefetch_thread_stop = true;
}
/** Kick off a background thread to get data as far back in time as \a
* span. */
void prefetch_ (Preferences&, const vector<Duration>& span);
/** Wait if necessary for the background thread to finish, and then
* reap the new data into the existing \c prices time-series. */
void reap_prefetch ();
/** Take over the data contained in \a c. This is specifically for
* the case when \a c is a widget on the market thumbnail page, and
* we want to analyze the data in detail in the hand-analysis
* page. */
void subsume (Chart_Data *const c);
/** Give the subsumed data back to the original source, leaving us
* bereft until we subsume someone else's data. */
void return_subsumed ();
/** Assume that the range of prices inside the current range of dates
* has changed, and update the recorded upper and lower bounds. */
void update_extreme_prices ();
/** Re-compute the \c range for the given \a window back from the
* current time. */
void update_extremes (Duration const &window)
{
lock_guard<mutex> l {prices_mutex};
extremes = prices.get_range (window);
}
/** Add an event to the time-series corresponding to the \a value at
* the current time. This information is also stored on the company
* record in the database. */
void note_current_price (DB&, Currency_Value const &value);
/** Causes the span of our data to be changed in real time, i.e. not
* in the background thread. Usually, because of the work of the
* background thread, this function will not take very long to
* complete the operation. */
void timeseries__change_span (DB&, Duration const &window);
/** Re-initialize the class to hold the data of company with database
* identifier \a company_seqid. The data filling a period \a window
* to the start of the current day will be obtained as quickly as
* possible, but note that this might not happen as soon as the
* function returns; a large window will be broken up and the data
* progressively retrieved in a background thread. */
void new_company (DB&,
const int company_seqid,
const string& name,
const Duration& window,
const Duration& market_close_time);
/** Flag for the following method. */
static constexpr bool const NO_SIGNAL {1};
/** Insert the new datum \a e into the \c prices time-series, and
* update the extremes if the new point is more recent than the start
* of the current extremes (the usual case). A \c changed_signal
* will be emitted unless \c NO_SIGNAL is passed as the second
* argument. */
void new_event (Event const &e, bool const &no_signal = 0);
}; /* End of class Chart_Data. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__CHART_DATA__H. */

152
trader-desk/chart-grid.cc Normal file
View file

@ -0,0 +1,152 @@
/*
* Copyright (c) 2017 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/chart-grid.h>
/** \file
*
* Implementation of the \c Chart_Grid class. */
namespace DMBCS::Trader_Desk {
constexpr chrono::hours const Chart_Grid::DEFAULT_SPAN;
Chart_Grid::Chart_Grid (Preferences& P,
const Market_Meta_Data& m)
: user_prefs {P}, market {m}
{
add_events (Gdk::BUTTON_RELEASE_MASK);
add (table);
DB db {user_prefs};
regenerate (db, P, 1 /* force */);
}
Chart* Chart_Grid::find_chart (const int& company_seqid)
{
auto const c = find_if (begin (chart), end (chart),
[company_seqid] (unique_ptr<Chart> const &x)
{ return x->data.company_seqid
== company_seqid; });
return c != end (chart) ? c->get () : nullptr;
}
void Chart_Grid::regenerate (DB& db, Preferences& P, const bool force)
{
if (force || update_components (market,
db,
(Gtk::Window*) get_toplevel ()))
{
table.resize (1, 1);
chart.clear ();
auto sql = db.row_query ();
sql << " select seqid, rtrim(name) "
<< " from company "
<< " where market=" << market.seqid
<< " order by name asc";
sql.execute ();
int number_columns = int (ceil (sqrt (sql.number_rows ())));
chart.reserve (sql.number_rows ());
int row {0};
int column {0};
for (; sql; ++sql)
{
const auto seqid {sql.next_entry<int> ()};
chart.emplace_back (new Chart {Chart::Style::THUMB, P});
/* We fix up the zero duration when the widget is exposed, so
* that we donʼt delay getting the application started by
* pre-loading tons of data. */
chart.back ()->data.new_company (db,
seqid,
sql.next_entry<string> (),
chrono::hours {50*24},
market.world_data.close_time);
table.attach (*chart.back (), column, column + 1, row, row + 1);
if (0 == (column = (column+1) % number_columns)) ++row;
}
show_all ();
}
}
bool Chart_Grid::on_draw (const Cairo::RefPtr<Cairo::Context>& C)
{
unique_ptr<DB> db;
for (auto &c : chart)
{
if (c->data.extremes.start_time == c->data.extremes.end_time)
{
if (! db) db = make_unique<DB> (user_prefs);
c->data.timeseries__change_span (*db, DEFAULT_SPAN);
}
else
table.propagate_draw (*c, C);
}
return 1;
}
bool Chart_Grid::on_button_release_event (GdkEventButton *const event)
{
if (chart.empty ()) return 0;
const Gtk::Allocation alloc {chart.front ()->get_allocation ()};
guint rows, columns;
table.get_size (rows, columns);
const int index = int (event->x) / alloc.get_width ()
+ columns * (int (event->y) / alloc.get_height ());
if (index >= 0 && index < int (chart.size ()))
{
selection = chart [index].get ();
selection_signal.emit ();
}
return 1;
}
} /* End of namespace DMBCS::Trader_Desk. */

93
trader-desk/chart-grid.h Normal file
View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2017 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__CHART_GRID__H
#define DMBCS__TRADER_DESK__CHART_GRID__H
#include <trader-desk/chart.h>
#include <trader-desk/markets.h>
/** \file
*
* Declaration of the \c Chart_Grid class. */
namespace DMBCS::Trader_Desk {
/** Widget which manages a whole bunch of charts, for all companies in a
* market, and displays them all at once in a grid on the screen. */
struct Chart_Grid : Gtk::EventBox
{
Preferences& user_prefs;
/** The duration displayed in each thumbnail. */
static constexpr chrono::hours const DEFAULT_SPAN {50 * 24};
/** Object packed with nothing but \c Chart widgets. */
Gtk::Table table;
/** All the \c Chart's we are managing. */
vector <unique_ptr <Chart>> chart;
/** Pointer into the \c chart list: the one we last clicked on. */
Chart *selection {nullptr};
/** We emit this whenever the user clicks on a chart in the grid. */
sigc::signal <void> selection_signal;
/** The market we hold and display charts for. */
Market_Meta_Data market;
/** Sole constructor, gives us a fully operational object. */
Chart_Grid (Preferences&, const Market_Meta_Data&);
/** Find the chart corresponding to the company with the database \a
* seqid, or return \c nullptr. */
Chart *find_chart (int const &seqid);
/** Completely re-construct this object based on the data currently in
* the database. If \a force is TRUE, then this object will be
* constructed according to the information in the database; if \a
* force is FALSE then the database may be updated with new
* information about the market components, and if a change is made
* in these then this object will be refreshed. */
void regenerate (DB &, Preferences&, bool const force = 0);
/** Make sure all the individual charts have their data loaded before
* we attempt to render them on-screen. */
bool on_draw (const Cairo::RefPtr<Cairo::Context>&) override;
/** Called when the user selects an individual chart. We update our
* state and emit the \c selection_signal. */
bool on_button_release_event (GdkEventButton *const) override;
}; /* End of class Chart_Grid. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__CHART_GRID__H. */

483
trader-desk/chart.cc Normal file
View file

@ -0,0 +1,483 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/chart.h>
#include <iomanip>
#include <set>
/** \file
*
* Implementation of the \c Chart class. */
namespace DMBCS::Trader_Desk {
Chart::Chart (uint32_t const features_, Preferences& P)
: features (features_)
{
data . changed_signal . connect ([this] { queue_draw (); });
/* Big enough for at least a thumb; we will take up more space if we're
* offered it. */
set_size_request (40, 25);
if (features & Feature::CROSS_HAIRS)
add_events (Gdk::POINTER_MOTION_MASK | Gdk::LEAVE_NOTIFY_MASK
| Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK);
if (features & Feature::ANALYZERS)
analyzer = make_unique<Analyzer_Stack> (data, P);
}
bool Chart::on_motion_notify_event (GdkEventMotion *const motion)
{
if (features & Feature::CROSS_HAIRS)
{
if (analyzer)
analyzer->button_move (motion->x, motion->y);
pointer_x = (int) motion->x;
pointer_y = (int) motion->y;
queue_draw ();
}
return 1;
}
bool Chart::on_leave_notify_event (GdkEventCrossing *const)
{
pointer_x = pointer_y = -1;
queue_draw ();
return 1;
}
bool Chart::on_button_press_event (GdkEventButton *const event)
{
return features & Feature::CROSS_HAIRS
&& analyzer
&& analyzer->button_down (event->x, event->y);
}
bool Chart::on_button_release_event (GdkEventButton *const event)
{
return features & Feature::CROSS_HAIRS
&& analyzer
&& analyzer->button_up (event->x, event->y);
}
/* !! We really want to paint most of the chart into memory, and map it
* to the screen as required. But we need to re-assess all the
* tide-marks on the cursor line whenever the cursor moves. */
bool Chart::on_draw (const Cairo::RefPtr<Cairo::Context>& cairo)
{
/* This object is constructed as we draw the various aspects of the
* chart, and then it is rendered on top of everything else right at
* the end of this method. */
Tide_Mark::List tide_marks;
/*************** Set up the canvas. **************************/
Chart_Context canvas;
{
const Gtk::Allocation allocation {get_allocation ()};
canvas.width = allocation.get_width ();
canvas.height = allocation.get_height ();
}
canvas.left_border = features & (int) Feature::AXIS_LABELS ? 45 : 4;
canvas.bottom_border = features & (int) Feature::AXIS_LABELS ? 40 : 4;
canvas.top_border = 4;
canvas.right_border = 4;
canvas.cairo = cairo;
canvas.pango = Pango::Layout::create (canvas.cairo);
canvas.pango -> set_font_description (Pango::FontDescription ("Sans 7"));
canvas.set_source_rgb (data.unaccurate ? Colour::NO_DATA_REGION
: Colour::CHART_BACKGROUND);
canvas.cairo -> paint ();
canvas.outline = data.extremes;
double const space = 0.05 * (canvas.outline.max_value
- canvas.outline.min_value);
canvas.outline.min_value -= space;
canvas.outline.max_value += space;
if (analyzer)
analyzer->stretch_outline (canvas.outline);
auto cursor_mark
= Tide_Mark::price_marker
(canvas.date (pointer_x),
canvas.outline.contains ({ canvas.date (pointer_x),
canvas.value (pointer_y)})
? Colour::CURSOR_TIDES
: Colour::NO_DISPLAY);
auto current_mark = Tide_Mark::price_marker (canvas.outline.end_time,
Colour::CHART_BACKGROUND);
canvas.cairo->set_line_width (1.0);
/************************ No-data region. *******************************/
{
auto const r = data.prices.empty () ? canvas.outline.end_time
: data.prices.back ().time;
if (r > canvas.outline.start_time)
{
canvas.set_source_rgb (Colour::NO_DATA_REGION);
canvas.move_to ({canvas.outline.start_time,
canvas.outline.min_value});
canvas.line_to ({canvas.outline.start_time,
canvas.outline.max_value});
canvas.line_to ({r, canvas.outline.max_value});
canvas.line_to ({r, canvas.outline.min_value});
canvas.cairo->fill ();
}
}
/*********** Let the analyzers draw themselves. *************/
if (analyzer)
analyzer->graph_draw_hook (canvas,
tide_marks,
data.number_shares,
{cursor_mark, current_mark});
/******** X-axis *******/
if (features & Feature::AXIS_LABELS)
{
canvas.set_source_rgb (Colour::TIME_AXIS);
char buffer [200];
auto const *disc = Date_Axis::discretization;
time_t const t = chrono::system_clock::to_time_t
(data.extremes.end_time);
for (; disc->format; ++disc)
{
/* If the spacing between ticks is less than one pixel,
* move on. */
if (disc->real_interval.count ()
/ double ((data.extremes.end_time
- data.extremes.start_time).count ())
* (canvas.width - canvas.left_border
- canvas.right_border)
< 1.0)
continue;
/* Find the box needed to enclose a label showing the
* current time. */
strftime (buffer, sizeof (buffer),
disc->format,
localtime (&t));
canvas.pango->set_text (buffer);
Pango::Rectangle const rect
= canvas.pango->get_pixel_logical_extents ();
/* If the spacing between ticks is more than the space
* required to display the current date-time, then this is
* the tick spacing we will use. */
if (disc->real_interval.count ()
/ double ((data.extremes.end_time
- data.extremes.start_time).count ())
* (canvas.width - canvas.left_border
- canvas.right_border)
> rect.get_width () + 4)
break;
}
auto show_ticks = [&canvas, this]
(Date_Axis::Discretization const &disc,
uint32_t const &line_offset)
{
char buffer [200];
for (auto i = data.extremes.start_time;
i <= data.extremes.end_time;
i += disc.interval)
{
i = disc.round_down (i);
if (i > data.extremes.start_time)
{
auto i_ = chrono::system_clock::to_time_t (i);
strftime (buffer, sizeof (buffer),
disc.format,
localtime (&i_));
canvas.pango->set_text (buffer);
canvas.move_to ({i, canvas.outline.min_value});
canvas.cairo->rel_move_to (0, line_offset + 1);
canvas.pango->add_to_cairo_context (canvas.cairo);
canvas.cairo->fill ();
}
}
};
if (disc->format)
{
show_ticks (*disc, 0);
if ((disc + 1)->format)
{
show_ticks (*(disc + 1), 11);
if ((disc + 2)->format)
show_ticks (*(disc + 2), 22);
}
}
}
/******** Y-axis ********/
canvas.pango
->set_markup (string {"<span size=\"large\">"}
+ pgettext ("Label", "Position value (pounds)")
+ "</span>");
const Pango::Rectangle extents
= canvas.pango->get_pixel_logical_extents ();
const int font_height = extents.get_height ();
if (features & (int) Feature::AXIS_LABELS)
{
canvas.cairo->save ();
canvas.cairo->rotate (- M_PI / 2.0);
canvas.cairo->move_to (- (canvas.height - extents.get_width ())
/ 2,
1);
canvas.pango->add_to_cairo_context (canvas.cairo);
canvas.set_source_rgb (Colour::PRICE_AXIS);
canvas.cairo->fill ();
canvas.cairo->restore ();
}
if (features & (int) Feature::AXIS_LABELS)
{
double const share_scale = data.number_shares / 100.0;
double inc = 0.01;
for (inc = 0.01;
(inc / share_scale) / (canvas.outline.max_value
- canvas.outline.min_value)
* (canvas.height - canvas.bottom_border
- canvas.top_border)
< font_height * 1.0;
inc *= 10.0) ;
for (double b = inc * (floor (share_scale
* canvas.outline.min_value
/ inc)
+ 1);
b < share_scale * canvas.outline.max_value;
b += inc)
if (b / share_scale > canvas.outline.min_value)
{
ostringstream out;
out << setw (5) << b;
canvas.pango->set_text (out.str ());
Pango::Rectangle const extents
= canvas.pango->get_pixel_logical_extents ();
canvas.move_to ({data.extremes.start_time,
b / share_scale});
canvas.cairo->rel_move_to (- extents.get_width () - 2,
- extents.get_height () / 2);
canvas.pango->add_to_cairo_context (canvas.cairo);
canvas.set_source_rgb (Colour::PRICE_AXIS);
canvas.cairo->fill ();
}
}
if (data.prices.empty ())
{
if (features & Feature::COMPANY_NAME)
canvas.add (canvas.text,
data.company_name,
Colour::COMPANY_NAME_TITLE,
{ canvas.left_border, canvas.top_border });
canvas.render (canvas.text);
return 1;
}
canvas.set_source_rgb (Colour::PRICE_AXIS);
canvas.move_to ({data.extremes.start_time, canvas.outline.max_value});
canvas.line_to ({data.extremes.start_time, canvas.outline.min_value});
canvas.cairo->stroke ();
canvas.set_source_rgb (Colour::TIME_AXIS);
canvas.move_to ({data.extremes.start_time, canvas.outline.min_value});
canvas.line_to ({data.extremes.end_time, canvas.outline.min_value});
canvas.cairo->stroke ();
{
lock_guard<mutex> l {data.prices_mutex};
canvas.draw_time_series (data.prices, Colour::PRICE_GRAPH, 1.0);
tide_marks.emplace_back (current_mark (data.prices.front ().price,
Colour::PRICE_TIDES));
tide_marks.emplace_back (cursor_mark (data.prices.interpolated_value
(canvas.date (pointer_x)),
Colour::PRICE_TIDES));
}
if (features & Feature::CROSS_HAIRS)
tide_marks.emplace_back (cursor_mark (canvas.value (pointer_y),
Colour::CURSOR_TIDES));
/**** Company name ****/
if (features & Feature::COMPANY_NAME)
canvas.add (canvas.text,
data.company_name,
Colour::COMPANY_NAME_TITLE,
canvas.x ({data.extremes.start_time,
canvas.outline.max_value}));
/***** Tide marks. *****/
if (features & Feature::TIDE_MARKS)
{
set <Time_Point> dates_shown;
for (auto const &tide : tide_marks)
if (tide.temporal_colour != Colour::NO_DISPLAY
&& tide.time >= canvas.outline.start_time
&& tide.time <= canvas.outline.end_time)
{
canvas.cairo
->set_source_rgb
(1.0 - 0.5 * (1.0 - tide.value_colour.red),
1.0 - 0.5 * (1.0 - tide.value_colour.green),
1.0 - 0.5 * (1.0 - tide.value_colour.blue));
canvas.move_to ({canvas.outline.start_time, tide.price});
canvas.line_to ({canvas.outline.end_time, tide.price});
canvas.cairo->stroke ();
ostringstream hold;
hold << tide.price;
canvas.add (canvas.text,
hold.str (),
tide.value_colour,
{canvas.x (tide.time) + 20,
canvas.y (tide.price) - 13});
ostringstream hold_2;
hold_2 << data.number_shares * tide.price / 100.0;
canvas.add (canvas.text,
hold_2.str (),
tide.value_colour,
{canvas.x (tide.time) - 60,
canvas.y (tide.price) - 13});
if (dates_shown.insert (tide.time).second)
{
canvas.set_source_rgb (tide.temporal_colour);
canvas.move_to ({tide.time, canvas.outline.max_value});
canvas.line_to ({tide.time, canvas.outline.min_value});
canvas.cairo->stroke ();
struct tm tm;
time_t t = chrono::system_clock::to_time_t (tide.time);
localtime_r (&t, &tm);
char buffer [200];
strftime (buffer, sizeof (buffer), "%Y-%m-%d %H:%M", &tm);
canvas.add (canvas.text,
buffer,
Colour::CURSOR_TIDES,
{canvas.x (tide.time) + 2,
canvas.height - canvas.bottom_border
- 12});
}
}
}
canvas.render (canvas.text);
return 1;
} /* End of on_draw method. */
} /* End of namespace DMBCS::Trader_Desk. */

147
trader-desk/chart.h Normal file
View file

@ -0,0 +1,147 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__CHART__H
#define DMBCS__TRADER_DESK__CHART__H
#include <functional>
#include <trader-desk/analyzer.h>
#include <trader-desk/date-axis.h>
/** \file
*
* Declaration of the \c Chart class. */
namespace DMBCS::Trader_Desk {
/** This is a GTK widget which displays a chart complete with all its
* trimmings. Most of the bulk of the implementation deals with the
* gooey details of actually rendering all the parts into the part of
* the screen occupied by the widget, but there is also some code to
* support a stack of analyzers, which will pass along mouse events to
* them if appropriate. */
struct Chart : Gtk::DrawingArea
{
private:
/** Set of selectable trimmings which may adorn the chart. */
struct Feature
{
/** The company name near the top-left corner. */
uint32_t static constexpr const COMPANY_NAME = 1 << 0;
/** Titles and tick indicators outside the chart itself. */
uint32_t static constexpr const AXIS_LABELS = 1 << 1;
/** Live cursors which follow the mouse around and display numerical
* data at all points where the cross-hairs intersect an
* interesting point. */
uint32_t static constexpr const CROSS_HAIRS = 1 << 2;
/** Lines and numerical data highlighting interesting parts of a
* chart. */
uint32_t static constexpr const TIDE_MARKS = 1 << 3;
/** Display output of full analysis stack. */
uint32_t static constexpr const ANALYZERS = 1 << 4;
};
public:
/** Combinations of the above features which are enabled for each
* personality in which charts appear. */
struct Style
{
/** The appearance of charts on the market-wide thumbnail
* display. */
uint32_t static constexpr const THUMB = Feature::COMPANY_NAME;
/** The appearance of the chart shown in the detailed hand
* analysis widget. */
uint32_t static constexpr const HAND_ANALYSIS = Feature::AXIS_LABELS
+ Feature::CROSS_HAIRS
+ Feature::TIDE_MARKS
+ Feature::ANALYZERS;
};
/** The actual data we are presenting; our actions are driven to a
* large extent from the data_changed signal emitted by this
* object. */
Chart_Data data;
/** An object which will embellish our chart with extra analytical
* tools and features. */
unique_ptr<Analyzer_Stack> analyzer;
private:
/** The features we want to embellish our display with: bit-field of \c
* Feature's via \c Style. */
uint32_t const features;
/** The last known coordinates of the mouse cursor, when it was over
* our chart; used to convey mouse place into expose() method. */
int pointer_x, pointer_y;
public:
/** Sole constructor which partially initializes an object (note in
* particular that no company is specified here). */
Chart (uint32_t const features_, Preferences&);
/** A \c Chart is a very heavy object and we do not want to be copying
* or even moving these around. */
Chart (Chart const &) = delete;
Chart (Chart&&) = delete;
void operator= (Chart const &) = delete;
void operator= (Chart &&) = delete;
private:
/** Run all of the analyzers, and then render them, the chart, and all
* of its selected trimmings. */
bool on_draw (const Cairo::RefPtr<Cairo::Context>&) override;
/** If cross-hairs are live, send the button event to the analyzer
* stack. */
bool on_button_press_event (GdkEventButton *const) override;
/** If cross-hairs are live, send the button event to the analyzer
* stack. */
bool on_button_release_event (GdkEventButton *const) override;
/** If cross-hairs are live, update all of them and get the chart
* re-drawn. Finally pass the motion event to the analyzers. */
bool on_motion_notify_event (GdkEventMotion *const motion) override;
/** If cross-hairs are live, remove them by re-drawing the chart
* without. */
bool on_leave_notify_event (GdkEventCrossing *const) override;
}; /* End of class Chart. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__CHART__H. */

49
trader-desk/colour.cc Normal file
View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/colour.h>
namespace DMBCS::Trader_Desk {
const Colour Colour::COMPANY_NAME_TITLE {0.0, 0.0, 0.0};
const Colour Colour::CURSOR_TIDES {0.6, 0.6, 0.6};
const Colour Colour::PRICE_TIDES {0.4, 0.4, 1.0};
const Colour Colour::PROFIT_LINE {0.4, 0.4, 0.4};
const Colour Colour::POSITION_TIDES {0.0, 0.8, 0.0};
const Colour Colour::CHART_BACKGROUND {1.0, 1.0, 1.0};
const Colour Colour::MEAN_TIDE {1.0, 0.5, 0.5};
const Colour Colour::MEAN_GRAPH {1.0, 0.0, 0.0};
const Colour Colour::SD_ENVELOPE {1.0, 1.0, 0.5};
const Colour Colour::ENVELOPE_TIDES {0.7, 0.7, 0.0};
const Colour Colour::PRICE_GRAPH {0.2, 0.2, 0.2};
const Colour Colour::TIME_AXIS {0.0, 0.0, 1.0};
const Colour Colour::PRICE_AXIS {0.0, 0.9, 0.0};
const Colour Colour::MARK_LABEL_BACK {1.0, 1.0, 1.0};
const Colour Colour::NO_DATA_REGION {1.0, 0.8, 0.8};
const Colour Colour::POSITIVE_DELTA {0.5, 1.0, 0.5};
const Colour Colour::NEGATIVE_DELTA {1.0, 0.5, 0.5};
const Colour Colour::DELTA_VALUE {0.0, 0.0, 0.0};
const Colour Colour::NO_DISPLAY {-1.0, -1.0, -1.0};
} /* End of namespace DMBCS::Trader_Desk. */

98
trader-desk/colour.h Normal file
View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__COLOUR__H
#define DMBCS__TRADER_DESK__COLOUR__H
#include <cmath>
/** \file
*
* Declaration of the \c Colour class. */
namespace DMBCS::Trader_Desk {
using namespace std;
/** A characteristic given to tide lines and text labels. */
struct Colour
{
/** Component of colour, each in the range [0.0, 1.0]. */
float red, green, blue;
private:
/** Sole constructor, provide fully specified object. */
constexpr Colour (float const &r, float const &g, float const &b)
: red {r}, green {g}, blue {b}
{}
public:
static const Colour CURSOR_TIDES;
static const Colour COMPANY_NAME_TITLE;
static const Colour PRICE_TIDES;
static const Colour PROFIT_LINE;
static const Colour POSITION_TIDES;
static const Colour CHART_BACKGROUND;
static const Colour MEAN_TIDE;
static const Colour MEAN_GRAPH;
static const Colour SD_ENVELOPE;
static const Colour ENVELOPE_TIDES;
static const Colour PRICE_GRAPH;
static const Colour TIME_AXIS;
static const Colour PRICE_AXIS;
static const Colour MARK_LABEL_BACK;
static const Colour NO_DATA_REGION;
static const Colour POSITIVE_DELTA;
static const Colour NEGATIVE_DELTA;
static const Colour DELTA_VALUE;
/** Extra-special value which indicates that this feature should not
* be drawn at all. */
static const Colour NO_DISPLAY;
} ; /* End of class Colour. */
/** Two colours compare equal if they look the same to a human observer.
* Note that comparing anything with \c NO_DISPLAY (apart from \c
* NO_DISPLAY itself) will fail. */
inline constexpr bool operator== (const Colour &a, const Colour &b)
{
return abs (a.red - b.red) + abs (a.green - b.green)
+ abs (a.blue - b.blue)
< 0.01;
}
/** The exact inverse of the above function. */
inline constexpr bool operator!= (const Colour &a, const Colour &b)
{ return ! (a == b); }
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__COLOUR__H. */

View file

@ -0,0 +1,140 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/company-name-entry.h>
namespace DMBCS::Trader_Desk {
Company_Name_Entry::Company_Name_Entry (Chart_Data &cd)
: chart_data (cd),
previous_icon {Gtk::Stock::GO_BACK, Gtk::ICON_SIZE_SMALL_TOOLBAR},
next_icon {Gtk::Stock::GO_FORWARD, Gtk::ICON_SIZE_SMALL_TOOLBAR},
label {pgettext ("Label", "Company name: ")}
{
Gtk::TreeModel::ColumnRecord column_record;
column_record.add (name);
column_record.add (seqid);
tree_model = Gtk::ListStore::create (column_record);
auto completion = Gtk::EntryCompletion::create ();
completion->set_model (tree_model);
completion->set_text_column (name);
entry.set_completion (completion);
entry.set_width_chars (30);
previous_company_button . add (previous_icon);
next_company_button . add (next_icon);
sub_box.pack_start (previous_company_button, Gtk::PACK_SHRINK);
sub_box.pack_start (next_company_button, Gtk::PACK_SHRINK);
pack_start (sub_box, Gtk::PACK_SHRINK, 20);
pack_start (label, Gtk::PACK_SHRINK, 0);
pack_start (entry, Gtk::PACK_SHRINK, 0);
previous_company_button.signal_clicked ()
.connect ([this] { previous_company_required (); });
next_company_button.signal_clicked ()
.connect ([this] { next_company_required (); });
chart_data . changed_signal
. connect ([this]
{ if (chart_data.company_name != entry.get_text ())
entry.set_text (chart_data.company_name); });
/* When the user types/selects a company name, then presses enter... */
entry . signal_activate () . connect ([this] { do_name_select (); } );
}
void Company_Name_Entry::read_names (DB& db,
size_t const &market_id,
size_t const &company_id)
{
tree_model->clear ();
auto sql {db.row_query ()};
sql << " select rtrim(name), seqid "
<< " from company "
<< " where market=" << market_id
<< " order by name asc";
cursor = begin (tree_model->children ());
for (sql.execute (); sql; ++sql)
{
auto row = tree_model->append ();
(*row) [name] = sql.next_entry<string> ();
(*row) [seqid] = sql.next_entry<int> ();
if ((*row) [seqid] == company_id)
cursor = row;
}
}
void Company_Name_Entry::do_name_select ()
{
cursor = find_if (begin (tree_model->children ()),
end (tree_model->children ()),
[this, text=entry.get_text ()]
(Gtk::TreeRow const &x)
{ return x [name] == text; });
name_change . emit ((*cursor) [seqid]);
}
void Company_Name_Entry::next_company_required ()
{
if (cursor != tree_model->children ().end ())
++cursor;
if (cursor == tree_model->children ().end ())
cursor = begin (tree_model->children ());
entry.set_text ((*cursor) [name]);
name_change.emit ((*cursor) [seqid]);
}
void Company_Name_Entry::previous_company_required ()
{
if (cursor == begin (tree_model->children ()))
cursor = end (tree_model->children ());
entry.set_text ((*--cursor) [name]);
name_change.emit ((*cursor) [seqid]);
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__COMPANY_NAME_ENTRY__H
#define DMBCS__TRADER_DESK__COMPANY_NAME_ENTRY__H
#include <trader-desk/chart-grid.h>
#include <gtkmm.h>
/** \file
*
* Declaration of the \c Company_Name_Entry class. */
namespace DMBCS::Trader_Desk {
/** Drop-down box allowing display and selection of names of companies
* in a given market, plus a couple of little arrows to move backwards
* and forwards through the list. */
struct Company_Name_Entry : Gtk::HBox
{
/** The object whose company data we are interested in. */
Chart_Data &chart_data;
/** The actual on-screen widget. */
Gtk::Entry entry;
/** A place to pack the forwards/backwards arrows together, for
* aesthetic reasons. */
Gtk::HBox sub_box;
/** The image on the back button. */
Gtk::Image previous_icon;
/** The back button itself. */
Gtk::Button previous_company_button;
/** The image on the forward button. */
Gtk::Image next_icon;
/** The forward button itself. */
Gtk::Button next_company_button;
/** Static label in front of the selection box. */
Gtk::Label label;
/** Human-readable company names, part of \c tree_model. */
Gtk::TreeModelColumn <Glib::ustring> name;
/** The database sequence ID of the companies, in correspondence with
* the \c names above, part of \c tree_model. */
Gtk::TreeModelColumn <unsigned> seqid;
/** The tree model holds a list of all company names and seqids, and
* is used for two purposes: to provide completions to the entry
* input box, and to provide us with an STL-like container of names,
* e.g. for iterating through. */
Glib::RefPtr<Gtk::ListStore> tree_model;
/** Pointer to the currently selected (active) item in the \c
* tree_model. */
Gtk::TreeModel::iterator cursor;
/** We emit this signal when the name is changed (selected) by the
* user. */
sigc::signal <void, unsigned> name_change;
/** Sole constructor which composes the entire composite widget and
* has everything ready for operation. */
explicit Company_Name_Entry (Chart_Data &);
/* The various copy and move operations are deleted by default since
* the base class doesn't support them. */
/** Refresh the list of company names available to scroll through, and
* in the drop-down box. */
void read_names (DB&, size_t const &market_id, size_t const &company_id);
/** The user has clicked the next button. */
void next_company_required ();
/** The user has clicked the previous button. */
void previous_company_required ();
/** The user has picked a new company name in the entry widget. */
void do_name_select ();
}; /* End of class Company_Name_Entry. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__COMPANY_NAME_ENTRY__H. */

81
trader-desk/date-axis.cc Normal file
View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/date-axis.h>
namespace DMBCS::Trader_Desk { namespace Date_Axis {
inline constexpr time_t unix (Duration const &d)
{
return number<chrono::seconds> (d);
}
inline tm *squash (tm *const time, int count)
{
if (count-- > 0) time->tm_sec = 0;
if (count-- > 0) time->tm_min = 0;
if (count-- > 0) time->tm_hour = 0;
if (count-- > 0) time->tm_mday = 1;
if (count-- > 0) time->tm_mon = 0;
return time;
}
inline Duration round_count (Duration const &t, int const &count)
{
time_t t_ = unix (t);
return chrono::seconds (mktime (squash (localtime (&t_), count)));
}
Duration round_year (Duration t) { return round_count (t, 5); }
Duration round_month (Duration t) { return round_count (t, 4); }
Duration round_week (Duration t)
{
time_t t_ = unix (t);
tm date = *localtime (&t_);
squash (&date, 3);
mktime (&date);
/* Go back to the previous Monday (tm_mday = 1). */
if (date.tm_wday == 0)
date.tm_mday -= 6;
else
date.tm_mday -= date.tm_wday - 1;
return chrono::seconds (mktime (&date));
}
Duration round_day (Duration t) { return round_count (t, 3); }
Duration round_hour (Duration t) { return round_count (t, 2); }
Duration round_minute (Duration t) { return round_count (t, 1); }
Duration round_second (Duration t) { return t; }
} } /* End of namespace DMBCS::Trader_Desk::Date_Axis. */

133
trader-desk/date-axis.h Normal file
View file

@ -0,0 +1,133 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__DATE_AXIS__H
#define DMBCS__TRADER_DESK__DATE_AXIS__H
#include <gtkmm.h>
#include <trader-desk/time-series.h>
namespace DMBCS::Trader_Desk {
/** Really part of the implementation of \c Chart to help with drawing
* the date axis along the bottom edge. The class provides information
* about the possible spacings between tick marks, and methods which
* round any date down to the nearest tick. */
namespace Date_Axis
{
/** A \c Discretization represents a level of detail to display on a
* time axis. The core concept is the real_interval between tick
* marks, but this is not always consistent (lengths of months, leap
* years), so we consider an increased \c interval which is bigger
* than any interval which might occur and smaller than any double
* interval, and then we use the system's calendar functions to \c
* round_down to the last appropriate real date. */
struct Discretization
{
/** Slightly bigger than the largest real interval between ticks
* (remember that months and years vary), but definitely smaller
* than two ticks. */
Duration interval;
/** The real interval between ticks. */
Duration real_interval;
/** A \c strftime format which provides appropriate labels on the
* ticks. A \c nullptr here represents a sentinel place-holder for
* the end of a list of discretizations. */
char const *const format;
/** Function which will round the \a Duration down to the nearest
* tick. */
Duration (*round_down_) (Duration);
/** Convenience wrapper around above function. */
Duration round_down (Duration const &d) { return (*round_down_) (d); }
/** As above, but round the \c Time_Point \a t down to the nearest
* tick. */
Time_Point round_down (Time_Point t) const
{
return chrono::system_clock::from_time_t (0)
+ (*round_down_) (t.time_since_epoch ());
}
};
/* The specialized rounding functions. Since we use the C library
* calendar functions, we always work in terms of duration (as
* seconds) since the Unix epoch. */
Duration round_year (Duration t);
Duration round_month (Duration t);
Duration round_week (Duration t);
Duration round_day (Duration t);
Duration round_hour (Duration t);
Duration round_minute (Duration t);
Duration round_second (Duration t);
namespace C = chrono;
/* Null-terminated array of discretization levels which we use, in
* increasing granularity. */
static constexpr const Discretization discretization []
= { { C::seconds (1),
C::seconds (1),
"%H:%M:%S", &round_second },
{ C::seconds (70),
C::seconds (60),
"%H:%M", &round_minute },
{ C::seconds (6 * 60),
C::seconds (5 * 60),
"%H:%M", &round_minute },
{ C::seconds (70 * 60),
C::seconds (60 * 60),
"%H:00", &round_hour },
{ C::seconds (25 * 60 * 60),
C::seconds (24 * 60 * 60),
"%d", &round_day },
{ C::seconds (8 * 24 * 60 * 60),
C::seconds (7 * 24 * 60 * 60),
"%d", &round_week },
{ C::seconds (35 * 24 * 60 * 60),
C::seconds (31 * 24 * 60 * 60),
"%b", &round_month },
{ C::seconds (380 * 24 * 60 * 60),
C::seconds (365 * 24 * 60 * 60),
"%Y", &round_year },
{ C::seconds (0), C::seconds (0), nullptr, nullptr } };
} /* End of namespace Date_Axis. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__DATE_AXIS__H. */

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/date-range-scale.h>
namespace DMBCS::Trader_Desk {
Date_Range_Scale::Date_Range_Scale (Chart_Data &d, Preferences& P)
: Exponential_Scale (d, pgettext ("Label", "Date range = %.0f days"),
Gdk::RGBA {"#0000ff"},
Gdk::RGBA {"#aaaaff"},
Gdk::RGBA {"#6666ff"},
1, 6 * 30, P.time_horizon * 365,
50 /* Initial setting. */),
db {P}
{
d . changed_signal . connect ([this] { on_data_changed (); });
value_adjustment -> signal_value_changed ()
. connect ([this] { on_value_changed (); });
}
void Date_Range_Scale::on_value_changed ()
{
db.check_connection ();
data.timeseries__change_span
(db, chrono::hours {24} * int (value ()->get_value ()));
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__DATE_RANGE_SCALE__H
#define DMBCS__TRADER_DESK__DATE_RANGE_SCALE__H
#include <trader-desk/scale.h>
namespace DMBCS::Trader_Desk {
/** A \c Scale which controls the visual span of a \c Time_Series \c
* Chart. */
struct Date_Range_Scale : Exponential_Scale
{
DB db;
/** Sole constructor which provides a fully functioning
* object. */
Date_Range_Scale (Chart_Data &, Preferences&);
/** Called when the user slides the graphical slider. */
void on_value_changed ();
}; /* End of class Date_Range_Scale. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__DATE_RANGE_SCALE__H. */

51
trader-desk/db.cc Normal file
View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/db.h>
#include <iostream>
namespace DMBCS::Trader_Desk {
DB::DB (Preferences& P) : DB_Connection {P},
current_preferences {P}
{
/* Not sure if there is some proper way to do this, but things
* currently donʼt work unless we drop the current connection to the
* database and make a new one. */
/* reconnect (P); */
}
DB& DB::check_connection ()
{
if (! database_equal (current_preferences, last_preferences))
{
last_preferences = current_preferences;
DB_Connection::reconnect (current_preferences);
}
return *this;
}
} /* End of namespace DMBCS::Trader_Desk. */

62
trader-desk/db.h Normal file
View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__DB__H
#define DMBCS__TRADER_DESK__DB__H
#include <trader-desk/mysql.h>
/** \file
*
* Declaration of the DB class. */
namespace DMBCS::Trader_Desk {
/** Wrapper around a database connection which ensures that the tables
* we need are in place. */
/* Currently hard-wired around MySQL, but the intention is to
* abstract the database back-end. */
struct DB : Mysql::DB_Connection
{
Preferences& current_preferences;
Preferences last_preferences;
/** Establish a connection to the RDBMS, check for our database,
* create and pre-populate tables if necessary. May throw an \c
* Exception object. */
DB (Preferences&);
DB& check_connection ();
} ; /* End of class DB. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__DB__H. */

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/delta-analyzer.h>
namespace DMBCS::Trader_Desk {
Delta_Analyzer::Delta_Analyzer (Chart_Data &cd)
{
cd . new_company_signal
. connect ([this] { end_place = start_place; });
}
void Delta_Analyzer::graph_draw_hook
(Chart_Context &canvas,
Tide_Mark::List &,
unsigned number_shares,
vector <Tide_Mark::Price_Marker> const &)
{
if (end_place.x != start_place.x || end_place.y != start_place.y)
{
if (mouse_active)
{
delta_region.start = {canvas.date (start_place.x),
canvas.value (start_place.y)};
delta_region.end = {canvas.date (end_place.x),
canvas.value (end_place.y)};
if (start_place.x > end_place.x)
swap (delta_region.start, delta_region.end);
}
delta_region.render (canvas, number_shares);
}
}
bool Delta_Analyzer::button_down (int const x, int const y)
{
end_place = start_place = {x, y};
mouse_active = 1;
return 1;
}
bool Delta_Analyzer::button_move (int const x, int const y)
{
if (! mouse_active) return 0;
end_place = {x, y};
return 1;
}
bool Delta_Analyzer::button_up (int const, int const)
{
if (! mouse_active) return 0;
mouse_active = 0;
/* Needed to remove the box from the screen if the mouse was simply
* clicked and not moved. */
redraw_needed . emit ();
return 1;
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__DELTA_ANALYZER__H
#define DMBCS__TRADER_DESK__DELTA_ANALYZER__H
#include <trader-desk/analyzer.h>
#include <trader-desk/colour.h>
#include <trader-desk/delta-region.h>
namespace DMBCS::Trader_Desk {
struct Delta_Analyzer : Analyzer
{
/** Transparent structure to record mouse positions in the GTK
* window. */
struct Place { int x, y; };
/** The mouse coordinates of the starting corner of the delta region
* (where the mouse button was first pressed). */
Place start_place {-1, -1};
/** The mouse coordinates of the end corner of the delta region. If
* this equals \c start_place then the delta region is deemed not
* active. */
Place end_place {-1, -1};
/** Indicates if the mouse is currently being used to define the
* bounds of the delta rectangle. */
bool mouse_active {0};
/** Triggered internally if we need the system to re-draw the chart
* area. */
sigc::signal <void> redraw_needed;
/** The object which we are controlling, which is responsible for
* actually displaying the delta region and its computed parameters
* on a chart canvas. */
Delta_Region delta_region { Colour::POSITIVE_DELTA,
Colour::NEGATIVE_DELTA };
/** Set up for operations, and watch the \a chart_data for company
* changes when we must blank our on-screen presence. */
explicit Delta_Analyzer (Chart_Data &chart_data);
/** If there is anything to draw then set up \c delta_region if
* necessary and call through that object to get the actual work of
* drawing the region done. */
void graph_draw_hook (Chart_Context &context,
Tide_Mark::List &,
unsigned number_shares,
vector <Tide_Mark::Price_Marker> const &) override;
/** Provide our signal handler. */
sigc::signal<void> &signal_redraw_needed () override
{ return redraw_needed; }
/** Note that the mouse is now active and set (both) the corners of
* the delta region to the position of the mouse. */
bool button_down (int const x, int const y) override;
/** If the mouse is active, set the end point of the delta region
* definition to the mouse position. */
bool button_move (int const x, int const y) override;
/** If the mouse is active, set it to inactive and force a re-draw in
* case the region no longer should be displayed, e.g. if the mouse
* was just clicked. */
bool button_up (int const x, int const y) override;
} ; /* End of class Delta_Analyzer. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__DELTA_ANALYZER__H. */

109
trader-desk/delta-region.cc Normal file
View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/delta-region.h>
#include <iomanip>
/** \file
*
* Implementation of the \c Delta_Region class. */
namespace DMBCS::Trader_Desk {
template<typename DURATION>
static string readable_time_span (DURATION const &d)
{
namespace C = chrono;
ostringstream hold;
hold << fixed << setprecision (2);
if (d < C::hours {48})
hold << (number<C::minutes> (d) / 60.0)
<< " hours";
else if (d < C::hours {10 * 24})
hold << (number<C::hours> (d) / 24.0)
<< " days";
else
hold << (number<C::hours> (d) / (7.0 * 24.0))
<< " weeks";
return hold.str ();
}
void Delta_Region::render (Chart_Context &canvas,
unsigned const &number_shares) const
{
canvas.set_source_rgb (start.price > end.price ? negative_colour
: positive_colour);
canvas.move_to (start);
canvas.line_to ({end.time, start.price});
canvas.line_to (end);
canvas.line_to ({start.time, end.price});
canvas.line_to (start);
canvas.line_to (end);
canvas.cairo->stroke ();
canvas.set_source_rgb (start.price > end.price ? negative_colour
: positive_colour,
0.2);
canvas.move_to (start);
canvas.line_to ({end.time, start.price});
canvas.line_to (end);
canvas.line_to ({start.time, end.price});
canvas.cairo->fill ();
ostringstream percentage;
percentage << fixed << setprecision (2)
<< (end.price - start.price) / start.price * 100.0 << '%';
canvas.add (canvas.text, percentage.str (), Colour::DELTA_VALUE,
canvas.x ({end.time, (start.price + end.price) / 2.0}));
ostringstream delta;
delta << fixed << setprecision (2) << end.price - start.price;
canvas.add (canvas.text, delta.str (), Colour::DELTA_VALUE,
canvas.x ({end.time, (start.price + end.price) / 2.0}));
ostringstream cost;
cost << fixed << setprecision (2)
<< number_shares * (end.price - start.price) / 100.0;
canvas.add (canvas.text, cost.str (), Colour::DELTA_VALUE,
{canvas.x (start.time) - 60,
canvas.y ((start.price + end.price) / 2.0)});
canvas.add (canvas.text, readable_time_span (end.time - start.time),
Colour::DELTA_VALUE,
{ (canvas.x (start.time) + canvas.x (end.time)) / 2,
canvas.y (min (start.price, end.price))});
}
} /* End of namespace DMBCS::Trader_Desk. */

102
trader-desk/delta-region.h Normal file
View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__DELTA_REGION__H
#define DMBCS__TRADER_DESK__DELTA_REGION__H
#include <trader-desk/chart-context.h>
#include <trader-desk/text.h>
/** \file
*
* Declaration of the \c Delta_Region class. */
namespace DMBCS::Trader_Desk {
/** A \c Delta_Region is a rectangle marked out in (time, price) space
* with the intention of displaying the differences (deltas) in time
* and price along the edges and giving an indication of the rate of
* change of the price with time. These are the quantities which
* represent a traders gains or losses, actual or potential.
*
* The class is only half-autonomous, it holding the extreme values
* in (time, price) space but relying on the application to actually
* provide and manipulate these values as required (in practice this
* requirement is met by the \c Delta_Analyzer class). */
struct Delta_Region
{
/** The colour we use when \c start.price is _less_ than \c
* end.price. The border will be this solid colour, and the fill
* will be the same but with a large fraction of transparency. */
Colour positive_colour;
/** The alternative to the above colour which we use when \c
* start.price is _more_ than \c end.price. */
Colour negative_colour;
/** The position of the defining point on the left (earliest time)
* edge of the rectangle. */
Event start;
/** The position of the defining point on the right (latest time)
* edge of the rectangle. */
Event end;
/** Partial class constructor which establishes the colours that
* will be used to render any delta regions.
*
* It is left to the application to complete the specification of
* the object: viz the \c start and \c end points. */
Delta_Region (Colour const &p, Colour const &n)
: positive_colour {p}, negative_colour {n}
{}
/** Partial class constructor which establishes a uni-colour
* realization of the object on a chart.
*
* It is left to the application to complete the specification of
* the object: viz the \c start and \c end points. */
Delta_Region (Colour const &p) : Delta_Region {p, p}
{}
/** Show the region on the \a canvas along with summary data
* relating to the fact that the display refers to \a number_shares.
*
* Note that the application MUST set the \c start and \c end
* corners of the delta region before calling this method. */
void render (Chart_Context &canvas, unsigned const &number_shares) const;
} ; /* End of class Delta_Region. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__DELTA_REGION__H. */

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/hand-analysis-widget.h>
namespace DMBCS::Trader_Desk {
Hand_Analysis_Widget::Hand_Analysis_Widget
(function<void(Chart_Data&,int const &)> gcfg,
Preferences& P)
: chart {Chart::Style::HAND_ANALYSIS, P},
company_name {chart.data},
trade {P, chart.data},
date_range {chart.data, P},
shares {chart.data},
get_chart_from_grid {gcfg}
{
pack_start (address_bar, Gtk::PACK_SHRINK, 5);
pack_start (display_h_box, Gtk::PACK_EXPAND_WIDGET, 5);
address_bar.pack_start (company_name, Gtk::PACK_EXPAND_PADDING, 0);
address_bar.pack_start (trade, Gtk::PACK_EXPAND_PADDING, 0);
display_h_box.pack_start (chart, Gtk::PACK_EXPAND_WIDGET, 5);
display_h_box.pack_start (controls_h_box, Gtk::PACK_SHRINK, 0);
controls_h_box.set_spacing (SCALE_SEPARATION);
controls_h_box.pack_start (shares, Gtk::PACK_SHRINK, 0);
controls_h_box.pack_start (date_range, Gtk::PACK_SHRINK, 0);
for (auto const &w : chart.analyzer->make_control_widgets ())
controls_h_box.pack_start (*Gtk::manage (w), Gtk::PACK_SHRINK);
chart . analyzer
-> signal_redraw_needed ()
. connect ([this] { queue_draw (); });
company_name . name_change
. connect ([this, &P] (int const &seqid)
{ get_chart_from_grid (chart.data, seqid);
new_chart (P); });
} /* End of method Hand_Analysis_Widget::Hand_Analysis_Widget. */
void Hand_Analysis_Widget::new_chart (Preferences& P)
{
chart.data.extremes.start_time
= chart.data.extremes.end_time
- chrono::hours (24 * (int) date_range.value ()->get_value ());
chart.data . prefetch_
(P,
{chrono::hours {24 * (int)date_range.value ()->get_value ()},
chrono::hours {10 * 365 * 24}});
chart.data.changed_signal.emit ();
}
void Hand_Analysis_Widget::subsume_selected (Chart_Grid &grid)
{
DB db {grid.user_prefs};
company_name.read_names (db,
grid.market.seqid,
grid.selection->data.company_seqid);
chart.data.subsume (&grid.selection->data);
new_chart (grid.user_prefs);
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,126 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__HAND_ANALYSIS_WIDGET__H
#define DMBCS__TRADER_DESK__HAND_ANALYSIS_WIDGET__H
#include <trader-desk/company-name-entry.h>
#include <trader-desk/trade-instruction.h>
#include <trader-desk/date-range-scale.h>
#include <trader-desk/shares-scale.h>
namespace DMBCS::Trader_Desk {
/*
+-----VBox-------------------------------------------------------------------------------------------+
| +----address_bar--------------------------------------------------------------------------------+ |
| | +--company_name-------------+ +--positions---------------------+ +--trade_instruction----+ | |
| | | | | | | | | |
| | +---------------------------+ +--------------------------------+ +-----------------------+ | |
| +-----------------------------------------------------------------------------------------------+ |
| +----display_h_box------------------------------------------------------------------------------+ |
| | +----chart------------------------+ +----controls_h_box----------------------------------+ | |
| | | | | +-shares-+ +-date_range--+ +--<Analyzer scales>--+ | | |
| | | | | | | | | | | | | |
... ... ...
| | | | | | | | | | | | | |
| | | | | +--------+ +-------------+ +---------------------+ | | |
| | +---------------------------------+ +----------------------------------------------------+ | |
| +-----------------------------------------------------------------------------------------------+ |
+----------------------------------------------------------------------------------------------------+
*/
/** This is a compound widget which entirely looks after itself under
* the GTK machinery and the cooperation between various components
* (almost all hang off the back of the chart object which throws a
* signal whenever anything changes). The composition--layout--of the
* widget is shown above. */
struct Hand_Analysis_Widget : Gtk::VBox
{
/** The gap between sliders in the \c controls_h_box, needed so that
* complicated controls can emulate the appearance of this top-level
* widget. */
static constexpr int const SCALE_SEPARATION {10};
/** Layout widget as per diagram above. */
Gtk::HBox display_h_box;
/** Layout widget as per diagram above. */
Gtk::HBox controls_h_box;
/** Control widget as per diagram above. */
Gtk::HBox address_bar;
/** The chart which we are hand-analyzing. */
Chart chart;
/** Display and select the company whose data are in the \c chart. */
Company_Name_Entry company_name;
/** Buy/sell button and current price input. */
Trade_Instruction trade;
/** Select the range of dates over which data are shown. */
Date_Range_Scale date_range;
/** Select the number of shares in a hypothetical position (if we are
* in a position, this will be a static object which simply shows the
* number of shares). */
Shares_Scale shares;
/** Us calling this method, provided by the wider application, will
* get the \a Chart_Data populated with the data from a chart which
* corresponds to the company with \a seqid. */
function<void(Chart_Data &data, int const &seqid)> get_chart_from_grid;
/** Sole constructor which gives us a fully operational object. The
* incoming function \a gcfg must provide the machinery we need in \c
* get_chart_from_grid. */
Hand_Analysis_Widget (function<void(Chart_Data&, int const &)> gcfg,
Preferences&);
/** Find the selected chart in the \a grid, and then subsume that
* chart into our display: take a shadow copy of the data and show
* these in the graph. */
void subsume_selected (Chart_Grid &grid);
private:
/* Implementation detail: whenever we change to charting some new
* data we must arrange for the associated pre-fetch thread to run so
* that the amount of data available for display matches at least the
* time-period which the current chart spans. */
void new_chart (Preferences&);
}; /* End of class Hand_Analysis_Widget. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__HAND_ANALYSIS_WIDGET__H. */

62
trader-desk/makefile.am Normal file
View file

@ -0,0 +1,62 @@
# Copyright (c) 2020 Dale Mellor
#
# This file is part of the trader-desk package.
#
# The trader-desk package 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 3 of the
# License, or (at your option) any later version.
#
# The trader-desk package 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, see http://www.gnu.org/licenses/.
AM_CXXFLAGS = ${gtk_config_CFLAGS} -I${top_srcdir} \
-Wno-deprecated-copy \
-Wall -Wextra \
-Wno-error=int-in-bool-context \
-Werror \
-DCURLPP_GLOBAL_H=1 \
-DPKGDATADIR=\"${pkgdatadir}\" \
-DLOCALEDIR=\"${localedir}\" \
-DHAVE_MYSQL=${HAVE_MYSQL} \
-DHAVE_MARIADB=${HAVE_MARIADB} \
-D_GNU_SOURCE=1 \
-std=c++2a -I${includedir}
AM_LDFLAGS = ${gtk_config_LIBS}
bin_PROGRAMS = trader-desk
lib_LTLIBRARIES = libtrader-desk.la
CLASSES = alpha-vantage alpha-vantage--monitor analyzer application \
chart chart-context chart-data chart-grid \
colour company-name-entry \
date-axis date-range-scale db delta-analyzer delta-region \
hand-analysis-widget \
markets moving-average-analyzer mysql \
preferences \
scale sd-envelope-analyzer shares-scale \
text time-series trade-instruction \
update-closing-prices update-latest-prices \
wizard
pkginclude_HEADERS = ${CLASSES:=.h} tide-mark.h
nodist_noinst_HEADERS = auto-config.h
libtrader_desk_la_SOURCES = ${CLASSES:=.cc} \
application--ingest-market.cc \
application--update-closing-prices.cc
LDADD = libtrader-desk.la ${LTLIBINTL}
trader_desk_SOURCES = trader-desk.cc
MAINTAINERCLEANFILES = makefile.in auto-config.h.in

332
trader-desk/markets.cc Normal file
View file

@ -0,0 +1,332 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/markets.h>
#include <trader-desk/time-series.h> /* Only for number<> (). */
namespace DMBCS::Trader_Desk {
Markets::Markets (DB& db)
{
auto sql {db.row_query ()};
sql << "select seqid, symbol, name, component_extension, tracked, "
<< " unix_timestamp(last_update), "
<< " 60*hour(close_time)+minute(close_time) "
<< " from market";
Market_Meta_Data m;
try
{
for (sql.execute (); sql; ++sql)
{
sql >> m.seqid >> m.world_data.symbol
>> m.world_data.name >> m.world_data.component_extension
>> m.tracked;
m.last_time = sql.next_entry ((time_t) 0);
m.world_data.close_time = chrono::minutes {sql.next_entry (0)};
this->insert ({m.seqid, m});
}
}
catch (Mysql::DB_Connection::Exception&) {}
}
static Markets::iterator find_symbol (Markets& markets,
const string& symbol)
{
return find_if (markets.begin (), markets.end (),
[&symbol] (const Markets::value_type& d)
{ return d.second.world_data.symbol == symbol; });
}
void update_market_meta_data (Markets& markets, DB& db)
try
{
if (Market_Data_Api::short_time
(db.scalar_result
((time_t) 0,
"select unix_timestamp(last_markets_update) from global")))
return;
/* !!! Need to be prepared for this not to work (offline?) */
for (const Market_Data_Api::Market& i : Market_Data_Api::get_markets ())
if (find_symbol (markets, i.symbol) == markets.end ())
{
auto s {db.instruction ()};
s << "insert into market "
<< " set symbol='" << i.symbol << "', "
<< " name=\"" << i.name << "\", "
<< " component_extension='"
<< i.component_extension << "', "
<< " close_time=sec_to_time("
<< number<chrono::seconds> (i.close_time)
<< ") ";
s.execute ();
markets.insert ({(size_t) s.insert_id (),
{.world_data = i,
.seqid = (size_t) s.insert_id (),
.tracked = 0,
.last_time = 0}});
}
db.instruction ("update global "
"set last_markets_update=from_unixtime(%d)",
time (nullptr));
}
catch (Mysql::DB_Connection::Exception&) {}
bool update_components
(Markets& markets, DB& db, Gtk::Window *const window)
{
/* !!! This wants to be the time on the individual market, *not* the
* global time-stamp. */
if (Market_Data_Api::short_time
(db.scalar_result
((time_t) 0,
"select unix_timestamp(last_markets_update) from global")))
return false;
update_market_meta_data (markets, db);
bool ret {false};
for (auto& m : markets)
if (m.second.tracked)
ret = ret || Trader_Desk::update_components
(m.second, db, window);
db.instruction ("update global "
"set last_markets_update=from_unixtime(%d)",
time (nullptr));
return ret;
}
void update_database
(const Market_Data_Api::Delta& d, DB& db, const int market_seqid)
{
const int company_seqid
{db.scalar_result (0,
"select seqid "
"from company "
"where symbol='%s' and market=%d",
d.symbol.c_str (),
market_seqid)};
if (company_seqid)
db.quick () << "update company "
<< "set market=" << market_seqid
<< " where seqid=" << company_seqid;
else
db.quick () << "insert into company "
<< "set name=\"" << d.name << "\", "
<< "symbol='" << d.symbol << "', "
<< "market=" << market_seqid;
}
static void replace (string& in, const char a, const string& text)
{
for (size_t cursor {0};
(cursor = in.find (a, cursor)) != in.npos;
++cursor)
in.replace (cursor, 1, text);
}
static string htmlize (string in)
{
replace (in, '&', "&amp;");
replace (in, '<', "&lt;" );
replace (in, '>', "&gt;" );
return in;
}
bool update_components (Market_Meta_Data& market_data,
DB& db,
Gtk::Window *const window)
{
if (Market_Data_Api::short_time (market_data.last_time))
return false;
auto instructions {Market_Data_Api::get_component_delta
(market_data.world_data.symbol,
market_data.last_time)};
market_data.last_time = time (0);
db.instruction ("update market "
" set last_update=from_unixtime(%d) "
" where seqid=%d",
market_data.last_time, market_data.seqid);
if (instructions.empty ()) return false;
/* List added companies for the benefit of the user. */
string additions;
/* List removed companies for the benefit of the user. */
string removals;
/* The total number of additions and removals. */
size_t line_count {0};
/* Whether or not a terminating ... has been added to the additions. */
bool additions_terminated {false};
/* Whether or not a terminating ... has been added to the removals. */
bool removals_terminated {false};
auto make_updater
= [&line_count]
(string& line, bool& terminated)
{
return [&line, &terminated, &line_count]
(string const &name, string const &symbol)
{
if (++line_count < 10)
line += " " + htmlize (name)
+ " (" + symbol + ")\n";
else if (! terminated)
{
terminated = 1;
line += "... (more)...\n";
}
};
};
auto update_additions = make_updater (additions, additions_terminated);
auto update_removals = make_updater (removals, removals_terminated);
const Market_Data_Api::Delta* move_pending {nullptr};
for (auto const &i : instructions)
{
if (move_pending)
{
if (i.action != i.SIDEWAYS) break;
/* !!! This is a potentially dangerous operation if the same
* symbol is used in different markets. We will have to
* be careful how we run the server in this regard. */
auto const company_seqid
= db.scalar_result (0,
"select seqid "
"from company "
"where symbol='%s'",
move_pending->symbol.c_str ());
if (! company_seqid)
db.instruction ("insert into company "
"set name=\"%s\", "
"symbol='%s', "
"market=%d",
i.name.c_str (), i.symbol.c_str (),
market_data.seqid);
else
db.instruction ("update company "
"set name=\"%s\", "
"symbol='%s', "
"market=%d "
"where seqid=%d",
i.name.c_str (), i.symbol.c_str (),
market_data.seqid, company_seqid);
update_additions (i.name, i.symbol);
move_pending = nullptr;
}
else if (i.action == i.SIDEWAYS)
{
move_pending = &i;
}
else if (i.action == i.ADD)
{
update_database (i, db, market_data.seqid);
update_additions (i.name, i.symbol);
}
else /* i.action == REMOVE */
{
db.quick () << "update company "
<< "set market=0 "
<< "where symbol='" << i.symbol << "' "
<< "and market=" << market_data.seqid;
update_removals (i.name, i.symbol);
}
}
if (! window) return true;
string a = "<b>MARKET MOVEMENTS</b>\n\n<u>"
+ market_data.world_data.name + "</u>\n";
if (additions.length ())
a += "<span color=\"green\">New entries</span>\n" + additions;
if (removals.length ())
a += "<span color=\"red\">Dropped entries</span>\n" + removals;
Gtk::MessageDialog {*window, a, 1/*use mark-up*/} . run ();
return true;
}
void start_tracking (Markets& markets, DB& db, const string& symbol)
{
const Markets::iterator m {find_symbol (markets, symbol)};
if (m == markets.end ()) return;
m->second.tracked = 1;
db.instruction ("update market set tracked=1 where seqid=%d", m->first);
}
} /* End of namespace DMBCS::Trader_Desk. */

119
trader-desk/markets.h Normal file
View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__MARKETS__H
#define DMBCS__TRADER_DESK__MARKETS__H
#include <dmbcs-market-data-api.h>
#include "db.h"
#include <gtkmm.h>
/** \file
*
* Declaration of the \c Market_Meta_Data class and the \c Markets
* class. */
namespace DMBCS::Trader_Desk {
/** Really just a data-carrying structure (vessel) which fully describes
* a stock market. It is divided into two parts: data which describe
* the markets in the eyes of the outside world, and data which
* describe the state of the markets from our own, private, point of
* view. */
struct Market_Meta_Data
{
/** The subset of data which relate to the state of affairs in the
* world beyond this application; these are obtained from an
* Internet server through the medium of the
* dmbcs-market-data-api library. */
Market_Data_Api::Market world_data;
/** The sequence ID of this market in our local database. */
size_t seqid;
/** Whether we are keeping a database of prices for this
* market. */
bool tracked;
/** The last time the component data for this market were
* synchronized with a market data server. */
time_t last_time;
}; /* End of class Market_Meta_Data. */
void update_database
(const Market_Data_Api::Delta&, DB&, const int market_seqid);
/** Fetch the data for this market from the Internet and update the
* database if necessary. A progress indication may be provided in a
* dialog on top of the window. */
bool update_components (Market_Meta_Data&, DB&, Gtk::Window *const);
/** A self-building collection of all markets known to the on-line data
* server, indexed according to our local database sequence ID. */
struct Markets : map <size_t, Market_Meta_Data>
{
/** Sole constructor, which self-builds our \c map from information
* contained in the local \a database. */
explicit Markets (DB& database);
/** Constructing these objects is expensive; allow them to be moved
* around but not duplicated. */
Markets () = delete;
Markets (Markets const &) = delete;
Markets (Markets &&) = default;
Markets &operator= (Markets const &) = delete;
Markets &operator= (Markets &&) = default;
} ; /* End of class Markets. */
/** Market the market with \a symbol as being tracked, both in memory
* (here) and in the database. Note that this does NOT cause any price
* data to be fetched from the server. */
void start_tracking (Markets&, DB&, const string& symbol);
/** Get information on all known markets from the server, and update our
* own information and the database. */
void update_market_meta_data (Markets&, DB&);
/** Update the meta-data with the above method, then update the
* components of each market we follow (note *not* the price
* information!) These actions will not be allowed to take place more
* than once per 12 hours. The return value indicates if anything has
* actually changed. If a window is given, provide a report on changes
* in a dialog box on the window. */
bool update_components (Markets *const, Gtk::Window *const = nullptr);
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__MARKETS__H. */

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/moving-average-analyzer.h>
/** \file
*
* Implementation of the \c Moving_Average_Analyzer class. */
namespace DMBCS::Trader_Desk {
Moving_Average_Analyzer::Moving_Average_Analyzer (Chart_Data &cd)
: chart_data (cd),
mean_series {cd.prices.market_close_time}
{
chart_data . changed_signal . connect ([this] { compute (); });
}
vector <Gtk::Widget*> Moving_Average_Analyzer::make_control_widgets ()
{
auto *const scale
= new Scale {chart_data,
(double) (number<chrono::hours> (mean_window) / 24)};
scale -> value ()
-> signal_value_changed ()
. connect ([this, scale] { control_moved (scale); });
return {scale};
}
void Moving_Average_Analyzer::stretch_outline (Time_Series::Range &outline)
{
/* !!!! When we re-visit this, need to ensure that the mean
* time-series has been previously computed. */
auto const range = mean_series.get_range ();
auto const margin = (std::max (outline.max_value, range.max_value)
- std::min (outline.min_value, range.min_value))
* 0.05;
outline.max_value = max (outline.max_value, range.max_value + margin);
outline.min_value = min (outline.min_value, range.min_value - margin);
}
void Moving_Average_Analyzer::graph_draw_hook
(Chart_Context &canvas,
Tide_Mark::List &marks,
unsigned,
vector <Tide_Mark::Price_Marker> const &markers)
{
canvas . draw_time_series (mean_series, Colour::MEAN_GRAPH, 0.5);
/* The vertical bar which shows the mid-point of the latest window. */
canvas . set_source_rgb (Colour::MEAN_GRAPH);
canvas . move_to ({canvas.outline.end_time - mean_window / 2,
canvas.outline.min_value});
canvas . line_to ({canvas.outline.end_time - mean_window / 2,
canvas.outline.max_value});
canvas . cairo -> stroke ();
/* Put a tide-mark at the mean value at all points in time at which a
* marker has been specified. */
for (auto const &marker : markers)
marks.emplace_back (marker (mean_series.interpolated_value
(marker (0.0, Colour::MEAN_TIDE).time),
Colour::MEAN_TIDE));
}
void Moving_Average_Analyzer::control_moved (Scale const *const scale)
{
mean_window = chrono::hours ((int) scale->value ()->get_value () * 24);
compute ();
}
void Moving_Average_Analyzer::compute ()
{
{
lock_guard<mutex> l {chart_data.prices_mutex};
mean_series = Time_Series::compute_moving_average
(chart_data.prices,
mean_window,
chart_data.extremes.start_time);
}
redraw_needed_.emit ();
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__MOVING_AVERAGE_ANALYZER__H
#define DMBCS__TRADER_DESK__MOVING_AVERAGE_ANALYZER__H
#include <trader-desk/analyzer.h>
#include <trader-desk/scale.h>
/** \file
*
* Declaration of the \c Moving_Average_Analyzer class. */
namespace DMBCS::Trader_Desk {
/** An \c Analyzer which computes and displays a smoothed version of a
* prices time-series. */
struct Moving_Average_Analyzer : Analyzer
{
/** The widget which we proffer to control ourselves is nothing more
* than an exponential scale, calibrated to allow specification of
* averaging windows between one day and a year, with fine control
* out to 14 days. */
struct Scale : Exponential_Scale
{
Scale (Chart_Data &chart_data, double const &initial_value)
: Exponential_Scale (chart_data,
pgettext ("Label",
"Mean window = %.0f days"),
Gdk::RGBA {"#ff0000"},
Gdk::RGBA {"#ffaaaa"},
Gdk::RGBA {"#ff6666"},
1, 14, 365,
initial_value)
{}
};
/** The data that we are to analyze. */
Chart_Data &chart_data;
/** The size of the window over which we compute means. */
Duration mean_window {chrono::hours {14*24}};
/** The resulting time-series of local mean values. */
Time_Series mean_series;
/** Fired whenever the analysis of data produces new results, which will
* need rendering in the GUI. */
sigc::signal <void> redraw_needed_;
/** Called when the user slides the control on the \a
* exponential_scale, meaning to change the moving-average window. */
void control_moved (Scale const *const);
/** Called whenever we must re-compute the moving-average
* time-series, including when the \c chart_data change. */
void compute ();
/** Sole constructor which registers the \a chart_data we are to
* analyze. */
explicit Moving_Average_Analyzer (Chart_Data &);
/********************** Analyzer interface. ****************************/
/** Make a single widget which controls the size of the moving average
* window. */
vector <Gtk::Widget*> make_control_widgets () override;
/** Draw the \c mean_series and add a \a tide label at all the \a
* marked points in time. */
void graph_draw_hook (Chart_Context &,
Tide_Mark::List &tide,
unsigned number_shares,
vector <Tide_Mark::Price_Marker> const &marked)
override;
/** Make sure the \a range includes the entire mean series, with room
* to breathe. */
void stretch_outline (Time_Series::Range &range) override;
/** Return our signal so that the application can connect and act when
* we need a re-draw to take place. */
sigc::signal <void> &signal_redraw_needed () override
{ return redraw_needed_; }
}; /* End of class Moving_Average_Analyzer. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__MOVING_AVERAGE_ANALYZER__H. */

163
trader-desk/mysql.cc Normal file
View file

@ -0,0 +1,163 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/mysql.h>
#include <iostream>
#include <cstdio>
/** \file
*
* Implementation of class methods in \c Mysql namespace. */
namespace DMBCS::Trader_Desk { namespace Mysql {
int Instruction::execute ()
{
const string query {buffer->str ()};
const int test {mysql_real_query (mysql, query.c_str(), query.length())};
if (test && ! no_error)
{
cerr << "MYSQL ERROR: " << mysql_error (mysql) << endl;
throw DB_Connection::Exception {mysql_error (mysql)};
}
return test;
}
void DB_Connection::connect (const Preferences& P)
{
if (! mysql_real_connect (&mysql,
P.database_host.data (),
P.database_user.data (),
P.database_password.data (),
P.database_instance.data (),
P.database_port,
P.database_socket.data (),
0/*flags*/))
throw Exception {mysql_error (&mysql)};
initialized = 1;
}
void DB_Connection::reconnect (const Preferences& P)
{
if (initialized) mysql_close (&mysql);
initialized = 0;
connect (P);
}
DB_Connection::DB_Connection (const Preferences& P)
{
mysql_init (&mysql);
connect (P);
}
static int _run_query (MYSQL *const mysql,
string const &_template,
va_list arguments)
{
int buffer_length = _template.length () + 500;
char *buffer = new char [buffer_length];
int length;
for (;;)
{
length = vsnprintf
(buffer, buffer_length, _template.c_str (), arguments);
if (length == buffer_length)
{
buffer_length += 1000;
delete[] buffer;
buffer = new char [buffer_length];
}
else
break;
}
int const test = mysql_real_query (mysql, buffer, length);
delete[] buffer;
return test;
}
void DB_Connection::void_database_result (MYSQL *const mysql,
string const &template_,
va_list arguments)
{
if (_run_query (mysql, template_, arguments) != 0)
cerr << mysql_error (mysql) << endl;
}
string DB_Connection::string_database_result (MYSQL *const mysql,
string const &template_,
va_list arguments)
{
if (_run_query (mysql, template_, arguments) != 0)
{
cerr << mysql_error (mysql) << endl;
return string {};
}
MYSQL_RES *const results = mysql_store_result (mysql);
if (! results)
return string {};
MYSQL_ROW const row = mysql_fetch_row (results);
string const ret = (row && row [0]) ? string {row [0]} : string {};
mysql_free_result (results);
return ret;
}
void DB_Connection::instruction (string const &template_, ...)
{
va_list args; va_start (args, template_);
void_database_result (&mysql, template_, args);
va_end (args);
}
} } /* End of namespace DMBCS::Trader_Desk::Mysql. */

508
trader-desk/mysql.h Normal file
View file

@ -0,0 +1,508 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__MYSQL__H
#define DMBCS__TRADER_DESK__MYSQL__H
#include <chrono>
#include <string>
#include <sstream>
#include <cstdarg>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <trader-desk/preferences.h>
#if HAVE_MYSQL
# include <mysql/mysql.h>
#endif
#if HAVE_MARIADB
# include <mariadb/mysql.h>
#endif
/** \file
*
* Definition of \c Mysql::DB_Connection, \c Mysql::Instruction, \c
* Mysql::Quick_Instruction, \c Mysql::Simple_Query and \c
* Mysql::Row_Query, objects which capture between them all the specifics
* of operating a MySQL/MariaDB database. */
/* Note that it is the intention to base these classes on abstract ones,
* and thence to provide alternative database back-ends. */
namespace DMBCS::Trader_Desk {
using namespace std;
/** This namespace encaptures everything which is specific to a
* MySQL/MariaDB database back-end (specifically \c libmysqlclient).
* Nothing outside should have any inkling of the fact that we are using
* such a database. */
namespace Mysql {
/** Object which provides std::ostream-type features to develop an SQL
* query string (should be one which produces no useful results) and
* then allows for its execution. If this produces some unique \c
* seqid (e.g. by inserting into a table with an automatic index
* column), then that value can subsequently be obtained with the \c
* insert_id method. */
struct Instruction
{
/** The connection to the database which we will be using. */
MYSQL *mysql;
/** Flag to indicate if we should print any error messages (\c FALSE)
* or not. */
bool const no_error;
/** Place where we accumulate the query string. */
unique_ptr <ostringstream> buffer;
/** Flag for the constructor which allows to specify that no error
* messages should be printed. */
enum : bool { NO_ERROR = true };
/** Sole constructor which takes a connection to the database and an
* optional flag (see above) to indicate no error messages should
* appear. */
explicit Instruction (MYSQL *const m, bool const ne = false)
: mysql {m},
no_error {ne},
buffer {make_unique<ostringstream> ()}
{}
/** We can't copy these objects as that would wreak havoc with the \c
* buffer, but we want to be able to move them so they can be passed
* back from factory functions. */
Instruction (Instruction const &) = delete;
Instruction (Instruction &&) = default;
Instruction &operator= (Instruction const &) = delete;
Instruction &operator= (Instruction &&) = default;
/** This is the method which makes our class look like an
* std::ostream. */
template <typename T>
Instruction &operator<< (T const &i) { *buffer << i; return *this; }
/** Send the query string, which should by now have been sent into the
* \c buffer, to the database and return the resulting status (zero
* is success) of the operation. */
int execute ();
/** If the query caused an auto-incrementing table column to be
* updated, this method will return the last value assigned. */
int insert_id () { return mysql_insert_id (mysql); }
} ; /* End of class Instruction. */
/** A \c Quick_Instruction is just a \c Instruction which
* executes the query on object destruction, allowing for one-line
* instructions to make modifications to the database,
* e.g. `Quick_Instruction {} << "update table ...";'. */
struct Quick_Instruction : Instruction
{
/** All construction, move, no-copying construction is exactly as \c
* Instruction. */
using Instruction::Instruction;
Quick_Instruction (Quick_Instruction const &) = delete;
Quick_Instruction (Quick_Instruction &&m) = default;
Quick_Instruction &operator= (Quick_Instruction const &) = delete;
Quick_Instruction &operator= (Quick_Instruction &&) = delete;
/** Do the work in the destructor. */
~Quick_Instruction () { Instruction::execute (); }
/* Make sure the user can't accidentally call the execution
* directly. */
int execute () = delete;
};
/** A \c Simple_Query is like a \c Instruction which returns
* a single meaningful value. The usage pattern is to construct,
* assemble a query with the inherited \c operator<<, and then call \c
* return_scalar to get the solitary resulting value. */
struct Simple_Query : Instruction
{
/** Construction, move, non-copy is exactly as for \c
* Instruction. */
using Instruction::Instruction;
Simple_Query (Simple_Query const &) = delete;
Simple_Query (Simple_Query &&) = default;
Simple_Query &operator= (Simple_Query const &) = delete;
Simple_Query &operator= (Simple_Query &&) = default;
/** Execute the assembled query and return the result, cast to type \c
* T. The \a fallback both determines the return type and also the
* value that will be returned if the database fails to provide
* this. */
template <typename T>
T return_scalar (T const &fallback = T ());
/** Make sure the user can't accidentally call for the execution
* directly. */
int execute () = delete;
} ;
/** This class represents a database query which produces (selects)
* multiple rows of multiple columns of results. It is used exactly as
* a \c Instruction, but after calling the \c execute method the
* class provides iterators and other convenience access methods for
* retrieving the data. */
class Row_Query : public Instruction
{
/** The result from the database query. Note that this is a resource
* we manage locally, so have to be careful with class move and
* destruction. */
MYSQL_RES *result {nullptr};
/** An index into the rows of the \c result. */
MYSQL_ROW row;
/** An index into the columns of the \c row we are currently
* examining. */
int next_index;
public:
/** Construction, move and non-copy is the same as for the base
* classes, but we have to take care with the handling of our
* controlled resource: the \c result. */
using Instruction::Instruction;
Row_Query (Row_Query const &) = delete;
Row_Query (Row_Query &&m)
: Instruction {move (m)},
result {m.result}, row {m.row}, next_index {m.next_index}
{ m.result = nullptr; }
Row_Query &operator= (Row_Query const &) = delete;
Row_Query &operator= (Row_Query &&m)
{
result = m.result; m.result = nullptr;
row = m.row;
next_index = m.next_index;
return *this;
}
~Row_Query () { if (result) mysql_free_result (result); }
/** Perform the database query, and then obtain a result manager,
* fetch the first row of results, and set up the result indexers to
* indicate that the first column of the first row will be the next
* available result value. */
Row_Query &execute () { Instruction::execute ();
result = mysql_store_result (mysql);
row = result ? mysql_fetch_row (result) : 0;
next_index = 0;
return *this; }
/** After \c execute has been called, return the number of rows of
* data that are available. */
int number_rows () const { return mysql_num_rows (result); }
/** Skip over \a count columns in the current row. */
Row_Query &skip_entry (const int count = 1) { next_index += count;
return *this; }
/** Read the next result value into \a ret, and advance the indexers
* to the next column. */
template <typename T>
Row_Query &operator>> (T &ret)
{
istringstream in (row [next_index++]);
in >> ret;
return *this;
}
/** Return the next value cast to type \c T, returning \a fallback if
* the database did not provide a valid value, and advancing the
* index along to the next column so that subsequent calls to this
* method automatically iterate through the values of the row. This
* method can be use interchangeably with the previous one (\c
* operator>>), according to convenience. */
template <typename T> T next_entry (const T& fallback = {})
{
if (row [next_index] == 0) { ++next_index;
return fallback; }
T ret;
*this >> ret;
return ret;
}
/** Return \c TRUE if there are more rows to reap data for. */
operator bool () const { return row; }
/** Iterate to the next row in the result data set. Combined with the
* above result this allows for the straight-forward implementation
* of \c for(;;) loops over all the rows in a result set. */
void operator++ () { row = mysql_fetch_row (result);
next_index = 0; }
} ; /* End of class Row_Query. */
/** Essentially just a MYSQL object which automatically opens and closes
* a connection to the \c trader-desk database on object construction
* and destruction.
*
* The sole constructor may throw an exception if a database connection
* cannot be established. The copy constructor is implicitly deleted,
* the move constructors implicitly defined.
*
* Once the connection is established, the class provides factory
* methods for the above query object types, plus a couple of
* convenience functions which allow for one-line printf-style query
* specification and execution.
*
* Note that most of the connection parameters come through the
* config.h file, except the password which is passed directly on the
* compiler command line, via makefile.am and configure.ac; we do not
* store it in any source files, hence it doesn't get into GIT, but do
* beware that the raw string is present in built binaries and, no
* doubt, in infrastructure files in the package build directory. */
struct DB_Connection
{
/** Thrown if a connection to the RDBMS cannot be made. */
struct Exception : runtime_error
{ using runtime_error::runtime_error; };
/** The real connection object which we are wrapping. */
MYSQL mysql;
bool initialized {0};
/** Establish a connection to the RDBMS. May throw an \c Exception
* object. */
explicit DB_Connection (const Preferences&);
/** We only want one of these for the application, so copy and move
* are irrelevant and are \c delete'd. */
DB_Connection (DB_Connection const &) = delete;
DB_Connection (DB_Connection &&m) = delete;
DB_Connection &operator= (DB_Connection const &) = delete;
DB_Connection &operator= (DB_Connection &&m) = delete;
/** Close the connection to the database. */
~DB_Connection () { if (initialized) mysql_close (&mysql); }
/** Manufacture an \c Instruction. */
Instruction instruction () { return Instruction {&mysql}; }
/** Manufacture a \c Quick_Instruction. */
Quick_Instruction quick () { return Quick_Instruction {&mysql};}
/** Manufacture a \c Simple_Query. */
Simple_Query simple_query () { return Simple_Query {&mysql}; }
/** Manufacture a \c Row_Query. */
Row_Query row_query () { return Row_Query {&mysql}; }
/** Implementation of following (\c instruction) method. */
static void void_database_result (MYSQL *const mysql,
string const &template_,
va_list arguments);
/** Execute a one-shot SQL statement on the database, expressed
* through the printf-style \a template_ and arguments. There can be
* no return information, thus the query should be one which does not
* return any. */
void instruction (string const &template_, ...);
/** Implementation of following (\c scalar_result) method. */
static string string_database_result (MYSQL *const mysql,
string const &template_,
va_list arguments);
/** Make a query on the database which obtains a single-valued result.
* The \a fallback_value serves both to define the type of result
* returned, and sets the returned value in the case that the
* database is unable to furnish the information. The SQL query
* itself is composed of the printf()-type string \a template_ with
* substitutions from the following arguments. */
template <typename T>
T scalar_result (T const &fallback_value,
string const &template_,
...);
/** Either make a connection to the database (\c mysql must *not*
* already be connected), or else throw an \c Exception. */
void connect (const Preferences&);
/** Close and re-make the connection to the database. */
void reconnect (const Preferences&);
} ; /* End of class DB_Connection. */
/*******************************************************************
****************** Template implementations ********************
*******************************************************************/
template <typename T>
inline T Simple_Query::return_scalar (T const &fallback)
{
string const hold = return_scalar (string {"@@"});
if (hold.length () == 2 && hold == "@@") return fallback;
T ret; istringstream i {hold}; i >> ret;
return ret;
}
template<>
inline string Simple_Query::return_scalar<string> (string const &fallback)
{
if (Instruction::execute () != 0) return fallback;
MYSQL_RES *const results = mysql_store_result (mysql);
if (! results) return fallback;
MYSQL_ROW const row = mysql_fetch_row (results);
string const ret = row ? string {row [0]} : fallback;
mysql_free_result (results);
return ret;
}
template <>
inline string Row_Query::next_entry<string> (string const &fallback)
{
auto const &ret = row [next_index++];
return ret ? ret : fallback;
}
template <>
inline Row_Query &Row_Query::operator>> <string> (string &ret)
{
ret = next_entry (string {});
return *this;
}
template <>
inline Row_Query &Row_Query::operator>> <chrono::system_clock::time_point>
(chrono::system_clock::time_point &ret)
{
time_t t;
(*this) >> t;
ret = chrono::system_clock::from_time_t (t);
return *this;
}
template <typename T>
inline T DB_Connection::scalar_result (T const &fallback_value,
string const &template_,
...)
{
va_list args; va_start (args, template_);
string const value {string_database_result (&mysql, template_, args)};
va_end (args);
if (! value.length ())
return fallback_value;
istringstream i (value);
T ret;
i >> ret;
return ret;
}
template <>
inline string DB_Connection::scalar_result (const string& fallback_value,
const string& template_,
...)
{
va_list args; va_start (args, template_);
const string value {string_database_result (&mysql, template_, args)};
va_end (args);
if (! value.length ()) return fallback_value;
return value;
}
} } /* End of namespace DMBCS::Trader_Desk::Mysql. */
#endif /* Defined DMBCS__TRADER_DESK__MYSQL__H. */

360
trader-desk/preferences.cc Normal file
View file

@ -0,0 +1,360 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include "preferences.h"
#include "alpha-vantage.h"
#include <fstream>
#include <regex>
namespace DMBCS::Trader_Desk {
Preferences Preferences::defaults ()
{
return
{
.trade_cost_offset = 0.0,
.trade_cost_factor = 0.0,
.database_host = "localhost",
.database_user = "trader_desk",
.database_password = "123456",
.database_instance = "trader_desk",
.database_socket = "/run/mysqld/mysqld.sock",
.database_port = 3306,
.market_meta_data_service = "https://rdmp.org:9443/trader-desk/",
.market_data_service = "https://www.alphavantage.co/query",
.market_data_service_key = ""
};
}
void dump (const Preferences& P)
{
dump (P, P.file_path.empty ()
? getenv ("HOME") + string {"/.config/trader-desk.conf"}
: P.file_path);
}
void dump (const Preferences& P, const string& file)
{ dump (P, ofstream {file}); }
static Preferences dump (Preferences&& P, const string& file)
{ dump (P, file); return P; }
void dump (const Preferences& P, ostream&& O)
{
O << "trade_cost_offset: " << P.trade_cost_offset << "\n"
<< "trade_cost_factor: " << P.trade_cost_factor << "\n"
<< "database_host: " << P.database_host << "\n"
<< "database_user: " << P.database_user << "\n"
<< "database_password: " << P.database_password << "\n"
<< "database_instance: " << P.database_instance << "\n"
<< "database_socket: " << P.database_socket << "\n"
<< "database_port: " << P.database_port << "\n"
<< "market_meta_data_service: " << P.market_meta_data_service << "\n"
<< "market_data_service: " << P.market_data_service << "\n"
<< "market_data_service_key: " << P.market_data_service_key << "\n";
}
string Preferences::default_file_name ()
{ return getenv ("HOME") + string {"/.config/trader-desk.conf"}; }
Preferences Preferences::from_default_file ()
{ return from_file (default_file_name ()); }
Preferences Preferences::from_file (const string& file)
{
Preferences ret
{[&file] { if (ifstream F {file}; F.good ())
return from_file (move (F));
return dump (defaults (), file); } () };
ret.file_path = file;
return ret;
}
static string read_line (istream& I)
{
static regex R {" *[^:\n]*: *(.*)$"};
smatch M;
string line;
while (getline (I, line), I.good ())
if (regex_match (line, M, R)) return M [1];
return {};
}
Preferences Preferences::from_file (istream&& I)
{
Preferences ret;
ret.trade_cost_offset = strtod (read_line (I).data (), nullptr);
ret.trade_cost_factor = strtod (read_line (I).data (), nullptr);
ret.database_host = read_line (I);
ret.database_user = read_line (I);
ret.database_password = read_line (I);
ret.database_instance = read_line (I);
ret.database_socket = read_line (I);
ret.database_port = atoi (read_line (I).data ());
ret.market_meta_data_service = read_line (I);
ret.market_data_service = read_line (I);
ret.market_data_service_key = read_line (I);
return ret;
}
bool database_equal (const Preferences& A, const Preferences& B)
{
return A.database_host == B.database_host
&& A.database_user == B.database_user
&& A.database_password == B.database_password
&& A.database_instance == B.database_instance
&& A.database_socket == B.database_socket
&& A.database_port == B.database_port;
}
static void commit_prefs (Preferences_Dialog& D, Preferences& P)
{
/* Pounds to pence. */
P.trade_cost_offset = D.trade_cost_offset.get_value () * 100;
/* Percent to fraction. */
P.trade_cost_factor = D.trade_cost_factor.get_value () / 100.0;
P.database_host = D.database_host.get_text ();
P.database_user = D.database_user.get_text ();
P.database_password = D.database_password.get_text ();
P.database_instance = D.database_instance.get_text ();
P.database_port = atoi (D.database_port.get_text ().data ());
P.database_socket = D.database_socket.get_text ();
P.market_meta_data_service = D.market_meta_data_service.get_text ();
P.market_data_service = D.market_data_service.get_text ();
P.market_data_service_key = D.market_data_service_key.get_text ();
dump (P);
}
static void set_margins (Gtk::Widget *const W, const int size)
{
W->set_margin_start (size);
W->set_margin_end (size);
W->set_margin_top (size);
W->set_margin_bottom (size);
}
Preferences_Dialog::Preferences_Dialog
(Gtk::Window &parent,
Preferences& P,
const string& alpha_vantage__message)
: Gtk::Dialog (pgettext ("Window title", "Trader-Desk: Preferences"),
parent),
preferences {P}
{
get_vbox ()->set_spacing (20);
/************* Trade costs **************************/
trade_cost_offset.set_increments (1.0, 10.0);
trade_cost_offset.set_range (0.0, 1000.0);
/* Pence to pounds. */
trade_cost_offset.set_value (preferences.trade_cost_offset / 100);
trade_cost_factor.set_increments (1.0, 10.0);
trade_cost_factor.set_range (0.0, 100.0);
/* Fraction to percent. */
trade_cost_factor.set_value (preferences.trade_cost_factor * 100.0);
{
auto *const frame { Gtk::make_managed<Gtk::Frame>
(pgettext ("Label", "Trading costs"))};
get_vbox ()->pack_start (*frame, Gtk::PACK_SHRINK);
auto *const grid {Gtk::make_managed<Gtk::Grid> ()};
frame->add (*grid);
grid->set_column_spacing (20);
set_margins (grid, 10);
grid->set_margin_left (100);
auto grid_line
{ [grid] (const int row, const string& label,
Gtk::Widget& control, const string& units)
{
grid->attach (*Gtk::make_managed<Gtk::Label> (label, Gtk::ALIGN_END),
0, row);
grid->attach (control, 1, row);
grid->attach (*Gtk::make_managed<Gtk::Label>
(units, Gtk::ALIGN_START),
2, row);
}
};
grid_line (0, pgettext ("Label", "Fixed cost of trading"),
trade_cost_offset,
pgettext ("Units:Worded:Monetary", "pounds"));
grid_line (1, pgettext ("Label", "Proportional cost of trading"),
trade_cost_factor,
pgettext ("Units:Worded", "percent"));
}
Gtk::Grid *const database_ {Gtk::make_managed<Gtk::Grid> ()};
{
Gtk::Frame *const df {Gtk::make_managed<Gtk::Frame>
(pgettext ("Label", "Local database"))};
get_vbox ()->pack_start (*df, Gtk::PACK_SHRINK);
df->add (*database_);
database_->set_column_spacing (20);
set_margins (database_, 10);
database_->set_margin_left (100);
}
auto create_text_input
{
[this, &D = *database_]
(const int row, const string& label, Gtk::Entry& E,
const string& initial_value)
{
D.attach (*Gtk::make_managed<Gtk::Label> (label, Gtk::ALIGN_END),
0, row);
D.attach (E, 1, row);
E.set_text (initial_value);
E.set_input_purpose (Gtk::INPUT_PURPOSE_FREE_FORM);
E.set_hexpand (1);
}
};
create_text_input (0, pgettext ("Label", "Database host"),
database_host, preferences.database_host);
create_text_input (1, pgettext ("Label", "Database user"),
database_user, preferences.database_user);
create_text_input (2, pgettext ("Label", "Database password"),
database_password, preferences.database_password);
database_password.set_input_purpose (Gtk::INPUT_PURPOSE_PASSWORD);
database_password.set_visibility (0);
database_password.set_invisible_char ('*');
create_text_input (3, pgettext ("Label", "Database instance"),
database_instance, preferences.database_instance);
database_->attach (*Gtk::make_managed<Gtk::Label>
(pgettext ("Label", "Database port"), Gtk::ALIGN_END),
0, 4);
database_port.set_text (to_string (preferences.database_port));
database_port.set_input_purpose (Gtk::INPUT_PURPOSE_DIGITS);
database_->attach (database_port, 1, 4);
create_text_input (5, pgettext ("Label", "Database socket"),
database_socket, preferences.database_socket);
Gtk::Grid *const servers_ {Gtk::make_managed<Gtk::Grid> ()};
{
Gtk::Frame *const F {Gtk::make_managed<Gtk::Frame>
(pgettext ("Label", "Internet services"))};
get_vbox ()->pack_start (*F, Gtk::PACK_SHRINK);
F->add (*servers_);
servers_->set_column_spacing (20);
set_margins (servers_, 10);
servers_->set_margin_left (100);
}
servers_->attach (*Gtk::make_managed<Gtk::Label>
(pgettext ("Label", "Market meta-data service"),
Gtk::ALIGN_END),
0, 0);
market_meta_data_service.set_hexpand (1);
market_meta_data_service.set_text (preferences.market_meta_data_service);
market_meta_data_service.set_width_chars
(max<int> (preferences.market_meta_data_service.length (), 40));
servers_->attach (market_meta_data_service, 1, 0);
servers_->attach (*Gtk::make_managed<Gtk::Label>
(pgettext ("Label", "Market data service"),
Gtk::ALIGN_END),
0, 1);
market_data_service.set_hexpand (1);
market_data_service.set_text (preferences.market_data_service);
servers_->attach (market_data_service, 1, 1);
servers_->attach (*Gtk::make_managed<Gtk::Label>
(pgettext ("Label", "Market data service API key"),
Gtk::ALIGN_END),
0, 2);
market_data_service_key.set_hexpand (1);
market_data_service_key.set_input_purpose (Gtk::INPUT_PURPOSE_PASSWORD);
market_data_service_key.set_visibility (0);
market_data_service_key.set_invisible_char ('*');
market_data_service_key.set_text (preferences.market_data_service_key);
servers_->attach (market_data_service_key, 1, 2);
if (alpha_vantage__message.length ())
{
Gtk::Frame *const F {Gtk::make_managed<Gtk::Frame>
("Message from AlphaVantage service:")};
servers_->attach (*F, 0, 3, 2, 1);
F->set_margin_top (18);
Gtk::TextView *const T {Gtk::make_managed<Gtk::TextView> ()};
F->add (*T);
T->set_wrap_mode (Gtk::WRAP_WORD);
T->get_buffer ()
->insert_markup (T->get_buffer ()->end (),
"<span foreground=\"#aa0000\">"
+ alpha_vantage__message + "</span>");
}
/* The action buttons. */
{
Gtk::HBox *const H {Gtk::make_managed<Gtk::HBox> ()};
get_vbox ()->pack_start (*H, Gtk::PACK_SHRINK);
Gtk::Button *const B1 {Gtk::make_managed<Gtk::Button>
(pgettext ("Label", "Commit changes"))};
H->pack_start (*B1, Gtk::PACK_EXPAND_PADDING);
B1->signal_clicked ()
.connect ([this] { commit_prefs (*this, this->preferences);
close (); });
Gtk::Button *const B2 {Gtk::make_managed<Gtk::Button>
(pgettext ("Label", "Cancel"))};
H->pack_start (*B2, Gtk::PACK_EXPAND_PADDING);
B2 -> signal_clicked () . connect ([this] { close (); });
}
show_all ();
}
} /* End of namespace DMBCS::Trader_Desk. */

136
trader-desk/preferences.h Normal file
View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__PREFERENCES__H
#define DMBCS__TRADER_DESK__PREFERENCES__H
#include <gtkmm.h>
#include <build-aux/gettext.h>
/** \file
*
* Declaration of the \c Preferences and \c Preferences_Dialog
* classes. */
namespace DMBCS::Trader_Desk {
using namespace std;
/** There are a number of parameters which affect the whole of the
* trader-desk application, and this flat structure simply contains
* those parameters. */
struct Preferences
{
double trade_cost_offset;
double trade_cost_factor;
/* MariaDB connection parameters. */
string database_host;
string database_user;
string database_password;
string database_instance;
string database_socket;
uint16_t database_port;
/* The RDMP HTTP end-point. */
string market_meta_data_service;
/* The AlphaVantage HTTP end-point... */
string market_data_service;
/* ... and private key. */
string market_data_service_key;
/* Not really a user preference at this time. */
static constexpr const int time_horizon {10};
/* If set, the file from whence these parameters came. */
string file_path {};
static string default_file_name ();
static Preferences from_file (istream&&);
static Preferences from_file (const string& file_path);
static Preferences from_default_file ();
static Preferences defaults ();
};
void dump (const Preferences&, ostream&&);
void dump (const Preferences&, const string& file_name);
void dump (const Preferences&);
bool database_equal (const Preferences&, const Preferences&);
/** There are a number of parameters which affect the whole of the
* trader-desk application, and this dialog box is designed to let the
* user edit those global parameters from the Preferences item on the
* menu.
*
* From the applicationʼs point of view use of this object is very
* simple: use the sole constructor to instantiate and then call the \c
* run method. The dialog will take over all GUI operations until it
* is closed, and in the meantime will look after itself.
*
* In operation the dialog will directly modify the static data in \c
* Position_Analyzer and update the database as soon as the user makes
* a change to any entry. */
struct Preferences_Dialog : Gtk::Dialog
{
Preferences& preferences;
Gtk::SpinButton trade_cost_offset {1.0, 2};
Gtk::SpinButton trade_cost_factor {1.0, 2};
/* MariaDB connection parameters. */
Gtk::Entry database_host;
Gtk::Entry database_user;
Gtk::Entry database_password;
Gtk::Entry database_instance;
Gtk::Entry database_port;
Gtk::Entry database_socket;
/* The RDMP HTTP end-point. */
Gtk::Entry market_meta_data_service;
/* The AlphaVantage HTTP end-point... */
Gtk::Entry market_data_service;
/* ... and private key. */
Gtk::Entry market_data_service_key;
Preferences_Dialog (Gtk::Window &, Preferences&,
const string& alpha_vantage_message = {});
} ; /* End of class Preferences. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__PREFERENCES__H. */

121
trader-desk/scale.cc Normal file
View file

@ -0,0 +1,121 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/scale.h>
/** \file
*
* Implementation of the \c Scale and \c Exponential_Scale classes. */
namespace DMBCS::Trader_Desk {
Scale::Scale (Chart_Data &d,
string const &label_,
Gdk::RGBA const normal,
Gdk::RGBA const active,
Gdk::RGBA const prelight,
double const min,
double const max,
double const initial_value)
: data (d),
label_format (label_),
scale (min, max, (max-min)/100)
{
label.set_angle (90.0);
pack_start (label, Gtk::PACK_SHRINK, 0);
pack_start (scale, Gtk::PACK_SHRINK, 0);
scale.get_adjustment () -> set_value (initial_value);
scale.set_draw_value (0);
scale.override_background_color (normal, Gtk::STATE_FLAG_NORMAL);
scale.override_background_color (active, Gtk::STATE_FLAG_ACTIVE);
scale.override_background_color (prelight, Gtk::STATE_FLAG_PRELIGHT);
scale . signal_value_changed ()
. connect ([this] { on_slider_changed (); });
data . changed_signal
. connect ([this] { on_data_changed (); });
}
void Scale::setup_label ()
{
char buffer [label_format.length () + 20];
sprintf (buffer, label_format.c_str (), value ()->get_value ());
label.set_text (buffer);
}
struct Exponential_Mapping
{
double a, b, c;
explicit Exponential_Mapping (Exponential_Scale const &e)
{
c = e.value_adjustment->get_lower ();
double const l = e.mid_value - c;
double const L = e.value_adjustment->get_upper () - c;
a = sq (L / l - 1);
b = l * l / (L - 2 * l);
}
double to_linear (double const &x) const
{ return log ((x - c) / b + 1) / log (a); }
double from_linear (double const &x) const
{ return c + b * (pow (a, x) - 1); }
static constexpr double sq (double const &x) {return x*x;}
};
void Exponential_Scale::on_slider_changed ()
{
value_adjustment
->set_value (Exponential_Mapping {*this}
.from_linear (scale.get_adjustment ()->get_value ()));
value_adjustment->value_changed ();
}
void Exponential_Scale::set_value (double const x)
{
scale.get_adjustment ()
-> set_value (Exponential_Mapping {*this} . to_linear (x));
value_adjustment->set_value (x);
}
} /* End of namespace DMBCS::Trader_Desk. */

218
trader-desk/scale.h Normal file
View file

@ -0,0 +1,218 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__SCALE__H
#define DMBCS__TRADER_DESK__SCALE__H
#include <trader-desk/chart-data.h>
#include <gtkmm.h>
/** \file
*
* Declaration of \c Scale, \c Linear_Scale, and \c Exponential_Scale
* classes. */
namespace DMBCS::Trader_Desk {
/** A \c Scale is a prototypical vertical slider control which gets
* stacked to the right of a detail (\c Hand_Analysis_Widget) chart
* allowing the user some control over the display of information on
* top of that chart.
*
* Note that the rest of the program interacts with these objects via
* the \c Adjustments, in particular listening for the \c value_changed
* signals on those objects. */
class Scale : public Gtk::HBox
{
protected:
/** The data which the user is currently observing, which should be
* regarded as a read-only object which may influence the operation
* of the \c Scale slider (although the--extremely special--\c
* Date_Range_Scale does cause changes to happen in the \c data). */
Chart_Data &data;
/** A graphical component of the \c Scale widget: displays an
* identifying text string according to the \c label_format. */
Gtk::Label label;
/** A \c printf-style format string which may include a place-holder
* for a floating-point value; this is used as the identifying label
* on the slider and will display the slider's current value instead
* of the placeholder. */
string const label_format;
/** The actual GTK widget which implements our \c Scale. */
Gtk::VScale scale;
/** Called internally whenever the user moves the slider on the
* screen. This is the place to transform the slider's internal \c
* Adjustment into the adjustment we present to the application. */
virtual void on_slider_changed () { setup_label (); }
/** Called when an externally generated signal fires on the \c data
* object to indicate that some change has taken place in those
* data. */
virtual void on_data_changed () {}
public:
/** The sole constructor, which provides a fully populated object
* ready for operations (though often the range of values covered by
* the slider is not known until later). */
Scale (Chart_Data &d,
string const &label_format,
Gdk::RGBA const normal,
Gdk::RGBA const active,
Gdk::RGBA const prelight,
double const min, double const max,
double const initial_value = -1.0);
/* The underlying GTK widget machinery severely restricts any hope we
* may have of copying and moving this type of object. */
/** Obligatory destructor for a pure virtual interface. */
virtual ~Scale () = default;
/** Provide an adjustment. This may be the actual \c Adjustment under
* control of the slider, but sometimes it is another object managed
* by a derived class which apparently provides a more sophisticated
* level of control. */
virtual Glib::RefPtr<Gtk::Adjustment> value () = 0;
/* /\** Ditto above. *\/ */
virtual Glib::RefPtr<const Gtk::Adjustment> value () const = 0;
/** Apply the `value' of the slider to the \c label_format string and
* put the result as the text displayed by the \c label. */
void setup_label ();
}; /* End of class Scale. */
/** Most basic concrete \c Scale, basically a wrapper around the base
* class which returns the slider's own (directly controlled)
* adjustment as the value given to the user. Even though this is a
* concrete type, it should only be used as the base of a more
* specialized \c Scale which represents some useful quantity. */
struct Linear_Scale : Scale
{
/** Sole constructor which creates a fully populated object. */
Linear_Scale (Chart_Data &d,
string const &label_,
Gdk::RGBA const &normal,
Gdk::RGBA const &active,
Gdk::RGBA const &prelight,
double const min, double const max,
double const initial_value)
: Scale (d, label_, normal, active, prelight, min, max, initial_value)
{
setup_label ();
}
/** Simply return the slider's directly-controlled adjustment
* object. */
Glib::RefPtr<Gtk::Adjustment> value () override { return scale.get_adjustment (); }
/* /\** Ditto. *\/ */
Glib::RefPtr<const Gtk::Adjustment> value () const override
{ return scale.get_adjustment (); }
};
/** A \c Scale which is exponential (or logarithmic, depending on which
* way you like to look at things), allowing for higher values to be
* more tightly packed together.
*
* The scale is parameterized at user level by the values of the
* end-points and (linear) mid-point, and in all other respects acts
* just like any other \C Scale object would with the exception that
* new values are assigned through the \c set_value method; the
* application should *not* try to set the value directly in the
* adjustment which is given out.
*
* Although this is a concrete class, it is anticipated that the
* application will only see further specializations derived from this
* one. */
struct Exponential_Scale : Scale
{
/** The value at the linear mid-point of the scale. */
double mid_value;
/** The \c Adjustment which we present to the user, exhibiting the
* exponentially varying values. */
Glib::RefPtr<Gtk::Adjustment> value_adjustment;
/** When the user moves the graphical slider. */
void on_slider_changed () override;
/** The sole constructor, which fully specifies an \c
* Exponential_Scale object. */
Exponential_Scale (Chart_Data &d,
string const &label,
Gdk::RGBA const normal,
Gdk::RGBA const active,
Gdk::RGBA const prelight,
double const lower, double const mid, double const upper,
double const initial_value)
: Scale (d, label, normal, active, prelight, 0.0, 1.0),
mid_value (mid),
value_adjustment {Gtk::Adjustment::create (mid, lower, upper)}
{
value_adjustment -> signal_value_changed ()
. connect ([this] { setup_label (); });
set_value (initial_value);
}
/** The means by which the application imposes a value on the
* slider. */
void set_value (double const x);
/** Give the logical, i.e. not the linear, value to the user. */
Glib::RefPtr<Gtk::Adjustment> value () override { return value_adjustment; }
/* /\** Ditto, but for \c const objects. *\/ */
Glib::RefPtr<const Gtk::Adjustment> value () const override
{ return value_adjustment; }
}; /* End of class Exponential_Scale. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__SCALE__H. */

View file

@ -0,0 +1,174 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <numeric>
#include <trader-desk/sd-envelope-analyzer.h>
/** \file
*
* Implementation of the \c SD_Envelope_Analyzer class. */
namespace DMBCS::Trader_Desk {
SD_Envelope_Analyzer::SD_Envelope_Analyzer (Chart_Data &cd)
: moving_average {cd}
{
moving_average . signal_redraw_needed ()
. connect ([this] { data_changed (); });
}
vector <Gtk::Widget*> SD_Envelope_Analyzer::make_control_widgets ()
{
vector <Gtk::Widget*> ret = moving_average.make_control_widgets ();
Scale *const s = new Scale {moving_average.chart_data,
envelope_width};
s -> value () -> signal_value_changed ()
. connect ([this, s] { control_moved (s); } );
ret.emplace_back (s);
return ret;
}
template <typename X> inline X sq (X const &x) { return x*x; }
static Currency_Value standard_deviation_ (Time_Series const &t,
Time_Series const &mean,
Time_Point const &earliest_time)
{
if (mean.size () < 2)
return 0.0;
auto const end_ = std::upper_bound
(begin (mean), end (mean),
earliest_time,
[] (Time_Point const &val,
Time_Series::value_type const &a)
{ return a.time < val; });
return std::sqrt (std::inner_product (begin (mean), end_,
begin (t),
Currency_Value {0.0},
plus<Currency_Value> {},
[] (Event const &a, Event const &b)
{ return sq (a.price - b.price); })
/ (mean.size () - 1));
}
void SD_Envelope_Analyzer::data_changed ()
{
{
lock_guard<mutex> l {moving_average.chart_data.prices_mutex};
standard_deviation
= standard_deviation_ (moving_average.chart_data.prices,
moving_average.mean_series,
moving_average.chart_data.extremes.start_time);
}
redraw_needed_.emit ();
}
void SD_Envelope_Analyzer::control_moved (Scale const *const scale)
{
if (abs (envelope_width - scale->value ()->get_value ()) > 1.0e-3)
{
envelope_width = scale->value ()->get_value ();
redraw_needed_.emit ();
}
}
void SD_Envelope_Analyzer::stretch_outline (Time_Series::Range &outline)
{
auto const range = moving_average.mean_series.get_range ();
auto const margin = (outline.max_value - outline.min_value) * 0.05;
outline.max_value = max (outline.max_value,
range.max_value
+ envelope_width * standard_deviation
+ margin);
outline.min_value = min (outline.min_value,
range.min_value
- envelope_width * standard_deviation
- margin);
}
void SD_Envelope_Analyzer::graph_draw_hook
(Chart_Context &context,
Tide_Mark::List &marks,
unsigned number_shares,
vector <Tide_Mark::Price_Marker> const &markers)
{
if (moving_average.mean_series.empty ())
return;
context.set_source_rgb (Colour::SD_ENVELOPE);
context.move_to (moving_average.mean_series.front ());
auto const envelope = envelope_width * standard_deviation;
auto i = begin (moving_average.mean_series);
for (;
i != end (moving_average.mean_series)
&& i->time >= context.outline.start_time;
++i)
context.line_to ({i->time, i->price + envelope});
for (--i; i >= begin (moving_average.mean_series); --i)
context.line_to ({i->time, i->price - envelope});
context.cairo->fill ();
for (auto const &t : markers)
{
auto const mean
= moving_average.mean_series
.interpolated_value (t (0.0, Colour::MEAN_TIDE).time);
marks.emplace_back (t (mean - envelope, Colour::ENVELOPE_TIDES));
marks.emplace_back (t (mean + envelope, Colour::ENVELOPE_TIDES));
}
moving_average . graph_draw_hook (context, marks, number_shares, markers);
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,127 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__SD_ENVELOPE_ANALYZER__H
#define DMBCS__TRADER_DESK__SD_ENVELOPE_ANALYZER__H
#include <trader-desk/moving-average-analyzer.h>
/** \file
*
* Declaration of the \c SD_Envelope_Analyzer class. */
namespace DMBCS::Trader_Desk {
/** A chart analyzer which puts an envelope around the prices chart
* related to the standard deviation (SD) of the prices data around
* their mean: any proportion of this SD can be chosen by the user
* acting on a slider (\c Scale).
*
* This analyzer incorporates a \c Moving_Average_Analyzer within it,
* and provides a composite control to the application which allows for
* user adjustment of both the averaging window and relative width (as
* proportion of SD) of the envelope. */
class SD_Envelope_Analyzer : public Analyzer
{
/** Simple linear scale which allows the user to choose an envelope
* width relative to SD between 0.1 and 10.0. */
struct Scale : Linear_Scale
{
Scale (Chart_Data &chart_data, double const &initial_value)
: Linear_Scale (chart_data,
pgettext ("Label Abbrev:Standard deviation",
"Envelope width = %.2f x std. dev."),
Gdk::RGBA {"#dddd00"},
Gdk::RGBA {"#ffffaa"},
Gdk::RGBA {"#ffff66"},
0.1, 10.0, initial_value)
{}
};
/** The moving average object which we encapsulate (and leverage for
* our own functioning). */
Moving_Average_Analyzer moving_average;
/** The width of the envelope as a fraction of the \c
* standard_deviation. */
double envelope_width {2};
/** The current standard deviation of the data in the prices
* time-series about the current mean time-series. */
double standard_deviation {0};
/** We emit this signal whenever we re-compute the \c
* standard_deviation. */
sigc::signal<void> redraw_needed_;
/** Called when the \a scale slider indicates that the user has
* changed the position. */
void control_moved (Scale const *const);
/** Called when the \c moving_average object signals that the mean
* time-series has just been re-computed. */
void data_changed ();
public:
/** Sole constructor which sets up a fully populated and operational
* object. */
explicit SD_Envelope_Analyzer (Chart_Data &cd);
/* Analyzer interface. */
/** Return a (newly allocated) composite object which includes sliders
* for ourself and the \c moving_average. */
vector <Gtk::Widget*> make_control_widgets () override;
/** Make sure the vertical (price) limits of the \a Range are wide
* enough to display the full envelope. */
void stretch_outline (Time_Series::Range &) override;
/** Display the envelope and \c moving_average chart. */
void graph_draw_hook (Chart_Context &context,
Tide_Mark::List &,
unsigned number_shares,
vector <Tide_Mark::Price_Marker> const &) override;
/** Return the signal object on which we emit when anything
* changes. */
sigc::signal<void> &signal_redraw_needed () override
{ return redraw_needed_; }
}; /* End of class SD_Envelope_Analyzer. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__SD_ENVELOPE_ANALYZER__H. */

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/shares-scale.h>
#include <iomanip>
namespace DMBCS::Trader_Desk {
Shares_Scale::Shares_Scale (Chart_Data &d)
: Exponential_Scale (d,
pgettext ("Label",
"Number of shares = %.0f"),
Gdk::RGBA ("#00dd00"),
Gdk::RGBA ("#aaffaa"),
Gdk::RGBA ("#66ff66"),
1, 1000, 10000,
1)
{
d.changed_signal . connect ([this] { on_data_changed (); });
value_adjustment -> signal_value_changed ()
. connect ([this] { on_value_changed (); });
}
void Shares_Scale::on_data_changed ()
{
if (data.open_position.price < 0)
scale.show ();
else
scale.hide ();
if (data.number_shares != unsigned (value ()->get_value ()))
{
set_value (data.number_shares);
scale.get_adjustment () -> value_changed ();
}
}
void Shares_Scale::on_value_changed ()
{
if (data.number_shares != (unsigned) (value ()->get_value () + 0.5))
{
data.number_shares = (unsigned) (value ()->get_value () + 0.5);
data.update_extreme_prices ();
data.changed_signal.emit ();
}
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__SHARES_SCALE__H
#define DMBCS__TRADER_DESK__SHARES_SCALE__H
#include <trader-desk/scale.h>
/** \file
*
* Declaration of the \c Shares_Scale class. */
namespace DMBCS::Trader_Desk {
/** Basic example of an \c Exponential_Scale which sets itself according
* to the \c Chart_Data, and conversely applies user input from the
* graphical slider to the \c Chart_Data. */
class Shares_Scale : public Exponential_Scale
{
/** Called when the user pushes the slider about. */
void on_value_changed ();
/** Called when the \c Chart_Data change. */
void on_data_changed () override;
public:
/** Sole constructor which registers the \a Chart_Data which we watch
* over. */
Shares_Scale (Chart_Data &d);
}; /* End of class Shares_Scale. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__SHARES_SCALE__H. */

193
trader-desk/text.cc Normal file
View file

@ -0,0 +1,193 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/text.h>
#include <limits>
namespace DMBCS::Trader_Desk {
inline double clamp (double const &x, double const &l, double const &h)
{
return max (min (x, h), l);
}
static double boundary_stress (Text::Item const &i,
Text::Boundary const &b,
double const &critical)
{
double const ret
= i.size.y * (clamp (i.placement.x + i.size.x - b.right,
0.0, i.size.x)
+ clamp (b.left - i.placement.x, 0.0, i.size.x))
+ i.size.x * (clamp (b.top - i.placement.y, 0.0, i.size.y)
+ clamp (i.placement.y + i.size.y - b.bottom,
0.0, i.size.y));
return ret > -numeric_limits<double>::min () ? 2 * ret + critical
: 0.0;
}
static float overlap (Text::Item const &a, Text::Item const &b,
float const &critical_value)
{
static double const pad = 6.0;
if (a.placement.x >= b.placement.x + b.size.x + pad
|| a.placement.x + a.size.x <= b.placement.x - pad
|| a.placement.y >= b.placement.y + b.size.y
|| a.placement.y + a.size.y <= b.placement.y)
return 0;
auto const right = min (a.placement.x + a.size.x,
b.placement.x + b.size.x) + pad;
auto const left = max (a.placement.x, b.placement.x) - pad;
auto const top = max (a.placement.y, b.placement.y);
auto const bottom = min (a.placement.y + a.size.y,
b.placement.y + b.size.y);
return critical_value + sqrt ((right - left) * (bottom - top));
}
inline double squash (double const &x)
{
return 1.0 - exp (-x / 100.0);
}
inline double tension (Text::Item const &i)
{
return squash (hypot (i.native_position.x - i.placement.x,
i.native_position.y - i.placement.y));
}
inline void bring_inside (Text::Item &i, Text::Boundary const &b)
{
i.placement = {clamp (i.placement.x, b.left, b.right - i.size.x),
clamp (i.placement.y, b.top, b.bottom - i.size.y)};
}
double Text::compute_tension (double const &critical_value,
Boundary const &boundary) const
{
double acc {0.0};
for (auto a = begin (items); a != end (items); ++a)
{
for (auto b = a + 1; b != end (items); ++b)
acc += overlap (*a, *b, critical_value);
acc += tension (*a) + boundary_stress (*a, boundary, critical_value);
}
return acc;
}
void Text::shuffle (double shift_size,
double const &shift_limit,
Boundary const &boundary)
{
if (items.empty ()) return;
for (; shift_size > shift_limit; shift_size /= 2.0)
{
for (auto &i : items)
bring_inside (i, boundary);
auto const acceptable_limit = items.size ();
auto initial_tension = compute_tension (acceptable_limit, boundary);
for (;;)
{
Item *hot_item {nullptr};
enum { up, down, left, right} best_direction {up};
double best_tension = numeric_limits<double>::max ();
{
auto test = [this, acceptable_limit, &best_tension,
&hot_item, &best_direction, boundary]
(Item &i,
Place const &shift,
decltype (best_direction) const &direction)
{
i.placement += shift;
auto const tension = compute_tension (acceptable_limit,
boundary);
if (tension < best_tension)
{
hot_item = &i;
best_direction = direction;
best_tension = tension;
}
};
for (auto &i : items)
{
test (i, Place {shift_size, 0}, right);
test (i, Place {- 2 * shift_size, 0}, left );
test (i, Place {shift_size, shift_size}, up );
test (i, Place {0, - 2 * shift_size}, down );
i.placement += Place {0, shift_size};
}
}
if (! hot_item) return;
switch (best_direction)
{
case up: hot_item->placement += {0, shift_size}; break;
case down: hot_item->placement += {0, -shift_size}; break;
case left: hot_item->placement += {-shift_size, 0}; break;
case right: hot_item->placement += {shift_size, 0}; break;
}
/* Has the tension decreased? If not we should give up,
* regardless of the acceptable limit. */
if (initial_tension - best_tension
< numeric_limits<decltype(initial_tension)>::epsilon ())
break;
if ((initial_tension = best_tension) < acceptable_limit)
break;
} /* Loop back for further improvements. */
} /* End of loop over shift_size. */
} /* End of shuffle function. */
} /* End of namespace DMBCS::Trader_Desk. */

167
trader-desk/text.h Normal file
View file

@ -0,0 +1,167 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__TEXT__H
#define DMBCS__TRADER_DESK__TEXT__H
#include <algorithm>
#include <cmath>
#include <memory>
#include <string>
#include <vector>
#include <trader-desk/colour.h>
/** \file
*
* Declaration of the \c Text class. */
namespace DMBCS::Trader_Desk {
/** The primary purpose of this class is to arrange short items of
* text in a rectangle of the screen in a way that avoids overlaps
* and keeps the items as close to their intended positions as
* possible.
*
* Thus the public interface is nice and simple: a method to add text
* items, \c operator+=, and a method to cause those items to be
* arranged optimally, \c arrange.
*
* The strategy is to move all items so that they fall inside the
* screen rectangle, and then for increasingly small deltas determine
* which movement, in any of the four cardinal directions, of which
* item leads to the best improvement in score: the total amount of
* area overlap of all the items plus a small number times the sum of
* the itemsʼ displacement from their starting values--we want to get
* rid of the overlaps at the expense of the displacements. The
* exercise continues until the overlaps are eliminated and delta is
* not more than the size of a pixel. */
struct Text
{
/** A structure to carry the details of a rectangular region on the
* screen. */
struct Boundary { double left, right, top, bottom; };
/** A structure to carry coordinates of a point on the screen, with
* some basic arithmetic where it serves our needs in the
* implementation of the \c Text class. */
struct Place
{
double x {0.0};
double y {0.0};
Place () = default;
Place (double const &x_, double const &y_) : x (x_), y (y_) {}
Place &operator+= (Place const &a)
{ x += a.x; y += a.y; return *this; }
Place &operator-= (Place const &a)
{ x -= a.x; y -= a.y; return *this; }
};
/** An item of text to be placed on the screen. */
struct Item
{
/** The actual text string. */
string text;
/** The colour in which the text should be rendered (not actually
* needed in this class, but we carry the information along for
* the convenience of the application). */
Colour colour;
/** The place where we would like the text to appear. */
Place native_position;
/** The size of the box which encloses the text. */
Place size;
/** The computed place on the screen where the text should be
* printed so that it does not overlap with any other of the
* items. */
Place placement;
/** Sole class constructor creates a fully populated object,
* except for the \c placement member which is set by subsequent
* optimization of a set of objects relative to each other. */
Item (string const &t, Colour const &c, Place const &p,
Place const &s)
: text {t}, colour {c}, native_position {p}, size {s},
placement {p}
{}
}; /* End of class Item. */
private:
/* The actual collection of text items we accumulate and
* arrange. */
vector<Item> items;
/* Compute a score, to be minimized, for the current arrangement of
* text \c Item \C placements. */
double compute_tension (double const &critical_value,
Boundary const &boundary) const;
/* Randomly move some items of text around. */
void shuffle (double shift_size,
double const &shift_limit,
Boundary const &boundary);
public:
/** Return a vector of items whose \c placement's indicate the
* optimal positions to render each text item. */
vector<Item> const &arrange (Boundary const &b)
{
for (auto &i : items) i.placement = i.native_position;
shuffle (10.0, 0.5, b);
return items;
}
/** Add a new text item to the list of items that need to be
* optimally arranged. */
Text &operator+= (Item &&d) { items.emplace_back (move (d));
return *this; }
} ; /* End of class Text. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__TEXT__H. */

112
trader-desk/tide-mark.h Normal file
View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__TIDE_MARK__H
#define DMBCS__TRADER_DESK__TIDE_MARK__H
#include <trader-desk/colour.h>
#include <trader-desk/time-series.h>
/** \file
*
* Definition and complete inline implementation of the \c Tide_Mark
* class. */
namespace DMBCS::Trader_Desk {
/** A \c Tide_Mark is simply a point in (time, price)-space (i.e. an \c
* Event) through which we draw horizontal and vertical cross-hairs.
* The main requirement is to be able to pass a list of interesting
* points in time around the program--the current mouse position,
* current time, position-entry time--and then have each sub-system
* furnish a set of price marks; this is the purpose of the \c
* Price_Marker function type which is an object with time bound and
* which takes a price as an argument, with the end product being an
* actual \c Tide_Mark at that (time, price) point.
*
* Note further how the horizontal and vertical lines have distinctive
* colours. */
struct Tide_Mark : Event
{
/** The container type we (the application) use for containers of \c
* Tide_Mark's. */
typedef vector<Tide_Mark> List;
/** The type of a function which manufactures a \c Tide_Mark using
* intrinsically predefined values for the time and time-tide
* colour. */
typedef function <Tide_Mark (Currency_Value const &, Colour const &)>
Price_Marker;
/** Colour of price-wise tide-line. */
Colour value_colour;
/** Colour of time-wise tide-line. */
Colour temporal_colour;
private:
/* Construction must be done via the \c tide_mark function below.
*
* Rather than constructing these objects in one go, applications
* must first use \c price_marker() to manufacture us a function,
* which in turn manufactures a \c Tide_Mark. */
constexpr Tide_Mark (Event const &e, Colour const &v, Colour const &t)
: Event {e}, value_colour {v}, temporal_colour {t}
{}
/* No reason to make copies, so guard against accidentally doing
* so. */
Tide_Mark (Tide_Mark const &) = delete;
Tide_Mark &operator= (Tide_Mark const &) = delete;
public:
/* Moves are necessary as these items are stored in \c vector's. */
Tide_Mark (Tide_Mark &&) = default;
Tide_Mark &operator= (Tide_Mark &&) = default;
/** A factory method which produces a \c Price_Marker, which in turn
* produces \c Tide_Marks with event time and time-tide colours fixed
* at \a time and \a time_colour, respectively. */
static Price_Marker price_marker (Time_Point const &time,
Colour const &time_colour)
{
return [time, time_colour] (Currency_Value const &v, Colour const &c)
{ return Tide_Mark {{time, v}, c, time_colour}; };
}
} ; /* End of class Tide_Mark. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__TIDE_MARK__H. */

294
trader-desk/time-series.cc Normal file
View file

@ -0,0 +1,294 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/time-series.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
/** \file
*
* Implementation of the \c Time_Series class. */
namespace DMBCS::Trader_Desk {
Time_Point const TODAY_MARK
{ [] {
auto round_days = [] (time_t const &t)
{
struct tm tm; localtime_r (&t, &tm);
tm.tm_sec = 0;
tm.tm_min = 0;
tm.tm_hour = 0;
return mktime (&tm);
};
return chrono::system_clock::from_time_t
(round_days
(chrono::system_clock::to_time_t
(chrono::system_clock::now ())));
} ()
};
inline Duration::rep T (Time_Point const &t)
{
return number<chrono::seconds> (t.time_since_epoch ());
}
void Time_Series::insert_event (Event const &e)
{
const auto t {find_if (begin (), end (), [t = e.time] (const Event& a)
{ return a.time < t; })};
if (t == end ()) emplace_back (e);
else insert (t, e);
}
void Time_Series::extend_range (DB &db,
int const seqid,
Duration const &window_size)
{
auto const earliest_date = (empty () ? TODAY_MARK : front ().time)
- window_size;
auto const last_time = empty () ? TODAY_MARK
: back ().time - chrono::seconds (1);
if (earliest_date <= last_time)
{
auto const x = from_database (db,
seqid,
last_time,
last_time - earliest_date,
market_close_time);
insert (end (), std::begin (x), std::end (x));
}
}
Time_Series Time_Series::from_database (DB &db,
int const seqid,
Time_Point const &latest_date,
Duration const &window_size,
Duration const &market_close_time)
{
Time_Series ret {market_close_time};
/* If the user has entered a recent price for this stock, we need to
* splice the value into the time-series we produce (presumably we are
* only interested in doing this if the date is later than any data we
* already have). */
time_t user_date {0};
Currency_Value user_price {0.0};
{
auto sql = db.row_query ();
sql << "select UNIX_TIMESTAMP(last_price_date), last_price "
<< " from company "
<< " where company.seqid=" << seqid;
sql.execute ();
if (sql) {
user_date = sql.next_entry (user_date);
user_price = sql.next_entry (user_price);
++sql;
}
}
if (user_date > T (latest_date)) user_date = 0;
{
auto sql = db.row_query ();
sql << " select unix_timestamp(date), close "
<< " from prices "
<< " where company=" << seqid
<< " and date >= from_unixtime("
<< T (latest_date - window_size) << ") "
<< " and date <= from_unixtime(" << T (latest_date) << ") "
<< "order by date desc";
sql.execute ();
if (! sql) return ret;
auto user_time = chrono::system_clock::from_time_t (user_date);
for (; sql; ++sql)
{
auto const date = sql.next_entry<Time_Point> ()
+ ret.market_close_time;
if (user_time > date)
{
ret.emplace_back (user_date, user_price);
user_time = chrono::system_clock::from_time_t (0);
}
ret.emplace_back (date, sql.next_entry (Currency_Value {0.0}));
}
}
return ret;
} /* End of from_database method. */
auto Time_Series::interpolated_value (Time_Point const &date) const
-> Currency_Value
{
if (empty ())
return 0;
auto const t = lower_bound (rbegin (), rend (),
Event {date, 0.0},
[] (value_type const &a,
value_type const &b)
{ return a.time < b.time; });
if (t == rbegin ())
return t->price;
return (t-1)->price + (t->price - (t-1)->price)
* ((T (date) - T ((t-1)->time))
/ (double) (T (t->time) - T ((t-1)->time)));
}
auto Time_Series::get_range (Duration const &date_range) const -> Range
{
Range ret;
if (empty ())
return ret;
ret.end_time = front ().time;
const_iterator const end_
= date_range == chrono::seconds::max ()
? end ()
: std::upper_bound (begin (),
end (),
ret.end_time - date_range,
[] (Time_Point const &val, value_type const &a)
{ return a.time < val; });
auto const e
= minmax_element (begin (),
end_,
[] (value_type const &a, value_type const &b)
{ return a.price < b.price; });
ret.min_value = e.first->price;
ret.max_value = e.second->price;
if (end_ != begin ()) ret.start_time = (end_ - 1)->time;
else ret.start_time = end_->time;
return ret;
}
Time_Series Time_Series::compute_moving_average (Time_Series const &in,
Duration const &window,
Time_Point const &earliest)
{
if (in.size () < 2)
return in;
Time_Series ret {in.market_close_time};
auto const forward_window_size = window / 2;
auto const backward_window_size = window - forward_window_size;
auto const start_time = earliest - window;
/* These iterators run through _increasing_ dates. */
auto window_front = in.rbegin ();
while (window_front->time < start_time && window_front != in.rend ())
++window_front;
auto window_back = window_front;
int count = 0;
double sum = 0.0;
for (;
window_front != in.rend ()
&& window_front->time < window_back->time + forward_window_size;
++ window_front)
{
sum += window_front->price;
++count;
}
ret.emplace_back (window_back->time,
count > 0 ? sum / count : 0.0);
for (auto i = window_back + 1; i != in.rend (); ++i)
{
for (;
window_front != in.rend ()
&& window_front->time < i->time + forward_window_size;
++window_front)
{
sum += window_front->price;
++ count;
}
for (;
window_back != window_front
&& window_back->time < i->time - backward_window_size;
++window_back)
{
sum -= window_back->price;
-- count;
}
ret.emplace_back (i->time, count > 0 ? sum / count : 0.0);
}
reverse (std::begin (ret), std::end (ret));
return ret;
}
} /* End of namespace DMBCS::Trader_Desk. */

208
trader-desk/time-series.h Normal file
View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__TIME_SERIES__H
#define DMBCS__TRADER_DESK__TIME_SERIES__H
#include <chrono>
#include <vector>
#include <trader-desk/db.h>
/** \file
*
* Definition of \c Time_Point, Currency_Value, Duration types and the
* composed \c Event type, declaration of \c Time_Series class, and
* definition of convenience functions \c number<tick> (count ticks in
* duration) and \c t (construct a \c time_t from real-world calendar
* data). */
namespace DMBCS::Trader_Desk {
/** All trades and closing prices are fixed to a \c Time_Point. */
typedef chrono::system_clock::time_point Time_Point;
/** All trades and closing prices are expressed as \c
* Currency_Value's. */
typedef double Currency_Value;
/** All time spans are expressed as \c Duration's. */
typedef Time_Point::duration Duration;
/* The time at which the day started when this application started
* running. Whenever we consider closing prices, we regard this as the
* high-tide mark of the window of closing events. */
/* !!! We really need to re-evaluate the need for this. */
extern const Time_Point TODAY_MARK;
/** Whatever flavour of \c DURATION \a t we have, determine the number
* of complete \c TICK's that the duration spans. */
template <typename TICK, typename DURATION>
inline constexpr Duration::rep number (DURATION const &t)
{
return chrono::duration_cast<TICK> (t) . count ();
}
/** Convert from the normal notion of calendar date to a \c time_t.
* THE ARGUMENTS ARE STREET-WISE VALUES. */
inline time_t t (int const year, int const month, int const day,
int const hour, int const minute, int const second)
{
struct tm t {second, minute, hour, day, month - 1, year - 1900,
0, 0, 0, 0, 0};
return mktime (&t);
}
/** Convert from the normal notion of calendar date to a \c time_t.
* THE ARGUMENTS ARE STREET-WISE VALUES. */
inline time_t t (int const year, int const month, int const day)
{
return t (year, month, day, 0, 0, 0);
}
/** Every trade position open and close, and every market-closing price,
* is expressed as an \c Event: simply a time/value pair. */
struct Event
{
Time_Point time;
Currency_Value price {0.0};
constexpr Event (const Time_Point& t, const Currency_Value& c)
: time {t}, price {c}
{}
Event (const time_t t, const Currency_Value& c)
: Event {chrono::system_clock::from_time_t (t), c}
{}
constexpr Event () = default;
};
/** A vector of (time, price) pairs representing the value history of a
* commodity. It is a class invariant that the vector will ALWAYS be
* sorted with later dates at the front, earlier ones at the back (the
* motivation being that when the history needs to be extended, it will
* invariably be extended backwards in time and then the new data will
* simply be appended to the existing data vector). */
struct Time_Series : vector <Event>
{
/** A general-purpose transparent data object used to demarcate a box
* in (time x price) space, usually used to indicate the achieved
* bounds of a \c Time_Series. Note that \c start_time is
* numerically less than \c end_time. */
struct Range
{
Time_Point start_time;
Time_Point end_time;
Currency_Value min_value {0};
Currency_Value max_value {0};
/** Is the event \a e inside or on the border of this range? */
constexpr bool contains (const Event& e) const
{ return start_time <= e.time && end_time >= e.time
&& min_value <= e.price && max_value >= e.price; }
constexpr bool operator!= (const Time_Series::Range& a) const
{ return a.start_time != start_time || a.end_time != end_time
|| a.min_value != min_value || a.max_value != max_value; }
};
/** The number of seconds after midnight that the market from which
* this time-series derives closes. */
Duration market_close_time;
/** Effectively our null constructor, creating an empty time
* series. */
explicit Time_Series (const Duration& m) : market_close_time {m}
{}
/** The one useful (named) class constructor. Manufacture a new
* time-series extracted from the database. The time-span will be
* from \a latest_time and spanning \a window_size'd time interval to
* the past. The \a market_close_time is added to the date of all
* data read from the closing prices table. */
static Time_Series from_database (DB &db,
const int seqid,
const Time_Point& latest_time,
const Duration& window_size,
const Duration& market_close_time);
/** Get more data from the database, extending the length of the time
* series we are holding further back in time, from our latest datum
* to a distance \a window_size back in time. */
void extend_range (DB &db,
const int seqid,
const Duration& window_size);
/** Get the value of the commodity at a point in time, linearly
* interpolated between the two closest spanning points recorded in
* the time-series. */
Currency_Value interpolated_value (const Time_Point& date) const;
/** Get a \c Range object which boxes the time-series data up to an
* interval of \a date_range into the past. */
Range get_range (const Duration& date_range) const;
/** Get a \c Range object which boxes the entire time-series data
* (this is actually dealt with as a special case inside the above
* method). */
Range get_range () const
{ return get_range (chrono::seconds::max ()); }
/** Insert \a e into its correct place in the current time-series.
* This will likely be an expensive operation. */
void insert_event (const Event& e);
/** Produce a new time series by applying a moving average of size \a
* window to the \a incoming one, which is left unchanged. The
* returned series only goes back as far as \a earliest_time. */
static Time_Series compute_moving_average (const Time_Series& incoming,
const Duration& window,
const Time_Point& earliest_time);
}; /* End of class Time_Series. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__TIME_SERIES__H. */

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include "../trader-desk/trade-instruction.h"
#include <sstream>
/** \file
*
* Implementation of \c Trade_Instruction class. */
namespace DMBCS::Trader_Desk {
Currency_Value Trade_Instruction::value () const
{
Currency_Value ret;
istringstream (entry.get_text ()) >> ret;
return ret;
}
void Trade_Instruction::chart_data_changed ()
{
if (! chart_data.prices.empty ())
{
ostringstream hold;
hold << (chart_data.latest_price.price > 0.0
? chart_data.latest_price.price
: chart_data.prices.front ().price);
entry.set_text (hold.str ().c_str ());
}
}
Trade_Instruction::Trade_Instruction (Preferences& P,
Chart_Data &cd) : chart_data (cd)
{
pack_start (main_label, Gtk::PACK_SHRINK, 0);
pack_start (entry, Gtk::PACK_SHRINK, 0);
pack_start (units_label, Gtk::PACK_SHRINK, 0);
entry.set_width_chars (4);
/* Set up the trade_now_button. */
chart_data_changed ();
entry.signal_activate ()
.connect ([this, &P]
{ DB db {P};
chart_data.note_current_price (db, value ()); });
chart_data.changed_signal
.connect ([this] { chart_data_changed (); });
}
} /* End of namespace DMBCS::Trader_Desk. */

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__TRADE_INSTRUCTION__H
#define DMBCS__TRADER_DESK__TRADE_INSTRUCTION__H
#include <trader-desk/chart-data.h>
#include <gtkmm.h>
/** \file
*
* Declaration of the \c Trade_Instruction class. */
namespace DMBCS::Trader_Desk {
/** A component of a \c Hand_Analysis_Widget which entirely looks after
* itself (interacting with the rest of the program through signals),
* and allows the user to set the current price of a commodity, and to
* indicate that /buy/ and /sell/ actions have taken place on that
* current price.
*
* It is expected that the owning \c Hand_Analysis_Widget will maintain
* the \c Chart_Data object given to the constructor for the lifetime
* of one of these objects. */
class Trade_Instruction : public Gtk::HBox
{
/** The data we have on the commodity being watched. We react to any
* changes in these via the \c chart_data_changed method. */
Chart_Data &chart_data;
/** String printed in front of information box. */
Gtk::Label main_label {"Current price "};
/** String printed after information box. */
Gtk::Label units_label {"p "};
/** The user edit box. Updates from the user are sent directly to \c
* chart_data. */
Gtk::Entry entry;
/** The user has pressed the buy/sell button. */
void trade_now_pressed ();
/** A change in the chart (price history of commodity of interest) has
* taken place. */
void chart_data_changed ();
/** Get the contents of the edit box as a currency (pence) value. */
Currency_Value value () const;
/* Duplicating one of these objects is both pointless and dangerous. */
Trade_Instruction (Trade_Instruction const &) = delete;
Trade_Instruction &operator= (Trade_Instruction const &) = delete;
Trade_Instruction (Trade_Instruction &&) = delete;
Trade_Instruction &operator= (Trade_Instruction &&) = delete;
public:
/** Sole constructor which registers the \a chart_data object we
* follow, and sets up the GTK widget ready for use. */
Trade_Instruction (Preferences&, Chart_Data &);
}; /* End of class Trade_Instruction. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__TRADE_INSTRUCTION__H. */

513
trader-desk/trader-desk.cc Normal file
View file

@ -0,0 +1,513 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include "auto-config.h"
#include "alpha-vantage--monitor.h"
#include "application.h"
#include "markets.h"
#include "update-latest-prices.h"
#include "wizard.h"
/** \file
*
* Implementation of the \c Window object, and the applicationʼs \c main
* entry point. */
namespace DMBCS::Trader_Desk {
static void grid_injector (Chart_Grid &grid,
const Update_Latest_Prices::Data& data)
{
Chart *const c {grid.find_chart (data.company_seqid)};
if (c) c->data.new_event ({chrono::system_clock::to_time_t (data.time),
data.price});
}
struct Window : Gtk::Window
{
/** All of the pieces that make up this GTK Window application,
* including widgets that go in the window, engine parts, and
* glue. */
Application app;
/** Top-level layout: menu stacked on top of a notebook. */
Gtk::VBox v_box;
Gtk::HBox menu_strip;
Gtk::HBox alphavantage_clocks;
explicit Window (Preferences&&);
void update_latest_data ();
void change_company_name (Chart_Data& chart_data, const int& seqid);
void create_application (Preferences&& P);
void destroy_application ();
}; /* End of Window class. */
static void show_about ()
{
Gtk::AboutDialog a;
a.set_program_name (PACKAGE);
a.set_version (VERSION);
a.set_copyright
(gettext ("Copyright (c) Dale Mellor 2017, 2020\nGPLv3+ applies"));
a.set_documenters (vector<Glib::ustring> {"Dale Mellor"});
a.set_license_type (Gtk::LICENSE_GPL_3_0);
a.set_website ("https://rdmp.org/trader-desk");
a.set_authors (vector<Glib::ustring> {"Dale Mellor"});
a.set_logo (Gdk::Pixbuf::create_from_file (PKGDATADIR "/trader-desk.png")
->scale_simple (100, 100, Gdk::INTERP_NEAREST));
a.run ();
}
static void run_wizard
(Preferences& P,
const bool force,
std::function<void (Preferences&)> post_process = {})
{
Wizard *const W {new Wizard {P.file_path, force}};
/* W->set_transient_for (*this); */
/* W->set_attached_to (*this); */
W->set_modal (1);
W->show ();
W->signal_unmap ().connect ([W, &P, post_process]
{ P = W->database_prefs.prefs;
dump (P);
if (post_process) post_process (P);
delete W; });
}
static void run_preferences_dialog
(Window& W, const string& alpha_vantage_message = {})
{
auto D {alpha_vantage_message.length ()
? new Preferences_Dialog {W, W.app.user_prefs,
alpha_vantage_message}
: new Preferences_Dialog {W, W.app.user_prefs}};
D->signal_hide ()
.connect ([&W, D, P=W.app.user_prefs]
{ delete D;
run_wizard (W.app.user_prefs, 1,
[&W, P] (Preferences& newP)
{
if (! database_equal (P, newP))
{
W.destroy_application ();
W.create_application (Preferences {newP});
}
}); });
D->show ();
}
static void run_unforced_wizard (Window& W)
{
const auto P {W.app.user_prefs};
run_wizard (W.app.user_prefs, 0,
[&W, P] (Preferences& newP)
{
if (! database_equal (P, newP))
{
W.destroy_application ();
W.create_application (Preferences {newP});
}
});
}
void Window::change_company_name (Chart_Data& chart_data, const int& seqid)
{
unique_ptr<DB> db;
for (auto &a : app.market_grids)
{
Chart *const c {a->find_chart (seqid)};
if (c) { chart_data.subsume (&c->data);
if (! db) db = make_unique<DB> (app.user_prefs);
app . hand_analysis -> company_name
. read_names (*db, a->market.seqid, seqid);
return; }
}
}
void Window::create_application (Preferences&& P)
{
app.window = this;
app.user_prefs = std::move (P);
app.preferences_error
= [this] (const string& alpha_vantage_message)
{ run_preferences_dialog (*this, alpha_vantage_message); };
app.hand_analysis
= make_unique<Hand_Analysis_Widget>
([this] (Chart_Data &cd, int const &seqid)
{ change_company_name (cd, seqid); },
app.user_prefs);
app.actions = Gtk::ActionGroup::create ();
app.actions->add (Gtk::Action::create ("quit", Gtk::Stock::QUIT),
Gtk::AccelKey {"<control>q"},
[this] { close (); });
app.actions->add (Gtk::Action::create ("file-menu",
pgettext ("Menu", "_File")));
app.actions->add
(Gtk::Action::create ("preferences",
pgettext ("Menu", "_Preferences")),
Gtk::AccelKey {"<control>p"},
[this] { run_preferences_dialog (*this); });
app.actions->add
(Gtk::Action::create ("wizard",
pgettext ("Menu", "_Wizard")),
Gtk::AccelKey {"<control>w"},
[this] { run_unforced_wizard (*this); });
app.actions->add (Gtk::Action::create ("display-menu",
pgettext ("Menu", "_Market")));
app.actions->add (Gtk::Action::create ("market-menu",
pgettext ("Menu", "_Update")));
app.actions->add (Gtk::Action::create ("help-menu",
pgettext ("Menu", "_Help")));
app.actions->add (Gtk::Action::create ("about",
pgettext ("Menu", "_About")),
&show_about);
DB db {app.user_prefs};
for (auto &m : Markets {db})
if (m.second.tracked)
{
const auto i {app.market_grids.size ()};
if (i == 0) set_title (pgettext ("Label", "Trader Desk")
+ string {" : "}
+ m.second.world_data.name);
app . market_grids
. emplace_back (make_unique<Chart_Grid>
(app.user_prefs, m.second));
/* This needs to go into a sub-routine as it is required
* elsewhere. */
ostringstream a; a << "display-grid-" << i;
if (i < 9)
{
ostringstream b; b << "<control>" << i+1;
app . actions
-> add (Gtk::Action::create (a.str (),
m.second.world_data.name),
Gtk::AccelKey (b.str ()),
[this, i, name = m.second.world_data.name]
{ app.display_grid (i);
set_title (pgettext ("Label",
"Trader Desk")
+ string {" : "}
+ name); });
}
else
app.actions->add (Gtk::Action::create (a.str (),
m.second.world_data.name),
[this, i, name = m.second.world_data.name]
{ app.display_grid (i);
set_title (pgettext ("Label",
"Trader Desk")
+ string {" : "}
+ name); });
}
app.last_data_menu
= Gtk::Action::create ("update-recent",
pgettext ("Menu", "Update _latest data"));
app.last_data_menu->set_sensitive (0);
app.actions->add (app.last_data_menu,
[this] { update_latest_data (); });
app.close_data_menu
= Gtk::Action::create ("update-closes",
pgettext ("Menu", "Update _close data"));
app.actions->add (app.close_data_menu,
[this] { app.update_closing_prices (); });
app.actions->add (Gtk::Action::create ("ingest-new-market",
pgettext ("Menu",
"_Ingest new market")),
[this] { app.ingest_new_market (); });
app.ui_manager = Gtk::UIManager::create ();
app.ui_manager->insert_action_group (app.actions);
add_accel_group (app.ui_manager->get_accel_group ());
app.ui_manager->add_ui_from_string
("<ui>"
" <menubar name=\"menu\">"
" <menu action=\"file-menu\">"
" <menuitem action=\"ingest-new-market\"/>"
" <menuitem action=\"preferences\"/>"
" <menuitem action=\"wizard\"/>"
" <menuitem action=\"quit\"/>"
" </menu>"
" <menu action=\"display-menu\">"
+ [this]
{ ostringstream a;
for (size_t i=0; i < app.market_grids.size (); ++i)
a << "<menuitem action=\"display-grid-"
<< i << "\"/>\n";
return a.str (); } ()
+
" </menu>"
" <menu action=\"market-menu\">"
" <menuitem action=\"update-recent\"/>"
" <menuitem action=\"update-closes\"/>"
" </menu>"
" <menu action=\"help-menu\">"
" <menuitem action=\"about\"/>"
" </menu>"
" </menubar>"
"</ui>");
v_box.pack_start (menu_strip, Gtk::PACK_SHRINK);
menu_strip.pack_start (*app.ui_manager->get_widget ("/menu"),
Gtk::PACK_SHRINK);
menu_strip.pack_end (alphavantage_clocks, Gtk::PACK_SHRINK);
Alpha_Vantage__Monitor::make_clock_widgets (app.user_prefs,
alphavantage_clocks);
Glib::signal_timeout()
.connect ([] { Alpha_Vantage__Monitor::clocks->update ();
return 1; },
200);
/* v_box.pack_start (*app.ui_manager->get_widget ("/ToolBar"), */
/* Gtk::PACK_SHRINK); */
app.notebook.set_show_tabs (0);
app.notebook.set_show_border (0);
app.notebook.append_page (*app.hand_analysis);
if (app.market_grids.empty ())
app.notebook.append_page (*Gtk::manage (new Gtk::Label {"No markets"}));
else
for (auto &g : app.market_grids)
app.notebook.append_page (*g);
v_box.pack_start (app.notebook);
add (v_box);
show_all ();
app.display_grid (0);
for (auto &a : app.market_grids)
a->selection_signal
.connect ([this, &a] { app.hand_analysis->subsume_selected (*a);
app.close_data_menu->set_sensitive (0);
app.last_data_menu->set_sensitive (1);
app.notebook.set_current_page (0); });
if (app.market_grids.empty ())
app.ingest_new_market ();
set_default_size (650, 420);
set_icon_from_file (PKGDATADIR "/trader-desk.png");
} /* End of create_application. */
void Window::destroy_application ()
{
for (auto *const W : app.notebook.get_children ())
app.notebook.remove (*W);
app.market_grids.clear ();
app.hand_analysis.reset ();
for (auto *const W : menu_strip.get_children ())
menu_strip.remove (*W);
v_box.remove (menu_strip);
Alpha_Vantage__Monitor::remove_clock_widgets (alphavantage_clocks);
v_box.remove (app.notebook);
remove (/* v_box */);
}
void Window::update_latest_data ()
try
{
DB db {app.user_prefs};
if (app.notebook.get_current_page () == 0)
{
Update_Latest_Prices::do_update (db,
app.hand_analysis->chart.data,
app.user_prefs);
return;
}
Chart_Grid *const market
{app.market_grids [app.notebook.get_current_page ()-1].get ()};
Update_Latest_Prices::Work update {market->market};
do_update (&update,
db,
app.user_prefs,
[&db, market]
(const Update_Latest_Prices::Data& data) -> void
{ sql_injector (db, data);
grid_injector (*market, data); });
}
catch (const Alpha_Vantage::Error& E)
{
Gtk::MessageDialog
{*this,
gettext ("There seems to be a problem with the AlphaVantage "
"account, please check your settings on the "
"preferences panel. The message from the "
"server is:") + string {"\n\n"} + E.what () + "",
0,
Gtk::MESSAGE_ERROR}
.run ();
run_preferences_dialog (*this);
}
catch (Update_Closing_Prices::No_Connection const &)
{
Gtk::MessageDialog {*this,
gettext ("No Internet Connection"),
0,
Gtk::MESSAGE_WARNING}
.run ();
}
Window::Window (Preferences&& P) : app {std::move (P)}
{
signal_map_event ()
.connect ([this] (GdkEventAny*) -> bool
{ run_wizard (app.user_prefs, 1,
[this] (Preferences& P)
{ create_application (Preferences {P}); });
return 0; } );
}
} /* End of namespace DMBCS::Trader_Desk. */
int main (int argc, char **argv)
try
{
setlocale (LC_ALL, "");
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);
/* Needed for Alpha_Vantage. */
curlpp::Cleanup curl_lifetime;
std::string config_file;
if (argc > 1)
{
using std::cout;
if (argv [1] == std::string {"--config"}
|| argv [1] == std::string {"-c"})
{
if (argc < 3)
{
std::cerr << PACKAGE_STRING
<< "Error: -c option requires an argument.\n";
exit (1);
}
config_file = argv [2];
}
else if (argv [1] == std::string ("--version"))
{
cout << PACKAGE_STRING << '\n';
cout << gettext ("Copyright (C) 2017, 2020 Dale Mellor") << "\n\n"
<< gettext ("License GPLv3+: GNU GPL version 3 or later "
"<http://gnu.org/licenses/gpl.html>\n"
"This is free software: you are free to change "
"and redistribute it.\n"
"There is NO WARRANTY, to the extent permitted "
"by law.\n");
exit (0);
}
else if (argv [1] == std::string ("--help"))
{
cout << gettext ("usage") << ": trader-desk [options]\n";
cout << gettext ("To report bugs or contact the authors please "
"refer to http://rdmp.org/trader-desk\n");
exit (0);
}
}
Glib::thread_init ();
Gtk::Main kit (argc, argv);
namespace TD = DMBCS::Trader_Desk;
TD::Window window {config_file.empty ()
? TD::Preferences::from_default_file ()
: TD::Preferences::from_file (config_file)};
Gtk::Main::run (window);
return 0;
}
catch (std::runtime_error const &e)
{
std::cerr << e.what () << ".\n";
std::exit (1);
}

View file

@ -0,0 +1,193 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/update-closing-prices.h>
#include <trader-desk/alpha-vantage--monitor.h>
/** \file
*
* Implementation of the \c Update_Closing_Prices class. */
namespace DMBCS::Trader_Desk { namespace Update_Closing_Prices {
void sql_injector (DB& db, const Data& data)
{
db.quick () << "replace into prices set date=\""
<< data.year << "-" << data.month << "-" << data.day
<< "\", open=" << data.open
<< ", high=" << data.high
<< ", low=" << data.low
<< ", close=" << data.close
<< ", volume=" << data.volume
<< ", adjusted_close=" << data.adj_close
<< ", company=" << data.company_seqid;
db.quick () << "update company set last_close_date=greatest(\""
<< data.year << "-" << data.month << "-" << data.day
<< "\", last_close_date) where seqid=" << data.company_seqid;
}
vector<Company> entries_from_database (DB& db, const size_t market_seqid)
{
auto sql {db.row_query ()};
sql << "select rtrim(name), "
<< " symbol, "
<< " seqid, "
<< " unix_timestamp(greatest(date_add(current_date(),"
<< " interval -" << 10/* TIME_HORIZON */ << " year),"
<< "last_close_date)) "
<< " from company "
<< " where market=" << market_seqid;
sql.execute ();
vector <Company> entries;
entries.reserve (sql.number_rows ());
for (; sql; ++sql)
{
Company e;
sql >> e.name >> e.symbol >> e.seqid >> e.last_close_date;
entries.push_back (move (e));
}
return entries;
}
static tm current_tm ()
{
using C = chrono::system_clock;
const time_t current_t {C::to_time_t (C::now ())};
return *localtime (&current_t);
}
static tm tm_from (const time_t date)
{
tm start {*localtime (&date)};
/* ++ start.tm_mday; */
/* mktime (&start); */
return start;
}
static bool same_day (const tm& A, const tm& B)
{
return A.tm_year == B.tm_year && A.tm_mon == B.tm_mon
&& A.tm_mday == B.tm_mday;
}
static tm not_weekend (tm&& T)
{
mktime (&T);
if (T.tm_wday == 0) T.tm_mday -= 2;
else if (T.tm_wday == 6) T.tm_mday -= 1;
else return T;
mktime (&T);
return T;
}
static tm day_before (tm& T)
{
mktime (&T);
T.tm_mday -= 1;
mktime (&T);
return T;
}
static void do_update
(Work& ucp,
Preferences& user_prefs,
const function <void (double, const Company&)> progress_callback,
const function <void (const Data&)> injector,
const function <void (const int& company_seqid)> done_processing,
const vector<Company>& entries)
{
ucp.stop = false;
for (size_t i {0}; i < entries.size (); ++i)
{
if (ucp.stop) break;
const Company& company {entries [i]};
if (progress_callback)
progress_callback (i / double (entries.size ()), company);
tm now {not_weekend (current_tm ())};
const auto close_hour {chrono::duration_cast<chrono::hours>
(ucp.market.world_data.close_time).count ()};
const auto close_min {chrono::duration_cast<chrono::minutes>
(ucp.market.world_data.close_time).count ()
% 60};
if (now.tm_hour < close_hour
|| (now.tm_hour == close_hour && now.tm_min < close_min))
now = day_before (now);
if (!same_day (tm_from (company.last_close_date), now))
try { while (Price_Server::get_closing_prices
(company,
ucp.market.world_data.component_extension,
user_prefs,
injector)
== Price_Server::TO_DO::MORE_WORK)
if (ucp.stop) break; }
catch (const Price_Server::Bad_API_Key&) { throw; }
catch (const Price_Server::Error&)
{ cerr << "Skipping company " << company.name << ".\n"; }
/* This will be thrown by curlpp if there is any serious problem
* with the networking. */
catch (const exception& e) { throw No_Connection {e}; }
if (ucp.stop) break;
if (done_processing) done_processing (company.seqid);
}
}
void do_update
(Work& ucp,
DB& db,
Preferences& user_prefs,
const function <void (double, const Company&)> progress_callback,
const function <void (const Data&)> injector,
const function <void (const int& company_seqid)> company_done)
{
return do_update (ucp,
user_prefs,
progress_callback,
injector,
company_done,
entries_from_database (db, ucp.market.seqid));
}
} } /* End of namespace DMBCS::Trader_Desk::Update_Closing_Prices. */

View file

@ -0,0 +1,155 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__UPDATE_CLOSING_PRICES__H
#define DMBCS__TRADER_DESK__UPDATE_CLOSING_PRICES__H
#include <trader-desk/markets.h>
#include <atomic>
/** \file
*
* Declaration of the \c Update_Closing_Prices class. */
namespace DMBCS::Trader_Desk {
struct Alpha_Vantage__Monitor;
using Price_Server = Alpha_Vantage__Monitor;
/** Really a function object, the implementation being the \c run
* method. The purpose of the combined function is to scan the
* database for all companies in some market, use the Yahoo! API to get
* the latest prices for that company, and then inject the results to
* some output destination, most likely back to the database itself,
* but also maybe to working \c Chart_Data objects. */
namespace Update_Closing_Prices
{
/** We will throw this object any time we canʼt get service from
* Yahoo!. */
struct No_Connection : runtime_error
{
explicit No_Connection (const exception& E)
: runtime_error {pgettext ("Error",
"Cannot get data from Internet")
+ string {": "} + E.what ()}
{}
};
/** Vessel to hold the data which comes back from the Yahoo!
* server. */
struct Data
{
/** Our own database sequence ID. */
int company_seqid;
/** The date of the closing transaction. */
int year, month, day;
/** The values of the daily amounts. We put all of this into
* the database, though the application currently only takes
* any notice of the \c close prices. */
double open, high, low, close;
/** The volume of trade on this day--put into the database,
* but not currently used by the application. */
int volume;
/** The adjusted closing price--put into the database, but
* not currently used by the application. */
double adj_close;
};
/** Vessel to hold information about companies for which we need to
* get updated information. */
struct Company
{
/** Human-readable name of the company. */
string name;
/** The companyʼs ticker symbol. */
string symbol;
/** Our database sequence ID for the company. */
int seqid;
/** The time of our most recent close price for this company. */
time_t last_close_date;
};
struct Work
{
/** Our database sequence ID for the market in question. */
const Market_Meta_Data& market;
/** A flag to tell us to stop processing (may be set from
* outside the class). */
atomic<bool> stop {false};
};
/** Write the \a data to the database. */
void sql_injector (DB&, const Data&);
/** Get the list of companies on which to get new data, from the
* database based on the market. */
vector<Company> entries_from_database (DB&, const size_t market_seqid);
/** Do the work: scan the \a db database for the appropriate \a
* companies and currently known closing prices, obtain new price
* information from data service and store these back to the database. The
* return will list the symbols of companies of which we were unable to
* obtain the latest information.
*
* If given (not \c nullptr), the \a progress_callback will be called
* at regular intervals with the percentage of work completed.
*
* The \a injector will be called for every new datum that needs adding
* to a companyʼs price records.
*
* If not \c nullptr, the \a company_done callback will be called after
* all data for a particular company have been processed as above. */
void do_update
(Work&,
DB&,
Preferences&,
const function <void (double, const Company&)> progress_callback,
const function <void (const Data&)> injector,
const function <void (const int& company_seqid)> company_done);
}; /* End of namespace Update_Closing_Prices. */
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__UPDATE_CLOSING_PRICES__H. */

View file

@ -0,0 +1,118 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/update-latest-prices.h>
#include <trader-desk/alpha-vantage--monitor.h>
#include <algorithm>
#include <numeric>
#include <set>
/** \file
*
* Implementation of the \c Update_Latest_Prices class. */
namespace DMBCS::Trader_Desk::Update_Latest_Prices {
using Data_Server = Alpha_Vantage__Monitor;
void sql_injector (DB& db, const Data& data)
{
time_t t = chrono::system_clock::to_time_t (data.time);
tm tm;
localtime_r (&t, &tm);
db.quick () << "update company "
<< "set last_price=" << data.price << ','
<< "last_price_date=\"" << (tm.tm_year+1900) << '-'
<< (tm.tm_mon+1)
<< '-' << tm.tm_mday << ' '
<< tm.tm_hour << ':' << tm.tm_min
<< ':' << tm.tm_sec << "\" "
<< " where seqid=" << data.company_seqid;
}
static void do_update (Work *const work,
Preferences& P,
function <void (Data const &)> injector,
vector<Company> const &entries)
try
{
for (const Company& C : entries)
injector (Data_Server::get_latest_data
(C, work->market.world_data.component_extension, P));
}
catch (exception const &e)
{
throw Update_Closing_Prices::No_Connection {e};
}
void do_update (Work *const work,
DB& db,
Preferences& P,
function <void (Data const &)> injector)
{
do_update (work,
P,
injector,
Update_Closing_Prices::entries_from_database
(db, work->market.seqid));
}
void do_update (DB& db, Chart_Data& data, Preferences& P)
{
auto row {db.row_query ()};
row << "select company.symbol, "
<< "unix_timestamp(company.last_close_date), "
<< "market.component_extension "
<< "from company, market "
<< "where company.seqid=" << data.company_seqid
<< " and market.seqid=company.market";
row.execute ();
Company C;
row >> C.symbol >> C.last_close_date;
C.seqid = data.company_seqid;
const string market_symbol {row.next_entry<string> ()};
const Data D {Data_Server::get_latest_data (C, market_symbol, P)};
++row;
{unique_lock L {data.prices_mutex};
data.last_fetch_time = D.time;
data.prices.insert_event ( {D.time, D.price} );
data.extremes.end_time = D.time; }
data.changed_signal.emit ();
}
} /* End of namespace DMBCS::Trader_Desk::Update_Latest_Prices. */

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS_TRADER_DESK__UPDATE_LATEST_PRICES__H
#define DMBCS_TRADER_DESK__UPDATE_LATEST_PRICES__H
#include <trader-desk/chart-data.h>
#include <trader-desk/update-closing-prices.h>
/** \file
*
* Declaration of the \c Update_Latest_Prices class. */
namespace DMBCS::Trader_Desk {
/** Function object which reads a list of companies in a market from the
* database, fetches the latest prices for those companies from the
* Yahoo! service, and then writes the new information back to the
* database and gives them back to the calling application.
*
* Similarities with the \c Update_Closing_Prices class are actually
* quite superficial, but we do inherit that class mainly to take
* advantage of the functionality for the initial fetch of the company
* list from the database. */
namespace Update_Latest_Prices
{
/** Vessel to carry the information that comes back from the data
* service. */
struct Data {
/** Our database sequence ID. */
int company_seqid;
/** The time at which the price holds. */
Time_Point time;
/** The current price of this commodity. */
double price;
};
using Company = Update_Closing_Prices::Company;
struct Work : Update_Closing_Prices::Work {};
/** Update the database with the new \a data. */
void sql_injector (DB&, const Data&);
/** Get the companies for the registered market, fetch their latest
* data from the Yahoo! server, and pass the results, one at a time,
* to \a injector. */
void do_update (Work *const work,
DB&,
Preferences&,
function <void (const Data&)> injector);
void do_update (DB&, Chart_Data&, Preferences&);
} } /* End of namespace DMBCS::Trader_Desk::Update_Latest_Prices. */
#endif /* Undefined DMBCS_TRADER_DESK__UPDATE_LATEST_PRICES__H. */

989
trader-desk/wizard.cc Normal file
View file

@ -0,0 +1,989 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#include <trader-desk/wizard.h>
#include <trader-desk/db.h>
#include <fstream>
#include <iostream>
#include <random>
#include <regex>
#include <thread>
namespace DMBCS::Trader_Desk {
Database_Prefs& Database_Prefs::operator= (const Prefs& P)
{
prefs = P;
host .set_text (P.database_host);
socket .set_text (P.database_socket);
port .set_text (to_string (P.database_port));
database .set_text (P.database_instance);
user .set_text (P.database_user);
password .set_text (P.database_password);
submit.set_sensitive (0);
return *this;
}
Prefs Database_Prefs::update_prefs (Prefs P)
{
P.database_host = host.get_text ();
P.database_socket = socket.get_text ();
P.database_port = atoi (port.get_text ().c_str ());
P.database_instance = database.get_text ();
P.database_user = user.get_text ();
P.database_password = password.get_text ();
return P;
}
static void entry_ (Database_Prefs& P, Gtk::Entry& E,
const string& label, const int row)
{
P.attach (*Gtk::manage (new Gtk::Label {label + ": "}), 0, row);
E.set_width_chars (30);
E.signal_changed ().connect ([&P] {P.submit.set_sensitive ();});
E.signal_activate ().connect ([&P] {P.submit.clicked ();});
P.attach (E, 1, row);
}
Database_Prefs::Database_Prefs ()
{
const auto entry
{ [this] (Gtk::Entry& E, const string& L, const int R)
{ return entry_ (*this, E, L, R); } };
entry (host, t_("Label", "Host"), 0);
entry (socket, t_("Label", "Socket"), 1);
attach (*Gtk::make_managed<Gtk::Label>
(t_("Label", "Port") + string {": "}),
0, 2);
port.signal_changed ().connect ([this] {submit.set_sensitive ();});
port.signal_activate ().connect ([this] {submit.clicked ();});
attach (port, 1, 2);
entry (database, t_("Label", "Database"), 3);
entry (user, t_("Label", "User"), 4);
user.signal_changed ().connect ([this] { prefs.blank_password_forced = 0;
password.set_sensitive (); });
entry (password, t_("Label", "Password"), 5);
auto *const B {Gtk::make_managed<Gtk::HBox> ()};
attach (*B, 0, 6, 2, 1);
B->pack_start (submit, Gtk::PACK_EXPAND_PADDING);
submit.set_sensitive (0);
show_all ();
}
static tuple<unique_ptr<Gtk::Widget>, Gtk::TextBuffer*, Gtk::HBox*>
make_panel (Gtk::VBox& V, const string& nature)
{
auto H {make_unique<Gtk::HBox> (0, 15)};
V.pack_start (*H, Gtk::PACK_SHRINK);
H->pack_start (*Gtk::make_managed<Gtk::Image> ("dialog-" + nature,
Gtk::ICON_SIZE_DIALOG),
Gtk::PACK_SHRINK);
auto *const V2 {Gtk::make_managed<Gtk::VBox> (0, 15)};
H->pack_start (*V2);
auto *const T {Gtk::make_managed<Gtk::TextView> ()};
V2->pack_start (*T, Gtk::PACK_SHRINK);
T->set_wrap_mode (Gtk::WRAP_WORD);
T->override_background_color (Gdk::RGBA {"rgba(0,0,0,0)"});
auto *const H2 {Gtk::make_managed<Gtk::HBox> ()};
V2->pack_start (*H2, Gtk::PACK_SHRINK);
return {move (H), T->get_buffer ().get (), H2};
}
static void make_configuration_page (Wizard& W)
{
W.configuration_page.set_spacing (65);
auto V {Gtk::make_managed<Gtk::VBox> (0, 15)};
W.configuration_page.pack_start (*V, Gtk::PACK_SHRINK);
V->pack_start (*Gtk::make_managed<Gtk::Label>
(t_("Label", "Using config file") + string {":"}),
Gtk::PACK_SHRINK);
V->pack_start (W.config_file_entry, Gtk::PACK_SHRINK);
W.config_file_entry.set_width_chars (30);
auto *const H {Gtk::make_managed<Gtk::HBox> (0, 15)};
V->pack_start (*H, Gtk::PACK_SHRINK);
H->pack_start (W.use_default_config_button, Gtk::PACK_EXPAND_PADDING);
H->pack_start (W.find_file_button, Gtk::PACK_EXPAND_PADDING);
H->pack_start (W.change_button, Gtk::PACK_EXPAND_PADDING);
W.configuration_page.show_all ();
{auto [P, T, H] {make_panel (W.configuration_page, "warning")};
W.create_file_panel = move (P);
T->set_text (gettext ("This file does not exist, shall I create it?"));
H->pack_start (W.create_file_button, Gtk::PACK_EXPAND_PADDING);
}
{auto [P, T, H] {make_panel (W.configuration_page, "warning")};
W.non_default_panel = move (P);
T->set_text (gettext ("This is not the default config file path. "
"This means that you will either have to copy "
"the file to the default location, or use the "
"-c option to specify the file whenever you "
"run the trader-desk application."));
H->pack_start (W.acknowledge_check, Gtk::PACK_SHRINK);
}
W.config_file_entry.signal_changed ()
.connect ([&W] { W.change_button.set_sensitive (); });
W.config_file_entry.signal_activate ()
.connect ([&W] { W.change_button.clicked (); });
}
template<typename Callback>
static void connect_clicked
(sigc::connection& C, Gtk::Button& B, Callback F)
{
C.disconnect ();
C = sigc::connection {B.signal_clicked ().connect (F)};
}
static void check_config_file
(Wizard& W, const string& file_name, bool not_default);
static void config_file_chooser (Wizard& W)
{
Gtk::FileChooserDialog D {W,
t_("Label", "trader-desk config file"),
Gtk::FILE_CHOOSER_ACTION_SAVE};
D.add_button (t_("Label", "Open"), Gtk::RESPONSE_ACCEPT);
D.set_default_response (Gtk::RESPONSE_ACCEPT);
D.signal_response ()
.connect ([&W, &D] (const int response)
{ D.hide ();
if (response == Gtk::RESPONSE_ACCEPT)
check_config_file (W, D.get_filename (), 0); });
D.run ();
}
static void check_config_file
(Wizard& W, const string& file_name, bool not_default = 0)
{
W.config_file_entry.set_text (file_name);
W.config_file_entry
.set_width_chars (max<int> (W.config_file_entry.get_width_chars (),
file_name.length ()));
W.use_default_config_button
.set_sensitive (file_name != Preferences::default_file_name ());
connect_clicked
(W.udcb_i,
W.use_default_config_button,
[&W, not_default]
{ check_config_file
(W, Preferences::default_file_name (), not_default); });
connect_clicked (W.ffb_i,
W.find_file_button,
[&W] { config_file_chooser (W); });
W.change_button.set_sensitive (0);
connect_clicked
(W.cb_i,
W.change_button,
[&W, not_default]
{ check_config_file
(W, W.config_file_entry.get_text (), not_default); });
ifstream i {file_name};
const bool file_exists {i.good ()};
if (file_exists)
{
i.close ();
W.database_prefs.prefs = Prefs {Preferences::from_file (file_name)};
W.create_file_panel->hide ();
}
else
{
W.create_file_panel->show_all ();
connect_clicked
(W.cfb_i,
W.create_file_button,
[&W, &E = W.config_file_entry, not_default]
{ Preferences::from_file (E.get_text ());
check_config_file (W, E.get_text (), not_default);});
}
if (file_name != Preferences::default_file_name ())
{
((Gtk::Image*) ((Gtk::Container*) W.non_default_panel.get ())
->get_children ().front ())
->set_from_icon_name (string {"dialog-"}
+ (not_default ? "information" : "warning"),
Gtk::ICON_SIZE_DIALOG);
W.non_default_panel->show_all ();
W.acknowledge_check.set_active (not_default);
connect_clicked
(W.ac_i, W.acknowledge_check,
[&W] { check_config_file (W,
W.config_file_entry.get_text (),
W.acknowledge_check.get_active ()); });
}
else
W.non_default_panel->hide ();
const bool done
{file_exists && (not_default
|| file_name == Preferences::default_file_name ())};
W.set_page_complete (W.configuration_page, done);
if (W.force && done) W.next_page ();
}
static unique_ptr<Gtk::Widget> absolute_message (Gtk::VBox& page,
const string& message)
{
auto [P, T, H] {make_panel (page, "warning")};
T->set_text (message);
return move (P);
}
static void make_database_page (Wizard& W, Gtk::VBox& page)
{
page.pack_start (W.database_prefs, Gtk::PACK_SHRINK);
page.show_all ();
{auto [P, T, H] { make_panel (page, "warning") };
W.no_password_panel = move (P);
T->set_text (gettext ("No password has been set for this user. You are "
"strongly recommended to set one. Enter and commit "
"a new password above, let me generate a random "
"password for you, or press skip if you are sure "
"you donʼt need one."));
H->pack_start (W.generate_password_button, Gtk::PACK_EXPAND_PADDING);
H->pack_start (W.use_no_password_button, Gtk::PACK_EXPAND_PADDING);
}
{auto [P, T, H] { make_panel (page, "information") };
W.blank_password_panel = move (P);
T->set_text (gettext ("No database password is being used for this "
"user."));
H->pack_start (W.blank_password_ack_button, Gtk::PACK_SHRINK);
}
W.cannot_access_panel_1
= absolute_message
(page,
gettext ("Cannot access the database with this account. Please "
"correct the user and password fields above. You may also "
"insert the root password, if you know it, and I will try "
"to help sort the problem out; you will need to do this if "
"you are trying to create a new user."));
W.cannot_access_panel_2
= absolute_message
(page,
gettext ("Cannot access the database with this account. Please "
"correct the user and password fields above."));
{auto [P, text, buttons] {make_panel (W.database_access_page, "warning")};
W.privilege_panel = move (P);
text->set_text (gettext ("This user does not have access to this database "
"instance. Either modify the settings above or "
"provide the root password in the box below"));
buttons->pack_start (*Gtk::make_managed<Gtk::Label>
(t_("Label", "Database root password: ")));
W.database_root_entry.set_width_chars (30);
buttons->pack_start (W.database_root_entry, Gtk::PACK_SHRINK);
}
W.database_root_entry
.signal_activate ()
.connect ([&W]
{ W.database_prefs.prefs.root_password
= W.database_root_entry.get_text ();
W.get_database_connection (W.database_prefs.prefs);});
{auto [P, T, H] {make_panel (W.database_access_page, "warning")};
W.create_database_panel = move (P);
T->set_text (gettext ("This database instance does not exist. Either "
"edit the entry above or select the create "
"button."));
H->pack_start (W.create_database_button, Gtk::PACK_EXPAND_PADDING);
}
{auto [P, T, H] { make_panel (W.database_access_page, "warning") };
W.create_password_panel = move (P);
T->set_text (gettext ("The user password is not set in the database. "
"Do you want me to do that for you now?"));
H->pack_start (W.blank_password_button, Gtk::PACK_EXPAND_PADDING);
H->pack_start (W.create_password_button, Gtk::PACK_EXPAND_PADDING);
}
W.port_problem_panel
= absolute_message (page,
gettext ("Cannot contact a database on that PORT."));
W.socket_problem_panel
= absolute_message
(page, gettext ("Cannot contact a local database on that SOCKET."));
W.host_problem_panel
= absolute_message
(page, gettext ("The database host you specified cannot be reached."));
}
static string slurp_file (ifstream&& input)
{
input.seekg (0, ios::end);
string line (input.tellg (), char {});
input.seekg (0, ios::beg);
input.read (line.data (), line.length ());
return line;
}
static void prepare_data_page (Wizard&);
static gboolean progress_ticker (gpointer W_)
{
Wizard& W {*(Wizard*) W_};
/* !! Thread local. */
static int old_progress = 0;
if (W.progress_total < 0)
{
prepare_data_page (W);
return 0;
}
if (W.progress_count != old_progress)
{
old_progress = W.progress_count;
if (W.progress_total == 0)
W.progress_bar.pulse ();
else
W.progress_bar.set_fraction
(W.progress_count / (double) W.progress_total);
}
return 1;
}
static void prepare_data_page (Wizard& W)
{
for (auto *const i : W.data_page.get_children ())
W.data_page.remove (*i);
DB db {W.database_prefs.prefs};
if (db.scalar_result<int> (0, "select count(*) from company") != 0)
{
if (W.force) { W.close (); /* next_page (); */ return; }
W.set_page_complete (W.data_page);
W.data_page.pack_start (*W.database_ready_panel, Gtk::PACK_SHRINK);
W.data_page.show_all ();
return;
}
ifstream I {PKGDATADIR "/data.sql.xz"};
if (! I.good ())
{
if (W.force) { W.close (); /* next_page (); */ return; }
W.set_page_complete (W.data_page);
W.data_page.pack_start (*W.no_data_panel, Gtk::PACK_SHRINK);
W.data_page.show_all ();
return;
}
W.data_page.pack_start
(*Gtk::make_managed<Gtk::Label>
(pgettext ("Label", "Pre-populating database")),
Gtk::PACK_SHRINK);
W.data_page.pack_start (W.progress_bar, Gtk::PACK_SHRINK);
W.show_all ();
gdk_threads_add_timeout (250, progress_ticker, &W);
std::thread
{
[&W]
{
DB db {W.database_prefs.prefs};
/* !! Would prefer to call out to xz library. */
system ("cat " PKGDATADIR "/data.sql.xz "
"| xz -d "
"> /tmp/trader-desk--data.sql");
const string sql
{slurp_file (ifstream {"/tmp/trader-desk--data.sql"})};
unlink ("/tmp/trader-desk--data.sql");
W.progress_total = 0;
W.progress_count = 0;
for (size_t a {0}; ; )
{
const size_t b {sql.find ("INSERT", a)};
if (b == sql.npos) break;
++W.progress_count;
a = b + 1;
}
W.progress_total = W.progress_count.load ();
W.progress_count = 0;
for (size_t a {0}; ; )
{
const size_t b {sql.find ("INSERT", a)};
if (b == sql.npos) break;
const size_t c {sql.find (";", b)};
db.instruction (sql.substr (b, c-b));
++W.progress_count;
a = b + 1;
}
W.progress_total = -1;
}
} . detach ();
}
static bool do_tables (Wizard& W, const bool build)
try
{
Prefs& P {W.database_prefs.prefs};
DB db {P};
if (1 == db.scalar_result (0, "select version from global"))
return 1;
if (! build) return 0;
db.instruction ("create table global "
"(version int, "
"last_markets_update datetime default 0)");
db.instruction ("insert into global "
"set version=1");
db.instruction ("create table company "
"(seqid int(6) primary key auto_increment, "
"name varchar(50) not null, "
"symbol varchar(6) not null, "
"market int(6) not null default 0, "
"last_price float, "
"last_price_date datetime, "
"last_close_date date not null "
"default '0000-00-00')");
db.instruction ("create table prices "
"(date date, "
"company int(6), "
"open float, high float, low float, "
"close float, "
"volume int(11), adjusted_close float)");
db.instruction ("alter table prices "
"add primary key (date, company)");
db.instruction ("create table market "
"(seqid int(6) primary key auto_increment, "
"symbol varchar(6), "
"name varchar(36), "
"component_extension varchar(6), "
"last_update datetime default 0, "
"tracked bool default 0, "
"close_time time)");
db.instruction ("create table alphavantage_ticks "
"(time int(11) primary key)");
return 1;
}
catch (exception&) { return 0; }
static void prepare_table_page (Wizard& W)
{
do_tables (W, 1);
W.set_page_complete (W.tables_page);
if (W.force) { W.next_page (); return; }
Gtk::TextView *const T {Gtk::make_managed<Gtk::TextView> ()};
W.tables_page.add (*T);
T->get_buffer ()->set_text ("The database tables are in place.");
W.tables_page.show_all ();
}
int Wizard::page_order (Gtk::Widget *const a, Gtk::Widget *const b)
{
if (a == 0 || b == 0 || a == b) return 0;
if (a == (Gtk::Widget*)&configuration_page)
return -1;
if (a == (Gtk::Widget*)&database_access_page)
return b == (Gtk::Widget*)&configuration_page ? 1 : -1;
if (a == (Gtk::Widget*)&tables_page)
return b == (Gtk::Widget*)&data_page ? -1 : 1;
return 1;
}
static void make_data_page (Wizard& W, Gtk::VBox& page)
{
{auto [P, T, H] { make_panel (page, "information") };
W.no_data_panel = move (P);
T->set_text (gettext ("There are no data available to pre-populate "
"the database. You will be invited to import "
"data from the Internet."));
}
{auto [P, T, H] { make_panel (page, "information") };
W.database_ready_panel = move (P);
T->set_text (gettext ("The database is populated with some data."));
}
}
Wizard::Wizard (const string& config_file_name, const bool f) : force {f}
{
const auto add_page
{ [&W=*this] (Gtk::VBox& V, const string& title)
{ W.append_page (V);
W.set_page_type (V, Gtk::ASSISTANT_PAGE_CONTENT);
W.set_page_title (V, title);
V.set_spacing (40); } };
add_page (configuration_page, t_("Label", "Config file"));
make_configuration_page (*this);
add_page (database_access_page, t_("Label", "Database access"));
make_database_page (*this, database_access_page);
add_page (tables_page, t_("Label", "Tables"));
add_page (data_page, t_("Label", "Historical data"));
make_data_page (*this, data_page);
set_page_type (data_page, Gtk::ASSISTANT_PAGE_CONFIRM);
database_prefs.submit
.signal_clicked ()
.connect ([this]
{ database_prefs.update_prefs ();
get_database_connection
(database_prefs.prefs); });
signal_close ()
.connect ([this] { signal_wizard_complete.emit (database_prefs.prefs);
hide (); });
signal_cancel ().connect ([this] {hide ();});
show_all ();
set_icon_from_file (PKGDATADIR "/trader-desk.png");
signal_prepare ()
.connect ([this] (Gtk::Widget *const page)
{ if (page_order (last_page, page) > 0) force = 0;
last_page = page;
if (page == (Gtk::Widget*) &configuration_page)
check_config_file (*this,
database_prefs.prefs.file_path);
else if (page == (Gtk::Widget*) &database_access_page)
get_database_connection (database_prefs.prefs);
else if (page == (Gtk::Widget*) &tables_page)
prepare_table_page (*this);
else if (page == (Gtk::Widget*) &data_page)
prepare_data_page (*this);});
check_config_file (*this, config_file_name);
}
static bool host_problem (Wizard& W, const string& error)
{
/* !!!! This probably wouldn't work if the machine is using a
* language other than English? */
static const regex unknown_host_RE {".*unknown.*server host.*",
regex::icase};
if (! regex_match (error, unknown_host_RE)) return 0;
W.host_problem_panel->show_all ();
return 1;
}
static bool socket_problem (Wizard& W, const string& error)
{
static const regex bad_socket_RE
{".*can.t.*connect.*local.*server.*socket.*",
regex::icase};
if (! regex_match (error, bad_socket_RE)) return 0;
W.socket_problem_panel->show_all ();
return 1;
}
static bool port_problem (Wizard& W, const string& error)
{
static const regex bad_port_RE {".*can.t.*connect.*server.*",
regex::icase};
if (! regex_match (error, bad_port_RE)) return 0;
W.port_problem_panel->show_all ();
return 1;
}
static void do_set_password_on_database (Database_Prefs& P)
{
string old_password {};
swap (old_password, P.prefs.database_password);
DB {P.prefs}
.instruction ("set password=password('" + old_password + "')");
swap (old_password, P.prefs.database_password);
}
static bool set_password_on_database (Wizard& W, Database_Prefs& P)
{
if (W.force) { do_set_password_on_database (P);
return 1; }
W.create_password_panel->show_all ();
connect_clicked (W.bpb_i, W.blank_password_button,
[&W, &P] { P.prefs.database_password = "";
W.get_database_connection (P.prefs); });
connect_clicked (W.cpb_i, W.create_password_button,
[&W, &P]
{ do_set_password_on_database (P);
W.get_database_connection (P.prefs); });
P.password.set_sensitive (1);
return 0;
}
static tuple <Prefs, unique_ptr<DB>> get_root_access (Prefs P)
{
Prefs root_P {P};
root_P.database_user = "root";
root_P.database_instance = "";
try { if (P.root_password.length ())
{
root_P.database_password = P.root_password;
return {move (P), make_unique<DB> (root_P)};
} }
catch (exception&) {}
root_P.database_password = P.database_password;
try { P.root_password = P.database_password;
P.database_password = {};
return {move (P), make_unique<DB> (root_P)}; }
catch (exception&) {}
root_P.database_password = {};
try { P.root_password = {};
return {move (P), make_unique<DB> (root_P)}; }
catch (exception& E) { cerr << "XXX: " << E.what () << "\n"; }
return {move (P), unique_ptr<DB> {}};
}
static void create_database_as_user (Database_Prefs& P)
{
Preferences prefs {P.prefs};
prefs.database_instance = "";
DB db {prefs};
auto I {db.instruction ()};
I << "create database " << P.prefs.database_instance
<< " character set 'utf8'";
I.execute ();
}
static void create_database_as_root (Database_Prefs& P)
{
Prefs prefs {P.prefs};
prefs.database_instance = "";
auto [p, root_db] {get_root_access (prefs)};
P.prefs.root_password = p.root_password;
if (! root_db) create_database_as_user (P);
root_db->instruction ("create database " + P.prefs.database_instance
+ " character set 'utf8'");
}
static bool database_instance_problem (Wizard& W, Database_Prefs& P)
{
Prefs prefs {P.prefs};
prefs.database_instance = "";
auto [p, root_db] {get_root_access (prefs)};
prefs.root_password = p.root_password;
if (root_db)
{
P.prefs.root_password = prefs.root_password;
if (root_db->scalar_result<int> (0,
"select count(*) "
"from information_schema.schemata "
"where schema_name='%s'",
P.prefs.database_instance.data ()))
return 1;
if (W.force) { create_database_as_root (P); return 1; }
W.create_database_panel->show_all ();
connect_clicked (W.cdb_i, W.create_database_button,
[&W, &P] { create_database_as_root (P); });
return 0;
}
try
{
DB user_db {prefs};
if (user_db.scalar_result<int> (0,
"select count(*) "
"from information_schema.schemata "
"where schema_name='%s'",
P.prefs.database_instance.data ()))
return 1;
if (W.force)
{
try { create_database_as_user (P); return 1; }
catch (exception&)
{
W.privilege_panel->show_all ();
return 0;
}
}
W.create_database_panel->show_all ();
connect_clicked (W.cdb_i, W.create_database_button,
[&W, &P] { create_database_as_user (P); });
}
catch (exception& E) {}
return 0;
}
inline void grant_privileges
(DB& root_db, const string& database, const string& user)
{ root_db.instruction ("grant all on " + database + ".* to " + user); }
static void grant_privileges (DB& root_db, Wizard& W)
{
const auto& P {W.database_prefs.prefs};
grant_privileges (root_db, P.database_instance, P.database_user);
}
static void grant_privileges (Wizard& W)
{
auto [prefs, root_db] {get_root_access (W.database_prefs.prefs)};
if (root_db) { W.privilege_panel->hide ();
return grant_privileges (*root_db, W); }
W.privilege_panel->show_all ();
}
static bool no_user_access (Wizard& W, const string& error)
{
static const regex error_re {".*access denied for user.*", regex::icase};
if (! regex_match (error, error_re)) return 1;
{Prefs p {W.database_prefs.prefs};
p.database_instance = "";
try { DB {p}; grant_privileges (W); return 1; }
catch (exception&) {}
}
auto [p, root_db] {get_root_access (W.database_prefs.prefs)};
W.database_prefs = p;
if (! root_db)
{
W.cannot_access_panel_1->show_all ();
W.database_prefs.password.set_sensitive (1);
return 0;
}
if (root_db->scalar_result<int>
(0,
"select count(*) from mysql.user where User='%s'",
W.database_prefs.prefs.database_user))
{
W.cannot_access_panel_2->show_all ();
W.database_prefs.password.set_sensitive (1);
return 0;
}
try {auto I {root_db->instruction ()};
I << "create user '"
<< W.database_prefs.prefs.database_user
<< "'";
if (W.database_prefs.prefs.database_password.length ())
I << " identified by '"
<< W.database_prefs.prefs.database_password << "'";
I.execute ();
}
/* Putting in user names containing a hypen produces errors,
* but the user is correctly constructed nonetheless. */
catch (exception&) {}
grant_privileges (*root_db, W);
return 1;
}
static void generate_random_password (Database_Prefs& P)
{
static mt19937 E;
static uniform_int_distribution I {'a', 'z'};
string new_password;
for (int i = 0; i < 20; ++i) new_password += I (E);
Preferences temp_P {P.prefs};
temp_P.database_instance = "";
DB {temp_P}
.instruction ("set password=password('" + new_password + "')");
P.prefs.database_password = new_password;
}
static bool blank_password_problem (Wizard& W, Database_Prefs& P)
{
bool complete {0};
if (! P.prefs.blank_password_forced)
{
if (W.force) { generate_random_password (P);
return 1; }
W.no_password_panel->show_all ();
connect_clicked (W.gpb_i, W.generate_password_button,
[&W, &P]
{ generate_random_password (P);
W.get_database_connection (P.prefs); });
connect_clicked (W.unpb_i, W.use_no_password_button,
[&W, &P]
{ P.prefs.blank_password_forced = 1;
W.get_database_connection (P.prefs); });
}
else /* Blank password forced. */
{
W.blank_password_panel->show_all ();
W.blank_password_ack_button.set_active ();
W.bpab_i.disconnect ();
W.bpab_i = W.blank_password_ack_button
.signal_toggled ().connect ([&W, &P]
{ P.prefs.blank_password_forced = 0;
W.get_database_connection (P.prefs); });
complete = 1;
}
P.password.set_sensitive (1);
return complete;
}
void Wizard::get_database_connection (Prefs P)
{
no_password_panel->hide ();
blank_password_panel->hide ();
cannot_access_panel_1->hide ();
cannot_access_panel_2->hide ();
privilege_panel->hide ();
create_database_panel->hide ();
create_password_panel->hide ();
port_problem_panel->hide ();
socket_problem_panel->hide ();
host_problem_panel->hide ();
database_prefs = P;
database_prefs.password.set_sensitive (0);
database_prefs.submit.set_sensitive (0);
set_page_complete (database_access_page, 0);
bool complete {1};
try { DB db {P}; }
catch (exception& E)
{
cerr << "Database response: " << E.what () << "\n";
if (host_problem (*this, E.what ())
|| socket_problem (*this, E.what ())
|| port_problem (*this, E.what ()))
return;
if (P.database_password.empty ())
complete = no_user_access (*this, E.what ()) && complete;
else
{
string original_password {""};
swap (P.database_password, original_password);
try { { DB db {P}; }
swap (P.database_password, original_password);
complete
= set_password_on_database (*this, database_prefs)
&& complete; }
catch (exception& E)
{
swap (P.database_password, original_password);
complete = no_user_access (*this, E.what ()) && complete;
}
}
}
if (database_prefs.prefs.database_password.empty () && complete)
complete = blank_password_problem (*this, database_prefs);
complete = database_instance_problem (*this, database_prefs)
&& complete;
set_page_complete (database_access_page, complete);
if (force && complete) next_page ();
}
} /* End of namespace DMBCS::Trader_Desk. */

145
trader-desk/wizard.h Normal file
View file

@ -0,0 +1,145 @@
/*
* Copyright (c) 2017, 2020 Dale Mellor
*
* This file is part of the trader-desk package.
*
* The trader-desk package 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 3 of the
* License, or (at your option) any later version.
*
* The trader-desk package 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, see http://www.gnu.org/licenses/.
*/
#ifndef DMBCS__TRADER_DESK__WIZARD__H
#define DMBCS__TRADER_DESK__WIZARD__H
#include <gtkmm.h>
#include <trader-desk/auto-config.h>
#include <trader-desk/preferences.h>
#include <atomic>
namespace DMBCS::Trader_Desk {
#define t_ pgettext
struct Prefs : Preferences { bool non_default_config_file {0};
bool blank_password_forced {0};
string root_password {}; };
struct Database_Prefs : Gtk::Grid
{
Prefs prefs;
Gtk::Entry host;
Gtk::Entry socket;
Gtk::Entry port;
Gtk::Entry database;
Gtk::Entry user;
Gtk::Entry password;
Gtk::Button submit {t_("Label", "Commit change")};
Database_Prefs ();
Database_Prefs& operator= (const Prefs& P);
Prefs update_prefs (Prefs P);
Prefs update_prefs () { return prefs = update_prefs (prefs); }
};
struct Wizard : Gtk::Assistant
{
/* The actual Preferences which we are checking/generating are held
* inside the database_prefs object below. */
bool force;
Gtk::Widget* last_page {0};
Gtk::VBox configuration_page;
Gtk::Entry config_file_entry;
Gtk::Button use_default_config_button {t_("Label", "Use _default"), 1};
sigc::connection udcb_i;
Gtk::Button find_file_button {t_("Label", "_Find file"), 1};
sigc::connection ffb_i;
Gtk::Button change_button {t_("Label", "Change")};
sigc::connection cb_i;
unique_ptr<Gtk::Widget> create_file_panel;
Gtk::Button create_file_button {t_("Label", "Create file")};
sigc::connection cfb_i;
unique_ptr<Gtk::Widget> non_default_panel;
Gtk::CheckButton acknowledge_check {t_("Label", "_Acknowledge"), 1};
sigc::connection ac_i;
Gtk::VBox database_access_page;
Database_Prefs database_prefs;
unique_ptr<Gtk::Widget> no_password_panel;
Gtk::Button generate_password_button
{t_("Label", "_Generate password"), 1};
sigc::connection gpb_i;
Gtk::Button use_no_password_button
{t_("Label", "_Use no password"), 1};
sigc::connection unpb_i;
unique_ptr<Gtk::Widget> blank_password_panel;
Gtk::CheckButton blank_password_ack_button
{t_("Label", "_Acknowledged"), 1};
sigc::connection bpab_i;
unique_ptr<Gtk::Widget> cannot_access_panel_1;
unique_ptr<Gtk::Widget> cannot_access_panel_2;
unique_ptr<Gtk::Widget> privilege_panel;
Gtk::Entry database_root_entry;
unique_ptr<Gtk::Widget> create_database_panel;
Gtk::Button create_database_button
{t_("Label", "Create database"), 1};
sigc::connection cdb_i;
unique_ptr<Gtk::Widget> create_password_panel;
Gtk::Button create_password_button
{t_("Label", "Yes, set password")};
sigc::connection cpb_i;
Gtk::Button blank_password_button
{t_("Label", "No, use blank password")};
sigc::connection bpb_i;
unique_ptr<Gtk::Widget> port_problem_panel;
unique_ptr<Gtk::Widget> socket_problem_panel;
unique_ptr<Gtk::Widget> host_problem_panel;
Gtk::VBox tables_page;
Gtk::VBox data_page;
Gtk::ProgressBar progress_bar;
atomic<int> progress_total;
atomic<int> progress_count;
unique_ptr<Gtk::Widget> database_ready_panel;
unique_ptr<Gtk::Widget> no_data_panel;
int page_order (Gtk::Widget *const, Gtk::Widget *const);
sigc::signal<void, Preferences> signal_wizard_complete;
Wizard (const string& config_file_name, const bool force);
void get_database_connection (Prefs);
};
} /* End of namespace DMBCS::Trader_Desk. */
#endif /* Undefined DMBCS__TRADER_DESK__WIZARD__H. */