1
0

finance.lisp 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.finance
  3. (:use :cl :chatikbot.common))
  4. (in-package :chatikbot.plugins.finance)
  5. (defparameter +yahoo-url+ "https://query1.finance.yahoo.com/v7/finance/quote?lang=en-US&region=US&corsDomain=finance.yahoo.com&fields=regularMarketPrice" "Yahoo Finance API endpoint")
  6. (defparameter +brent-url+ "http://www.cmegroup.com/CmeWS/mvc/Quotes/Future/424/G")
  7. (defvar *default-symbols* '("USDRUB=X" "EURRUB=X" "GBPRUB=X"))
  8. (defparameter +chart-ranges+ (list (cons "day" (* 24 60))
  9. (cons "week" (* 7 24 60))
  10. (cons "month" (* 30 24 60))
  11. (cons "quarter" (* 91 24 60))
  12. (cons "year" (* 365 24 60))))
  13. (defun get-rates (&optional (symbols *default-symbols*))
  14. (let ((response (agets (json-request
  15. +yahoo-url+
  16. :parameters `(("symbols" . ,(format nil "~{~A~^,~}" symbols))))
  17. "quoteResponse")))
  18. (when (aget "error" response)
  19. (error "Error in rates request"))
  20. (loop for rate in (agets response "result")
  21. collect (cons (aget "symbol" rate)
  22. (aget "regularMarketPrice" rate)))))
  23. (defun get-brent ()
  24. (handler-case
  25. (let ((last (read-from-string
  26. (aget "last" (first (aget "quotes" (json-request +brent-url+)))))))
  27. (when (numberp last)
  28. last))
  29. (error (e) (log:error e))))
  30. (defparameter +btc-e-usd-url+ "https://btc-e.com/api/2/btc_usd/ticker" "BTC-e BTC/USD ticker url")
  31. (defun get-btc-e ()
  32. (handler-case
  33. (aget "last" (aget "ticker" (json-request +btc-e-usd-url+)))
  34. (error (e) (log:error e))))
  35. (defparameter +coindesk-btc-usd-url+ "https://api.coindesk.com/site/headerdata.json?currency=BTC" "coindesk.com api endpoint")
  36. (defun get-coindesk-btc-usd ()
  37. (handler-case
  38. (agets (json-request +coindesk-btc-usd-url+)
  39. "bpi" "USD" "rate_float")
  40. (error (e) (log:error "~A" e))))
  41. ;; Formatting
  42. (defvar *symbol-aliases*
  43. (alexandria:plist-hash-table
  44. '("BTCUSD" ("btc" "bitcoin" "биток")
  45. "BRENT" ("чёрная" "нефть" "brent" "oil")
  46. "USDRUB=X" ("зелёный" "бакс" "доллар" "usd")
  47. "EURRUB=X" ("гейро" "eur" "евро")
  48. "GBPRUB=X" ("британец" "gbp" "фунт" "стерлинг")
  49. "CZKRUB=X" ("чешка" "czk")
  50. "DKKRUB=X" ("дашка" "dkk"))
  51. :test #'equal))
  52. (defun get-symbol-label (symbol)
  53. (let ((aliases (gethash symbol *symbol-aliases*)))
  54. (if aliases
  55. (car aliases)
  56. (let ((lower (string-downcase symbol)))
  57. (when (equal "=x" (subseq lower (- (length lower) 2)))
  58. (setf lower (subseq lower 0 (- (length lower) 2))))
  59. (if (= 6 (length lower))
  60. (format nil "~A/~A" (subseq lower 0 3) (subseq lower 3))
  61. lower)))))
  62. ;; Charts
  63. (defun get-serie (series symbol)
  64. (mapcar #'(lambda (r) (list (car r) (caddr r)))
  65. (remove symbol series :key #'cadr :test-not #'equal)))
  66. (defun range (seq &key (key 'identity))
  67. (loop
  68. for item in seq
  69. for value = (funcall key item)
  70. minimizing value into min
  71. maximizing value into max
  72. finally (return (values min max))))
  73. (defvar *chart-points* 800 "Data points in chart")
  74. (defun make-chart (series symbols)
  75. (let* ((r (multiple-value-list (range series :key #'car)))
  76. (fmt (if (<= (- (second r) (first r))
  77. (* 60 60 36))
  78. '((:hour 2) ":" (:min 2))
  79. '((:day 2) "." (:month 2) " " (:hour 2) ":" (:min 2)))))
  80. (adw-charting:with-line-chart ((+ 90 *chart-points*)
  81. (floor (* 3 *chart-points*) 4))
  82. (loop for symbol in symbols
  83. do (adw-charting:add-series (get-symbol-label symbol)
  84. (get-serie series symbol)))
  85. (adw-charting:set-axis
  86. :x "Time" :draw-gridlines-p t
  87. :label-formatter #'(lambda (v)
  88. (local-time:format-timestring
  89. nil
  90. (local-time:unix-to-timestamp v)
  91. :format fmt)))
  92. (adw-charting:set-axis :y "RUB" :draw-gridlines-p t :label-formatter "~,2F")
  93. (adw-charting:save-file "chart.png"))))
  94. ;; Database
  95. (def-db-init
  96. (db-execute "create table if not exists finance_ticker (ts, symbol, price, primary key (ts, symbol)) without rowid")
  97. (db-execute "create table if not exists finance_chat_symbols (chat_id, symbol, primary key (chat_id, symbol)) without rowid"))
  98. (defun migrate ()
  99. (db-execute "delete from finance_ticker")
  100. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, usd from finance where usd is not null" "USDRUB=X")
  101. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, eur from finance where eur is not null" "EURRUB=X")
  102. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, gbp from finance where gbp is not null" "GBPRUB=X")
  103. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, brent from finance where brent is not null" "BRENT")
  104. (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, btc from finance where btc is not null" "BTCUSD"))
  105. (defun db/add-finance (ts symbol price)
  106. (db-execute "insert into finance_ticker (ts, symbol, price) values (?, ?, ?)" ts symbol price))
  107. (defun db/get-last-finance (&optional (symbols *default-symbols*))
  108. (loop for symbol in symbols
  109. for last = (db-select "select ts, symbol, price from finance_ticker where symbol = ? order by ts desc limit 1"
  110. symbol)
  111. when last append last))
  112. (defun db/get-chat-symbols (&optional chat-id)
  113. (mapcar #'car
  114. (if chat-id
  115. (db-select "select symbol from finance_chat_symbols where chat_id = ?" chat-id)
  116. (db-select "select distinct symbol from finance_chat_symbols"))))
  117. (defun db/set-chat-symbols (chat-id symbols)
  118. (db-transaction
  119. (db-execute "delete from finance_chat_symbols where chat_id = ?" chat-id)
  120. (dolist (symbol symbols)
  121. (db-execute "insert into finance_chat_symbols (chat_id, symbol) values (?, ?)"
  122. chat-id symbol))))
  123. (defun db/get-series (after-ts &optional (symbols *default-symbols*) (avg 60))
  124. (when symbols
  125. (let ((sql (format nil
  126. "select ts/~a*~:*~a, symbol, avg(price) from finance_ticker where ts >= ? and symbol in (~{?~*~^, ~}) group by ts/~a, symbol order by ts"
  127. avg symbols avg)))
  128. (apply #'db-select sql (local-time:timestamp-to-unix after-ts) symbols))))
  129. ;; Cron
  130. (defcron process-rates ()
  131. (let* ((ts (local-time:timestamp-to-unix (local-time:now)))
  132. (symbols (db/get-chat-symbols))
  133. (rates (when symbols (get-rates symbols)))
  134. (brent (get-brent))
  135. (btc (get-coindesk-btc-usd)))
  136. (loop for (symbol . price) in rates
  137. do (db/add-finance ts symbol price))
  138. (when brent
  139. (db/add-finance ts "BRENT" brent))
  140. (when btc
  141. (db/add-finance ts "BTCUSD" btc))))
  142. ;;; Hooks
  143. (defun get-label-symbol (label)
  144. (or
  145. (loop for symbol being the hash-keys in *symbol-aliases* using (hash-value aliases)
  146. when (member label aliases :test #'string-equal)
  147. do (return symbol))
  148. (string-upcase label)))
  149. (def-message-cmd-handler handler-rates (:rates)
  150. (when args
  151. (db/set-chat-symbols chat-id (mapcar #'get-label-symbol args)))
  152. (let ((last (db/get-last-finance (or (db/get-chat-symbols chat-id) *default-symbols*))))
  153. (bot-send-message chat-id
  154. (if last
  155. (format nil "~{~A *~$*~^, ~} @ _~A_"
  156. (loop for (ts symbol price) in last
  157. append (list (get-symbol-label symbol) price))
  158. (format-ts (local-time:unix-to-timestamp (caar last))))
  159. "Пока нет данных. Если тикер реальный, проверь через минуту")
  160. :parse-mode "markdown")))
  161. (def-message-cmd-handler handler-charts (:charts)
  162. (telegram-send-chat-action chat-id "upload_photo")
  163. (let* ((args (mapcar 'string-downcase args))
  164. (all-symbols (db/get-chat-symbols))
  165. (symbols (or (intersection (mapcar #'get-label-symbol args) all-symbols :test 'equal) all-symbols))
  166. (day-range (some #'(lambda (a) (aget a +chart-ranges+)) args))
  167. (number (some #'(lambda (a) (parse-integer a :junk-allowed t)) args))
  168. (avg (* 60 (cond
  169. (day-range (round day-range *chart-points*))
  170. (number)
  171. (:otherwise 1))))
  172. (after-ts (local-time:timestamp- (local-time:now)
  173. (* avg *chart-points*) :sec))
  174. (rates (db/get-last-finance symbols))
  175. (chart (make-chart (db/get-series after-ts symbols avg) symbols)))
  176. (telegram-send-photo chat-id chart
  177. :caption
  178. (format nil "~{~A ~$~^, ~} @ ~A"
  179. (loop for (ts symbol price) in rates
  180. append (list (get-symbol-label symbol) price))
  181. (format-ts (local-time:unix-to-timestamp (caar rates)))))))