diff options
author | David <171410+dmb2@users.noreply.github.com> | 2023-05-27 08:59:14 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-27 08:59:14 -0400 |
commit | 7f8b48106ea2399ed679b83036602e8e0f13b62c (patch) | |
tree | 44c581b41e4e99c500199b5d2877f87a6b0772a4 | |
parent | 36daccd715e1cc6c1badab7cd87e34a8514f3b6b (diff) | |
parent | 5d62f0ec703d6e6c439fd0dfad7775f69378a247 (diff) |
Merge pull request #271 from santiagopim/ticker-new-module
Ticker new module
-rw-r--r-- | README.org | 1 | ||||
-rw-r--r-- | modeline/bitcoin/README.org | 2 | ||||
-rw-r--r-- | modeline/ticker/README.org | 142 | ||||
-rw-r--r-- | modeline/ticker/package.lisp | 6 | ||||
-rw-r--r-- | modeline/ticker/screenshot.png | bin | 0 -> 124164 bytes | |||
-rw-r--r-- | modeline/ticker/ticker.asd | 13 | ||||
-rw-r--r-- | modeline/ticker/ticker.lisp | 231 |
7 files changed, 395 insertions, 0 deletions
@@ -98,6 +98,7 @@ Advertise your module here, open a PR and include a org-mode link! - [[./modeline/mem/README.org][mem]] :: Display memory in the modeline, %M conflicts with maildir. - [[./modeline/net/README.org][net]] :: Displays information about the current network connection. - [[./modeline/stumptray/README.org][stumptray]] :: System Tray for stumpwm. +- [[./modeline/ticker/README.org][ticker]] :: Display ticker price on StumpWM modeline. - [[./modeline/wifi/README.org][wifi]] :: Display information about your wifi. ** Utilities - [[./util/alert-me/README.org][alert-me]] :: Alert me that an event is coming diff --git a/modeline/bitcoin/README.org b/modeline/bitcoin/README.org index e1625fd..7d0d386 100644 --- a/modeline/bitcoin/README.org +++ b/modeline/bitcoin/README.org @@ -1,5 +1,7 @@ * Bitcoin +*THIS MODULE IS DEPRECATED, AND SUPERSEDED BY THE* =ticker= *MODULE*. + Show Bitcoin (₿) value in the modeline. ** Usage diff --git a/modeline/ticker/README.org b/modeline/ticker/README.org new file mode 100644 index 0000000..e8cb387 --- /dev/null +++ b/modeline/ticker/README.org @@ -0,0 +1,142 @@ +* Ticker + +This module prints off values from stocks. It is developed with +cryptocurrencies in mind, so the default API target is the [[https://kraken.com/][Kraken]] +servers. + +[[./screenshot.png]] + +** Dependencies + +It gets actual price through API, so needs =dexador= and =yason=. +Also, the price getter is asynchronous with the =lparallel= machinery. + +#+begin_src lisp + (ql:quickload '("dexador" "yason" "lparallel")) +#+end_src + +** Usage + +Place the following in your =~/.stumpwmrc= file: + +#+begin_src lisp + (load-module "ticker") +#+end_src + +Use =%T= in your mode line format, for example: + +#+begin_src lisp + (setf *screen-mode-line-format* + (list "[%n]" ; Groups + "%v" ; Windows + "^>" ; Push right + " | %T" ; Ticker <<<--- this module + " | %d")) ; Clock +#+end_src + +And define some tickers with its parameters. This line defines one +ticker that defaults to the Bitcoin/USD pair: + +#+begin_src lisp + (ticker:define-ticker) ; Bitcoin as default +#+end_src + +You can define more tickers and parameterize as desired, see the [[Notes]]: + +#+begin_src lisp + (ticker:define-ticker + :symbol "XBT" ;;"₿" ; Bitcoin + :threshold 0.01) + (ticker:define-ticker + :pair "XETHZUSD" ; Ethereum + :symbol "ETH") + (ticker:define-ticker + :pair "ADAUSD" ; Cardano + :symbol "ADA" ;;"₳" + :threshold 0.0001 + :delay 60 + :decimals 3 + :gauge-width 9) +#+end_src + +** Notes + +The parameters that can be customized when defining a ticker and its +default values are: + +#+begin_src lisp + :pair "XXBTZUSD" ; pair to get from API + :symbol "BTC" ; label the ticker + :colors t ; use colors + :threshold 0.001 ; 0.1% deviation from average to colorize + :delay 30 ; seconds between updates + :decimals 0 ; number of decimal digits + :localization 2 ; formatting number + :gauge-width 7 ; width of the gauge bar in characters +#+end_src + +The minimum parameters to define are the =:pair= to get the value from +the API, and the =:symbol= to label the ticker in the modeline. The +=:pair= is one of the listed at: + ++ [[https://api.kraken.com/0/public/Ticker]] + +The =:symbol= is a string and can be blank. + +Price format is colorized depending on the =:colors= flag. You can +customize setting it to =t= or =nil= when defining the ticker. + +Colors depends on a comparison between actual value and the last +values average: + +| Color | Code | Description | +|---------------+---------+-----------------------------------| +| Bright yellow | =^B^3*= | Price is higher than average | +| Red | =^1*= | Price is below average | +| White | =^7*= | Price is similar to average | +| Default color | =^**= | When *modeline-use-colors* is nil | + +There is a threshold around average, so the increasing or decreasing +color is only applied if =:threshold= is passed. + +Last values average is calculated over a 3 hours values list, where +values are stored on every modeline refresh in a FIFO fashion. + +Connection to the API price server is limited by a =:delay= interval, +in seconds. So connection attempts between interval time are blocked. + +The number of decimal places is set by =:decimals=, when =0= there is +no decimals. + +The localization format is set by =:localization= code, when =0= there +is no thousand separator, gives =1234.56= and the =:decimals= +parameter does not work, when =1= the thousand separator is =comma= +and gives =1,234.56=, when =2= the thousand separator is =period= and +gives =1.234,56=, and when =3= the thousand separator is =space= and +gives =1 234,56=. + +It is possible to add a gauge bar with the tendency of the actual +value between the low and high in the last 24 hours with +=:gauge-width=. Value must be greater than =1= to be shown. + +There is an external parameter =*tickers-separator*= that defines the +string to put between tickers, as a separator. Can be customized, but +be aware not to use tilde "~" or other combinations because it is +interpreted by the =format= function: + +#+begin_src lisp + (setf ticker:*tickers-separator* " | ") +#+end_src + +** Issues + +Try to use conditions' =handler-case= machinery to avoid the internet +timeouts or the computer sleeping process, to stuck the modeline. + +The =truncate= function is used when formatting the values, so some +precission loss is expected. + +There is an internal function =ticker::reset-tickers= that closes all +=lparallel= tasks and kernels and resets the =*tickers*= list. Once +called, if you want to redefine new tickers, should wait up to the +maximum =delay= interval. diff --git a/modeline/ticker/package.lisp b/modeline/ticker/package.lisp new file mode 100644 index 0000000..d777628 --- /dev/null +++ b/modeline/ticker/package.lisp @@ -0,0 +1,6 @@ +;;;; package.lisp + +(defpackage :ticker + (:use :cl) + (:export #:define-ticker + #:*tickers-separator*)) diff --git a/modeline/ticker/screenshot.png b/modeline/ticker/screenshot.png Binary files differnew file mode 100644 index 0000000..e88be3c --- /dev/null +++ b/modeline/ticker/screenshot.png diff --git a/modeline/ticker/ticker.asd b/modeline/ticker/ticker.asd new file mode 100644 index 0000000..e44a710 --- /dev/null +++ b/modeline/ticker/ticker.asd @@ -0,0 +1,13 @@ +;;;; bitcoin.asd + +(asdf:defsystem "ticker" + :description "Display ticker price on StumpWM modeline." + :author "Santiago Payà Miralta @santiagopim" + :license "MIT" + :homepage "https://github.com/stumpwm/stumpwm-contrib/" + :depends-on ("stumpwm" ; Use add-screen-mode-line-formatter + "lparallel" ; Connect to API with concurrency + "dexador" ; Get data from url + "yason") ; Parse json + :components ((:file "package") + (:file "ticker" :depends-on ("package")))) diff --git a/modeline/ticker/ticker.lisp b/modeline/ticker/ticker.lisp new file mode 100644 index 0000000..4d05ac8 --- /dev/null +++ b/modeline/ticker/ticker.lisp @@ -0,0 +1,231 @@ +;;;; ticker.lisp + +;;; Ticker formatter for the Stumpwm mode-line. There is no timestamp, +;;; so let's store up to some historical serie size values got from +;;; url and calculate its average. Comparing actual value with this +;;; average, set a color format. Adds a gauge control that draws +;;; tendency between low and high in 24 hours values. + +;;; When creating a new ticker, it launches an asynchronous process +;;; that reads values every delay time from the API and stores them in +;;; the structure. The mode-line uses those structures to print the +;;; values on every refresh. + +;;; CODE: + +(in-package :ticker) + +(defstruct ticker + "Parameters of the ticker and state variables." + pair ; get from API url + symbol ; to show in modeline + colors ; show colors + threshold ; color change interval + delay ; update interval + decimals ; digits in decimal part + localization ; thousands/comma format + gauge-width ; width of gauge in characters + ;; Internal state variables + (values ()) ; store the last 3 hours values + (value 0.0) ; last value got from url + (values-low 0.0) ; low value last 24h + (values-high 0.0) ; high value last 24h + (values-average 0.0)) ; average last 3 hours values + +;;; Exported + +(defun define-ticker (&key (pair "XXBTZUSD") (symbol "BTC") (colors t) + (threshold 0.001) (delay 30) (decimals 0) + (localization 2) (gauge-width 7)) + "Ticker constructor which defaults to Bitcoin and 3 hours historical values." + (let ((ticker (make-ticker + :pair pair + :symbol symbol + :colors colors + :threshold threshold + :delay delay + :decimals decimals + :localization localization + :gauge-width gauge-width + ;; Internal state variable + :values (make-list (truncate (/ (* 3 60 60) ; 3 hours + delay)) + :initial-element NIL)))) + ;; Push the `ticker' into the `*tickers*' list, and launch the + ;; asynchronous process that will update the values from the API + ;; every `delay' seconds. + (push ticker *tickers*) + (let ((lparallel:*kernel* + (lparallel:make-kernel 1 :name pair))) + (lparallel:submit-task (lparallel:make-channel) + (lambda () + (parallel-getter (car *tickers*))))))) + +(defparameter *tickers-separator* " | " + "String to separate between tickers in de modeline.") + +;;; Global variables + +(defparameter *tickers* () + "List of tickers to show.") + +(defparameter *url* "https://api.kraken.com/0/public/Ticker?pair=" + "Location of price provider, the ticker pair will be concatenated.") + +(defparameter *stop-parallel-getters* nil + "When `t' stop and close all asynchronous loops that get the tickers +values.") + +;;; Get the values + +(defun parallel-getter (tick) + "The values are stored in the `*tickers*' structure, from where can be +read by the `ticker-modeline' function." + (do () + (*stop-parallel-getters* + (lparallel:end-kernel)) + ;; Store actual, 24h low, and 24h high values from the `*url*' API. + ;; If there is no response, store just `nil' values. + (let ((values + (let* ((url (concatenate 'string *url* (ticker-pair tick))) + (response (handler-case + (gethash (ticker-pair tick) + (gethash "result" + (yason:parse + (dexador:get url + :keep-alive nil)))) + ;; Return NIL in case some condition is triggered + (condition () nil)))) + (if response + (list (read-from-string (first (gethash "c" response))) + (read-from-string (second (gethash "l" response))) + (read-from-string (second (gethash "h" response)))) + (list nil nil nil))))) + ;; From actual, 24 low, and 24h high, calculate average and + ;; store all in the `*tickers*' ticker. + (setf (ticker-value tick) (first values) + (ticker-values-low tick) (second values) + (ticker-values-high tick) (third values)) + ;; Add value to values list, pushing to front + (push (ticker-value tick) (ticker-values tick)) + ;; Preserve values list size, popping from end + (setf (ticker-values tick) (nreverse (ticker-values tick))) + (pop (ticker-values tick)) + (setf (ticker-values tick) (nreverse (ticker-values tick))) + ;; Calculate average of values, excluding NIL values + ;; that could exist because network issues. + (let ((values-clean (remove-if-not #'numberp (ticker-values tick)))) + (setf (ticker-values-average tick) (/ (reduce #'+ values-clean) + (max 1 (length values-clean)))))) + ;; And again + (sleep (ticker-delay tick)))) + +(defun reset-tickers () + "Stop the getters and reset the list of tickers." + (let ((max-delay (reduce 'max + *tickers* + :key 'ticker-delay + :initial-value 0))) + (setf *stop-parallel-getters* t) + ;; Reset the flag to nil after some time + (let ((lparallel:*kernel* (lparallel:make-kernel 1))) + (lparallel:submit-task + (lparallel:make-channel) + (lambda () + (sleep (1+ max-delay)) + (setf *stop-parallel-getters* nil) + (lparallel:end-kernel)))) + ;; Reset the *tickers* list + (setf *tickers* ()))) + +;;; Write on modeline + +(defun format-decimal (n sep int com dec) + "Return Number formated in groups of INTerval length every, and +separated by SEParator, with COMma character as decimal separator. +DECimals is the number of digits in the decimal part. All parameters +but N are strings. COMma character should not be the tilde `~'. + +Works as a simple formatting positive numbers using directive `~D', +for thousand separator and direct value displacement in the decimal +part. Uses `truncate' so there is some precission loss. Does NOT work +with negative numbers. + +Based on https://stackoverflow.com/questions/35012859" + (let* ((num-string (concatenate 'string "~,,'" sep "," int ":D")) + (decimals (format nil "~D" dec)) + (dec-string (concatenate 'string com "~" decimals ",'0D"))) + (multiple-value-bind (i r) (truncate n) + (concatenate + 'string + (format nil num-string i) + (when (< 0 dec) + (format nil dec-string (truncate (* (expt 10 dec) r)))))))) + +(defun gauge (v l h n) + "Draw a gauge control with Value at the point between Low and High in +an N length control." + (if (and (< l h) (<= l v) (<= v h) (> n 1)) + (let* ((line (make-sequence 'string n :initial-element #\-)) + (segment (floor (* n (/ (- v l) (- h l))))) + (segment (if (= v h) (1- segment) segment))) + (replace line "*" :start1 segment)) + "-*-*-")) + +(defun get-value-string (tick) + "Generate the ticker string to show in modeline." + (let ((results ())) + (when (< 0 (length (ticker-symbol tick))) + (push (ticker-symbol tick) results)) + (push (case (ticker-localization tick) + (0 (format nil "~,2F" (ticker-value tick))) + (1 (format-decimal (ticker-value tick) "," "3" "." + (ticker-decimals tick))) + (2 (format-decimal (ticker-value tick) "." "3" "," + (ticker-decimals tick))) + (3 (format-decimal (ticker-value tick) " " "3" "," + (ticker-decimals tick))) + (otherwise (format nil "~,2F" (ticker-value tick)))) + results) + (when (< 1 (ticker-gauge-width tick)) + (push (gauge (ticker-value tick) + (ticker-values-low tick) + (ticker-values-high tick) + (ticker-gauge-width tick)) + results)) + (format nil "~{~A~^ ~}" (nreverse results)))) + +(defun ticker-modeline (ml) + "This function is evaluated on every modeline refresh and returns the +modeline string. The values are always printed off, but only updated +by the `parallel-getter' function when the `delay' interval has been +reached. If there are not returned values from the API (nil), then the +ticker name is printed." + (declare (ignore ml)) + (if *tickers* + (let ((results ())) + (dolist (tick *tickers*) + (if (and (numberp (ticker-value tick)) (plusp (ticker-value tick))) + ;; Actual value is a positive number, so print off + (let ((value-string (get-value-string tick))) + ;; Return with color if desired + (push (if (ticker-colors tick) + (let* ((diff (- (ticker-value tick) (ticker-values-average tick))) + (pdiff (/ diff (max 1 (ticker-value tick))))) + (cond ((> pdiff (ticker-threshold tick)) + (format nil "^[^B^3*~A^]" value-string)) + ((< pdiff (- (ticker-threshold tick))) + (format nil "^[^1*~A^]" value-string)) + (t (format nil "^[^7*~A^]" value-string)))) + (format nil "^[^**~A^]" value-string)) + results)) + ;; The value is not a positive number, set the tick name as response + (push (format nil "-~A-" (ticker-pair tick)) results))) + ;; Return aggregated ticks results with proper separator + (let ((s (concatenate 'string "~{~A~^" *tickers-separator* "~}"))) + (format nil s results))) + ;; There are no tickers defined + "-Ticker-")) + +;; Bind modeline formatter character to the drawer function +(stumpwm:add-screen-mode-line-formatter #\T 'ticker-modeline) |