СТАТЬЯ

Анализ временных рядов цен на акции с помощью массивов Fortran, часть 2

Из Современного Фортрана Милана Курчича
________________________________________________________________________________

Получите скидку 37% на Современный Фортран. Просто введите код fcccurcic в поле скидки при оформлении заказа на manning.com.
_______________________________________________________________________

В этой части статьи мы углубимся в явное распределение и нарезку массивов Fortran и продемонстрируем, как использовать эти методы для поиска лучших и худших акций.

Часть 2. Выделение, индексирование и нарезка массивов для анализа цен на акции

Эта статья является второй частью серии статей об анализе временных рядов цен на акции с помощью массивов Fortran.

В Части 1 я рассмотрел основы объявления и инициализации динамических массивов Fortran. Я также представил конструкторы массивов, которые обеспечивают простой способ выделения и инициализации массива в одной строке кода. Если вы еще не читали часть 1, я предлагаю вам вернуться и прочитать ее, прежде чем двигаться дальше. Во второй части мы подробно изучим, как работает выделение и освобождение массивов Fortran, позволяя вам размещать массивы с произвольными диапазонами (начальный и конечный индексы). Вы также узнаете, как разрезать массивы в любом направлении и с помощью пользовательского шага, чтобы подмножить именно те элементы, которые вам нужны. Наконец, в этой части мы применим эти знания для реализации решения нашей первой задачи — поиска акций с наилучшей и худшей доходностью.

Выделение массивов определенного размера или диапазона

В части 1 мы узнали, как объявлять и инициализировать динамические массивы. Однако что, если нам нужно присвоить значения отдельным элементам массива один за другим в цикле? Это будет иметь место, когда мы загружаем данные из файлов CSV в массивы — мы будем перебирать записи в файлах и присваивать значения элементам массива по одному. Однако на самом деле у нас нет способа инициализировать массивы данных, как мы это делали раньше с stock_symbols. Обратите внимание, что неявное выделение путем присвоения пустому массиву [integer ::] или [real ::] здесь не сработает, потому что нам может понадобиться индексировать элементы массива в каком-то другом порядке, чем просто добавление значений. Это требует более явного механизма для выделения массива без присвоения ему известных значений:

real, allocatable :: a(:) ! declare a dynamic array a  
integer :: im = 5 !  allocate(a(im)) ! 
allocate memory for array a with im elements

Приведенный выше оператор указывает программе зарезервировать память для массива a размера im, в данном случае 5. При таком вызове a по умолчанию будет иметь нижнюю границу 1 и верхнюю границу im. Нижняя граница 1 используется по умолчанию, аналогично Julia, R или MATLAB. Это отличается от C, C++, Python или JavaScript, где индексы массива или списка начинаются с 0.

Однако Fortran не ограничивает начальный индекс равным 1, в отличие от Python, где первый индекс всегда равен 0. Вы можете указать нижнюю и верхнюю границы в операторе распределения:

integer :: is = -5, ie = 10  
allocate(a(is:ie)) ! Allocate a with range from is to ie inclusive

Обратите внимание, что я знаю, что использовал двоеточие (:) между is и ie для указания диапазона. Этот диапазон является инклюзивным (в отличие от Python!), поэтому размер a теперь равен ie - is + 1, в данном случае 16. Вы можете использовать встроенные функции lbound и ubound для получения нижней и верхней границы соответственно любого массива.

Выделение массива из другого массива

Также возможно динамическое выделение массива на основе размера другого массива. Оператор allocate принимает два необязательных аргумента:

  • mold — переменная или выражение того же типа, что и выделяемый объект.
  • source — эквивалентно mold, за исключением того, что значения source используются для инициализации выделяемого объекта.

Например, выделение с помощью a из b с помощью пресс-формы зарезервирует место в памяти для a, но не инициализирует его элементы:

real, allocatable :: a(:), b(:)  
allocate(b(10:20))  
allocate(a, mold=b) ! allocate a with same range and size as b  
a = 0

Однако, если мы выделим a из b с помощью source, оно будет выделено и инициализировано значениями b:

real, allocatable :: a(:), b(:)  
b = [1.0, 2.0, 3.0]  
allocate(a, source=b) ! allocate and initialize a from b

Совет по стилю. Независимо от того, как вы распределяете массивы, всегда инициализируйте их сразу после выделения. Это сведет к минимуму вероятность случайного использования неинициализированных массивов в выражениях. Хотя Fortran позволит вам это сделать, вы, скорее всего, получите бессвязный результат.

Возможно, вы заметили, что при описании конструкторов массивов ранее я инициализировал массивы без явного выделения их с помощью оператора allocate. Почему я это сделал? Вы можете справедливо спросить, нужно ли явно выделять массивы или нет? Начиная с Fortran 2003, в языке появилась удобная функция, называемая размещением по назначению.

Автоматическое выделение при назначении. Если вы присваиваете массив размещаемой переменной массива, целевая переменная массива автоматически выделяется с правильным размером, чтобы соответствовать массиву в правой части. Переменная массива может быть уже выделена или нет. Если это так, он будет перераспределен, если его текущий размер отличается от размера исходного массива. Отличным примером для игры с этим является добавление элементов к массиву на лету:

integer, allocatable :: a(:)     
a = [integer ::] ! create an empty array []  
a = [a, 1] ! append 1 to a, now [1]  
a = [a, 2] ! append 2 to a, now [1, 2]  
a = [a, 2 * a] ! [1, 2, 2, 4]

Эта функция особенно полезна при попытке присвоить массив, который является результатом функции и размер которого заранее неизвестен.

Есть еще одно важное различие между явным выделением с помощью оператора allocate и выделением по присваиванию. Первый вызовет ошибку времени выполнения, если он будет запущен дважды, то есть если вы выполните оператор allocate для уже выделенного объекта. С другой стороны, последний изящно перераспределит массив, если он уже выделен. Чтобы иметь возможность эффективно повторно использовать динамические массивы, Fortran предоставляет в качестве аналога оператору allocate, который позволяет нам явно удалить объект из памяти.

Уборка после использования

Когда мы закончили работу с массивом, мы можем очистить его из памяти следующим образом:

deallocate(a) ! clear a from memory

Автоматическое освобождение. Распределяемый массив автоматически освобождается, когда он выходит за пределы области действия. Например, если вы объявляете и выделяете массив внутри функции или подпрограммы, он будет освобожден по возвращении.

После выдачи deallocate массив a должен быть снова выделен, прежде чем использовать его в правой части выражений. Мы применим этот механизм для повторного использования массивов между разными акциями.

Подобно тому, как выделение объекта, который уже выделен, является ошибкой, так же ошибкой является и освобождение объекта, который еще не выделен! В следующем разделе я объясню, как вы можете проверить статус распределения, чтобы никогда не было ошибочного выделения или освобождения и возражения дважды. В противном случае нет никаких ограничений в отношении того, был ли массив инициализирован или нет. Вы можете освободить неинициализированный массив, например, если вы узнаете, что массивы не имеют ожидаемого размера или похожи.

Совет по стилю. Освобождайте все выделяемые переменные, когда закончите с ними работать.

На рис. 2 показан типичный жизненный цикл динамического массива.

Сначала мы объявляем массив как allocatable. На данный момент массив еще не выделен в памяти и его размер не указан. Когда все будет готово, мы выполним оператор allocate, чтобы зарезервировать кусок памяти для хранения этого массива. Здесь же мы определяем размер массива или начальный и конечный индексы (в данном примере 3 и 8). Если выделение не из другого исходного массива, значения будут неинициализированы. Таким образом, нам нужно инициализировать массив, прежде чем делать с ним что-либо еще. Наконец, закончив работу с массивом, мы выполним оператор deallocate, чтобы освободить память, в которой находился массив. Статус массива снова стал нераспределенным, и он доступен для выделения. Динамический массив можно использовать повторно любое количество раз, даже с разными размерами или начальным и конечным индексами. Это именно то, что мы будем делать в нашем приложении для анализа цен на акции. Для каждой акции мы будем выделять массивы, использовать их для загрузки данных из файлов, работать с ними, а затем освобождать их перед передачей в следующую акцию.

Проверка статуса распределения

Иногда будет полезно знать статус выделения переменной, то есть, выделена ли она в данный момент или нет. Для этого мы можем использовать встроенную функцию allocated:

real, allocatable :: a(:)  
print *, allocated(a) ! will print “F”  
allocate(a(10))  
print *, allocated(a) ! will print “T”  
deallocate(a)  
print *, allocated(a) ! will print “F”

Попытка выделить уже выделенную переменную или освободить невыделенную переменную вызовет ошибку времени выполнения.

Совет по стилю. Всегда проверяйте статус выделения перед явным выделением или освобождением переменной.

Обнаружение ошибок выделения и освобождения

Ваши выделения и освобождения иногда будут давать сбой. Это может произойти, если вы попытаетесь выделить больше памяти, чем доступно, если вы попытаетесь выделить уже выделенный объект или освободить уже освобожденный объект. Когда это произойдет, программа прервется. Однако оператор allocate также поставляется со встроенной обработкой исключений, если вам нужен более точный контроль над тем, что происходит, когда выделение не удается:

allocate(u(im), stat=stat, errmsg=err)

где stat и errmsg — необязательные аргументы:

  • statinteger, указывающий статус оператора allocate. stat будет равно нулю, если выделение прошло успешно, в противном случае это будет ненулевое положительное число.
  • errmsg — строка character, которая содержит сообщение об ошибке, если произошла ошибка (например, stat не равно нулю), и не определена в противном случае.

Используя встроенную обработку исключений, вы получаете возможность решить, как программа должна действовать в случае сбоя выделения. Например, если не хватает памяти для размещения большого массива, возможно, мы можем разделить работу на более мелкие фрагменты. Даже если вы хотите, чтобы программа останавливалась при сбое выделения памяти, этот подход позволяет корректно справляться с проблемами и выводить осмысленное сообщение об ошибке.

Совет по стилю. Если вы хотите управлять тем, что произойдет в случае сбоя (де)распределения, используйте stat и errmsg в операторах allocate и deallocate, чтобы отлавливать любые ошибки, которые могут возникнуть. Конечно, вам все равно нужно указать программе, что делать в случае возникновения ошибки, например, остановить программу с помощью пользовательского сообщения, распечатать предупреждающее сообщение и продолжить работу или попытаться восстановить каким-либо другим способом.

Я думаю, нам следует использовать встроенную обработку исключений в нашем приложении для анализа акций. Однако нам понадобится это для нескольких массивов. Это кажется подходящим для реализации один раз в подпрограмме, а затем повторного использования по мере необходимости. Явное выделение и освобождение массивов может быть довольно утомительным. Это особенно верно, если вы решите использовать встроенную обработку исключений. Если вы работаете со многими разными массивами одновременно, это может быстро привести к большому количеству шаблонного кода.

Давайте реализуем простые подпрограммы alloc и free, которые соответственно выделяют и освобождают входной массив, а также обрабатывают исключения. Обе подпрограммы должны использовать аргументы stat и errmsg, чтобы перехватывать и сообщать о любых ошибках, если они происходят. После реализации вы сможете выделять и освобождать свои массивы следующим образом:

call alloc(a, 5)  
! do work with a  
call free(a)

Начнем с подпрограммы распределителя alloc. Чтобы ключевые функции работали, наша подпрограмма должна:

  1. Убедитесь, что входной массив уже выделен, и если да, освободите его, прежде чем продолжить.
  2. Выделите массив с входным размером n.
  3. Если во время выделения возникает исключение, прервите программу и выведите на экран сообщение об ошибке.

Вот реализация:

subroutine alloc(a, n)    
  real, allocatable, intent(in out) :: a(:)    
  integer, intent(in) :: n    
  integer :: stat    
  character(len=100) :: errmsg    
  if (allocated(a)) call free(a)    
  allocate(a(n), stat=stat, errmsg=errmsg)    
  if (stat > 0) error stop errmsg  
end subroutine alloc

Теперь давайте посмотрим на реализацию подпрограммы free:

subroutine free(a)    
  real, allocatable, intent(in out) :: a(:)    
  integer :: stat    
  character(len=100) :: errmsg      
  if (.not. allocated(a)) return    
  deallocate(a, stat=stat, errmsg=errmsg)    
  if (stat > 0) error stop errmsg  
end subroutine free

Код очень похож на alloc за исключением того, что здесь в начале исполняемой части кода мы проверяем, не выделен ли уже a. Если нет, то наша работа здесь сделана, и мы можем немедленно вернуться.

Эти подпрограммы также являются частью репозитория stock-prices. Вы можете найти их в stock_prices/src/mod_alloc.f90, и они используются программой чтения CSV в stock_prices/src/mod_io.f90. Мы будем использовать эти вспомогательные подпрограммы, чтобы значительно сократить шаблон в подпрограмме read_stock.

Реализация подпрограммы чтения CSV

Рассмотрев детальную механику выделения и освобождения массивов, включая встроенную обработку исключений, мы, наконец, подошли к реализации подпрограммы чтения CSV-файлов:

subroutine read_stock(filename, time, open, high,&
                       low, close, adjclose, volume)
  ...
  integer :: fileunit
  integer :: n, nm
  
  nm = num_records(filename) - 1
  
  if (allocated(time)) deallocate(time)
  allocate(character(len=10) :: time(nm))
  call alloc(open, nm)
  call alloc(high, nm)
  call alloc(low, nm)
  call alloc(close, nm)
  call alloc(adjclose, nm)
  call alloc(volume, nm)
  
  open(newunit=fileunit, file=filename) ! open the file
  read(fileunit, fmt=*, end=1) ! use read() to skip the CSV header
  do n = 1, nm ! loop over records and store into array elements
   read(fileunit, fmt=*, end=1) time(n), open(n),&
     high(n), low(n), close(n), adjclose(n), volume(n)
  end do
  1 close(fileunit) ! close file when done
  
end subroutine read_stock

Чтобы узнать длину массивов перед их выделением, я запрашиваю длину CSV-файла с помощью пользовательской функции num_records, определенной в stock-prices/src/mod_io.f90. Если вам интересно, что означает число 1 в 1 close(fileunit), то это просто метка строки, которую Фортран использует, если и когда он встречает исключение в операторах read(fileunit, fmt=*, end=1). Если вам интересно, как работает эта функция, загляните внутрь stock-prices/src/mod_io.f90. Я не буду тратить здесь много времени на код, специфичный для ввода-вывода, так как нам просто нужно, чтобы он приступил к анализу массива.

При каждой записи подпрограммы массивы time, open, high, low, close, adjclose и том будут выделены размером nm. Подпрограмма alloc теперь легко перераспределяет массивы для нас. Обратите внимание, что мы по-прежнему используем явный способ выделения и освобождения массива меток времени. Это потому, что мы реализовали удобные подпрограммы alloc и free, которые работают с реальными массивами. Из-за строгой дисциплины типизации Фортрана мы не можем просто передать массив строк подпрограмме, которая ожидает массив вещественных чисел. Позже мы узнаем, как писать универсальные процедуры, которые могут принимать аргументы разных типов. На данный момент будет достаточно явного выделения массива меток времени. Кроме того, нам также необходимо указать длину строки при размещении массива time.

Прочитав CSV-файлы и загрузив массивы цен акций данными, мы можем перейти к фактическому анализу и развлечься с массивами.

Получение количества строк в текстовом файле.Если вам интересно, как реализована функция num_records, не стесняйтесь взглянуть на stock-prices/src/mod_io.f90. Эта функция открывает файл и подсчитывает количество строк, читая его построчно.

Индексирование и нарезка массивов

Вы заметили, что биржевые данные в CSV-файлах упорядочены от самых последних к самым старым? Это означает, что когда мы читаем его в массивы сверху вниз, первый элемент будет соответствовать самой последней цене акции. Давайте перевернем массивы, чтобы они были ориентированы более естественным образом, продвигаясь вперед во времени с номером индекса. Если мы представим обратную операцию как функцию, мы сможем применить ее к любому массиву, например:

adjclose = reverse(adjclose)

Функция reverse окажется полезной для двух других целей приложения курсов акций. Перед его реализацией нам нужно кое-что узнать о том, как работает индексация и нарезка массива.

Чтобы выбрать один элемент, мы заключаем в круглые скобки целочисленный индекс, например, adjclose(1) будет относиться к первому элементу массива, adjclose(5) — к пятому и так далее.

Чтобы выбрать диапазон элементов, например, от пятого до десятого, используйте начальный и конечный индексы в круглых скобках, разделенные двоеточием:

real, allocatable :: subset(:)  
...  
subset = adjclose(5:10)

В этом случае subset будет автоматически выделен как массив с 6 элементами и значениями, соответствующими adjclose от индекса 5 до 10.

По умолчанию срез adjclose(start:end) будет включать все элементы между индексами start и end включительно. Однако вы можете указать произвольный шаг. Например, adjclose(5:10:2) приведет к срезу с элементами 5, 7 и 9. Общий синтаксис для среза массива a с пользовательским шагом:

a(start:end:stride)

где start, end и stride — целочисленные переменные, константы или выражения. start и end могут иметь любое допустимое целочисленное значение, включая ноль и отрицательные значения. stride должно быть ненулевым (положительным или отрицательным) целым числом.

Аналогичные правила применяются для start, end и stride из do-петель:

  1. Если stride не указано, его значение по умолчанию равно 1.
  2. Если start > end и stride > 0, или если start < end и stride < 0, срез представляет собой пустой массив.
  3. Если start == end, например a(5:5), срез представляет собой массив с одним элементом. Будьте осторожны, чтобы не принять это за a(5), которое является скаляром (не массивом).

Кроме того, если start равно нижней границе массива, его можно опустить, и то же самое верно, если end равно верхней границе массива. Например, если мы объявим массив как real :: a(10:20), то все следующие ссылки на массивы и срезы будут соответствовать одному и тому же массиву: a, a(:), a(10:20), a(10:), a(:20), a(::1). Последний синтаксис из этого списка особенно полезен, когда вам нужно разрезать каждый n-й элемент всего массива — это так же просто, как a(::n).

Интермеццо: обращение массива

Давайте напишем функцию reverse, которая принимает реальный одномерный массив в качестве входного аргумента и возвращает тот же массив в обратном порядке. Мы будем использовать правила нарезки массива для выполнения реверсирования.

Мы можем решить это всего за два шага и написать как однострочную функцию (не считая кода объявления). Во-первых, мы знаем, что, поскольку мы просто меняем порядок элементов на обратный, результирующий массив всегда будет того же размера, что и входной массив. Размер также будет соответствовать конечному индексу массива. Во-вторых, как только мы узнаем размер, мы можем разрезать входные данные от последнего к первому и использовать отрицательный шаг, чтобы вернуться назад:

pure function reverse(x)    
  real, intent(in) :: x(:) ! assumed-size input array    
  real :: reverse(size(x)) ! declare reverse with same size as x      
  reverse = x(size(x):1:-1) ! use negative stride to copy backwards  end function reverse

Обратите внимание, что наш входной массив не нужно объявлять allocatable. Это так называемый массив предполагаемого размера, и он будет принимать массив любого размера, переданный вызывающей стороной. Мы можем использовать информацию о размере непосредственно при объявлении результирующего массива.

Вы можете проверить свою новую функцию, дважды перевернув входной массив и сравнив его с самим собой:

print *, all(a == reverse(reverse(a))) ! should always print “T”

Вы можете задаться вопросом, зачем вообще делать эту функцию отдельной, если мы можем просто выполнить x(size(x):1:-1) для реверсирования любого массива. Выделение этой функции reverse() имеет два преимущества. Во-первых, если вам нужно перевернуть массив несколько раз, приведенный выше синтаксис среза вскоре станет громоздким. Каждый раз, когда вы читаете его, в мыслительном процессе делается дополнительный шаг, чтобы понять намерение, стоящее за синтаксисом. Во-вторых, синтаксис среза разрешен только при ссылке на переменную массива, и вы не можете использовать его в выражениях, конструкторах массивов или результатах функций. Напротив, вы можете передать любой из них в качестве аргумента reverse(). Вот почему мы можем сделать такой тест, как all(x == reverse(reverse(x))). Попробуй!

Потратьте некоторое время, чтобы поиграть с различными способами нарезки массивов. Попробуйте разные значения начала, конца и шага. Что произойдет, если вы попытаетесь создать срез, который больше, чем сам массив? Другими словами, можете ли вы ссылаться на массив за пределами границ?

Ссылки на элементы массива за пределами границ:будьте очень осторожны, чтобы не ссылаться на элементы массива, которые находятся за пределами границ! Сам Fortran этого не запрещает, но вы либо получите недопустимое значение, либо ошибку сегментации, отладить которую может быть особенно сложно.

По умолчанию компиляторы не проверяют, возникает ли ссылка за пределами границ во время выполнения, но вы можете включить ее с помощью флага компилятора. Используйте gfortran -fcheck=bounds и ifort -check bounds для компиляторов GNU и Intel Fortran соответственно. Имейте в виду, что это может привести к значительному замедлению работы программ, поэтому лучше всего использовать его во время разработки и отладки, но не в рабочей среде.

Теперь, когда мы понимаем, как работает индексация массива, можно легко рассчитать прирост запасов по всему временному ряду. Это просто вопрос разницы между последним и первым элементом скорректированной цены закрытия, чтобы рассчитать абсолютную прибыль в долларах США:

adjclose = reverse(adjclose)  
gain = (adjclose(size(adjclose)) - adjclose(1))

Здесь я использую встроенную функцию size, которая возвращает целое число элементов для ссылки на последний элемент массива. Как и все остальное, что мы делали раньше, необходимо объявить gain, в данном случае скаляр real. Абсолютная прибыль, однако, говорит нам только о том, насколько акции выросли за определенный период времени, но ничего не говорит нам о том, мал этот рост или велик по сравнению с самой ценой акции. Например, прибыль от 1 до 10 долларов на акцию больше, чем прибыль от 100 до 200 долларов на акцию, при условии, что вы инвестируете 100 долларов в любую акцию. В первом случае вы получите 1000 долларов, а во втором — всего 200 долларов! Чтобы рассчитать относительную прибыль в процентах, мы можем разделить абсолютную прибыль на начальную цену акции, умножить на 100, чтобы получить процент, то есть gain / adjclose(1) * 100. Для краткости я также округлю относительный коэффициент усиления до ближайшего целого числа, используя встроенную функцию nint:

print *, symbols(n), gain, nint(gain / adjclose(1) * 100)

Вывод программы:

2000-01-03 through 2018-05-14   
 Symbol, Gain (USD), Relative gain (%)   
 ---------------------------------   
 AAPL   184.594589            5192   
 AMZN   1512.16003            1692   
 CRAY   9.60000038              56   
 CSCO   1.71649933               4   
 HPQ    1.55270004               7   
 IBM    60.9193039              73   
 INTC   25.8368015              89   
 MSFT   59.4120979             154   
 NVDA   251.745300            6964   
 ORCL   20.3501987              77

Из этого вывода мы видим, что у Amazon была самая большая абсолютная прибыль в размере 1512,16 доллара на акцию, а у Hewlett-Packard была самая маленькая прибыль всего в 1,55 доллара на акцию. Однако относительная прибыль имеет большее значение, чем абсолютная сумма на акцию, потому что она говорит нам, насколько акции выросли по сравнению с их начальной ценой. Глядя на относительный прирост, Nvidia продемонстрировала впечатляющий рост на 6864%, а Apple заняла второе место с 5192%. Акции Cisco Systems (CSCO) с наихудшими показателями роста за этот период времени составили всего 4%.

Если вы клонировали репозиторий цен на акции с Github, скомпилировать и запустить эту программу несложно. В каталоге stock-prices введите:

Make  
./stock_gain

Вы можете прочитать полную программу в stock-prices/src/stock_gain.f90.

Мы рассмотрели множество мельчайших подробностей о том, как работают массивы. Мы будем использовать эти знания в части 3 этой последовательности статей для реализации решений оставшихся двух задач анализа цен на акции.

Сводка

В этой части мы продолжили с того места, где остановились в конце части 1, и углубились в механику выделения и освобождения динамических массивов Fortran. Мы рассмотрели выделение массивов с произвольными диапазонами индексов и увидели, как мы можем нарезать и нарезать массивы с помощью настраиваемых шагов. В этой статье мы также реализовали решение первой из трех задач. В третьей и последней части этой последовательности мы будем использовать встроенные функции Фортрана и арифметику всего массива для вычисления таких показателей, как скользящее среднее значение и стандартное отклонение. Мы будем использовать их для решения оставшихся двух задач — определения того, какие акции являются более рискованными, чем другие в любой момент времени, и определения подходящего времени для покупки или продажи акций.

Если вы хотите узнать больше о книге, загляните на liveBook здесь и посмотрите эту презентацию.

Об авторе:
Милан Чурчич — метеоролог и океанограф. Программист на Фортране с 2006 года, он работал с командами ВМС США и НАСА над разработкой и улучшением моделей прогнозирования системы Земли. Милан является автором двух универсальных библиотек Fortran и в настоящее время работает над стартапом, переносящим Fortran в облако для предсказания погоды и состояния океана.

Первоначально опубликовано на freecontent.manning.com.